diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityControllerCreationContext.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityControllerCreationContext.java index 1651d1bc3a6..1f199646857 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityControllerCreationContext.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityControllerCreationContext.java @@ -20,6 +20,8 @@ public class ActivityControllerCreationContext { Map privacyModulesConfigs; + Set skipPrivacyModules; + @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) Set usedPrivacyModules = EnumSet.noneOf(PrivacyModuleQualifier.class); diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreator.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreator.java index 49e24a06183..ae139798dcf 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreator.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreator.java @@ -27,10 +27,13 @@ import java.util.Arrays; import java.util.Collections; import java.util.EnumMap; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Supplier; @@ -41,6 +44,8 @@ public class ActivityInfrastructureCreator { private static final Logger logger = LoggerFactory.getLogger(ActivityInfrastructureCreator.class); + private static final int MODULE_MAX_SKIP_RATE = 100; + private final ActivityRuleFactory activityRuleFactory; private final Purpose defaultPurpose4; private final Metrics metrics; @@ -75,15 +80,22 @@ Map parse(Account account, GppContext gppContext, final Map activitiesConfiguration = accountPrivacyConfig .map(AccountPrivacyConfig::getActivities) .orElseGet(Collections::emptyMap); + final Map modulesConfigs = accountPrivacyConfig .map(AccountPrivacyConfig::getModules) .orElseGet(Collections::emptyList) .stream() + .filter(Objects::nonNull) .collect(Collectors.toMap( AccountPrivacyModuleConfig::getCode, UnaryOperator.identity(), takeFirstAndLogDuplicates(account.getId()))); + final Set skipPrivacyModules = modulesConfigs.entrySet().stream() + .filter(entry -> shouldSkipPrivacyModule(entry.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toCollection(() -> EnumSet.noneOf(PrivacyModuleQualifier.class))); + return Arrays.stream(Activity.values()).collect(Collectors.toMap( UnaryOperator.identity(), fallbackActivity( @@ -93,6 +105,7 @@ Map parse(Account account, GppContext gppContext, activity, activitiesConfiguration.get(activity), modulesConfigs, + skipPrivacyModules, gppContext, debug)), (oldValue, newValue) -> oldValue, @@ -131,9 +144,14 @@ private Function fallbackActivity( : activityControllerCreator.apply(originalActivity); } + private static boolean shouldSkipPrivacyModule(AccountPrivacyModuleConfig config) { + return ThreadLocalRandom.current().nextInt(MODULE_MAX_SKIP_RATE) < config.getSkipRate(); + } + private ActivityController from(Activity activity, AccountActivityConfiguration activityConfiguration, Map modulesConfigs, + Set skipPrivacyModules, GppContext gppContext, ActivityInfrastructureDebug debug) { @@ -147,6 +165,7 @@ private ActivityController from(Activity activity, final ActivityControllerCreationContext creationContext = ActivityControllerCreationContext.of( activity, modulesConfigs, + skipPrivacyModules, gppContext); final boolean allow = allowFromConfig(activityConfiguration.getAllow()); diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreator.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreator.java index bb68ff60df0..f1854fa7ee9 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreator.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreator.java @@ -5,6 +5,7 @@ import org.prebid.server.activity.infrastructure.creator.ActivityControllerCreationContext; import org.prebid.server.activity.infrastructure.creator.PrivacyModuleCreationContext; import org.prebid.server.activity.infrastructure.creator.privacy.PrivacyModuleCreator; +import org.prebid.server.activity.infrastructure.privacy.AbstainPrivacyModule; import org.prebid.server.activity.infrastructure.privacy.PrivacyModule; import org.prebid.server.activity.infrastructure.privacy.PrivacyModuleQualifier; import org.prebid.server.activity.infrastructure.rule.AndRule; @@ -82,6 +83,10 @@ private static boolean isModuleEnabled(AccountPrivacyModuleConfig accountPrivacy private PrivacyModule createPrivacyModule(PrivacyModuleQualifier privacyModuleQualifier, ActivityControllerCreationContext creationContext) { + if (creationContext.getSkipPrivacyModules().contains(privacyModuleQualifier)) { + return new AbstainPrivacyModule(privacyModuleQualifier); + } + return privacyModulesCreators.get(privacyModuleQualifier) .from(creationContext(privacyModuleQualifier, creationContext)); } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/AbstainPrivacyModule.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/AbstainPrivacyModule.java new file mode 100644 index 00000000000..b1b183cc08b --- /dev/null +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/AbstainPrivacyModule.java @@ -0,0 +1,30 @@ +package org.prebid.server.activity.infrastructure.privacy; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.prebid.server.activity.infrastructure.debug.Loggable; +import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; + +import java.util.Objects; + +public class AbstainPrivacyModule implements PrivacyModule, Loggable { + + private final PrivacyModuleQualifier privacyModuleQualifier; + + public AbstainPrivacyModule(PrivacyModuleQualifier privacyModuleQualifier) { + this.privacyModuleQualifier = Objects.requireNonNull(privacyModuleQualifier); + } + + @Override + public Result proceed(ActivityInvocationPayload activityInvocationPayload) { + return Result.ABSTAIN; + } + + @Override + public JsonNode asLogEntry(ObjectMapper mapper) { + return mapper.createObjectNode() + .put("privacy_module", privacyModuleQualifier.moduleName()) + .put("skipped", true) + .put("result", Result.ABSTAIN.name()); + } +} diff --git a/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountPrivacyModuleConfig.java b/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountPrivacyModuleConfig.java index eea8b4f644e..98352cf6bee 100644 --- a/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountPrivacyModuleConfig.java +++ b/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountPrivacyModuleConfig.java @@ -23,6 +23,9 @@ public sealed interface AccountPrivacyModuleConfig permits PrivacyModuleQualifier getCode(); + @JsonProperty("skipRate") + int getSkipRate(); + @JsonProperty Boolean enabled(); } diff --git a/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSCustomLogicModuleConfig.java b/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSCustomLogicModuleConfig.java index 2506321e031..92ccff4c3e9 100644 --- a/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSCustomLogicModuleConfig.java +++ b/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSCustomLogicModuleConfig.java @@ -17,6 +17,8 @@ public class AccountUSCustomLogicModuleConfig implements AccountPrivacyModuleCon @Accessors(fluent = true) Boolean enabled; + int skipRate; + Config config; @Override diff --git a/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSNatModuleConfig.java b/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSNatModuleConfig.java index fa51135791b..1c6adf7c00d 100644 --- a/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSNatModuleConfig.java +++ b/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountUSNatModuleConfig.java @@ -14,6 +14,8 @@ public class AccountUSNatModuleConfig implements AccountPrivacyModuleConfig { @Accessors(fluent = true) Boolean enabled; + int skipRate; + Config config; @Override diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountGppConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountGppConfig.groovy index add9834d5c5..bb4e0ab0b74 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountGppConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountGppConfig.groovy @@ -8,5 +8,6 @@ class AccountGppConfig { PrivacyModule code Boolean enabled + Integer skipRate GppModuleConfig config } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ActivityInfrastructure.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ActivityInfrastructure.groovy index 1325a65e219..0d1a3ba16ad 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ActivityInfrastructure.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ActivityInfrastructure.groovy @@ -17,7 +17,7 @@ class ActivityInfrastructure { RuleConfiguration ruleConfiguration Boolean allowByDefault Boolean allowed - String result + RuleResult result String region String country } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/And.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/And.groovy index 4072d3e1f62..73799db84d5 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/And.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/And.groovy @@ -1,11 +1,18 @@ package org.prebid.server.functional.model.response.auction +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.PrivacyModule @ToString(includeNames = true, ignoreNulls = true) @EqualsAndHashCode +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) class And { List and + PrivacyModule privacyModule + Boolean skipped + RuleResult result } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/RuleResult.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/RuleResult.groovy new file mode 100644 index 00000000000..62b4256bb66 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/RuleResult.groovy @@ -0,0 +1,6 @@ +package org.prebid.server.functional.model.response.auction + +enum RuleResult { + + ALLOW, DISALLOW, ABSTAIN +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy index e0c3b279200..e42fa3fb594 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy @@ -23,14 +23,21 @@ import static org.prebid.server.functional.model.pricefloors.Country.CAN import static org.prebid.server.functional.model.pricefloors.Country.USA import static org.prebid.server.functional.model.request.GppSectionId.US_CA_V1 import static org.prebid.server.functional.model.request.GppSectionId.US_CO_V1 +import static org.prebid.server.functional.model.request.GppSectionId.US_NAT_V1 import static org.prebid.server.functional.model.request.auction.ActivityType.FETCH_BIDS +import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_EIDS import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_TID import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD import static org.prebid.server.functional.model.request.auction.Condition.ConditionType.BIDDER +import static org.prebid.server.functional.model.request.auction.PrivacyModule.ALL +import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_CUSTOM_LOGIC import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_GENERAL import static org.prebid.server.functional.model.request.auction.TraceLevel.BASIC import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.model.response.auction.RuleResult.ABSTAIN +import static org.prebid.server.functional.model.response.auction.RuleResult.ALLOW +import static org.prebid.server.functional.model.response.auction.RuleResult.DISALLOW import static org.prebid.server.functional.util.privacy.model.State.ALABAMA import static org.prebid.server.functional.util.privacy.model.State.ARIZONA @@ -42,6 +49,9 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { private static final def PROCESSING_ACTIVITY_TRACE = ["Processing rule."] + private final static Integer MIN_PERCENT_AB = 0 + private final static Integer MAX_PERCENT_AB = 100 + def "PBS auction shouldn't log info about activity in response when ext.prebid.trace=null"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String @@ -97,7 +107,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { country: USA.ISOAlpha3)) assert fetchBidsActivity.ruleConfiguration.every { it == null } assert fetchBidsActivity.allowByDefault.contains(activity.defaultAction) - assert fetchBidsActivity.result.contains("DISALLOW") + assert fetchBidsActivity.result.contains(DISALLOW) assert fetchBidsActivity.country.every { it == null } assert fetchBidsActivity.region.every { it == null } } @@ -137,7 +147,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { country: USA.ISOAlpha3)) assert fetchBidsActivity.allowByDefault.contains(activity.defaultAction) assert fetchBidsActivity.ruleConfiguration.every { it == null } - assert fetchBidsActivity.result.contains("ALLOW") + assert fetchBidsActivity.result.contains(ALLOW) assert fetchBidsActivity.country.every { it == null } assert fetchBidsActivity.region.every { it == null } @@ -237,7 +247,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { gpc: gpc, geoCodes: [new GeoCode(country: CAN, region: ARIZONA.abbreviation)])) assert fetchBidsActivity.allowByDefault.contains(activity.defaultAction) - assert fetchBidsActivity.result.contains("ABSTAIN") + assert fetchBidsActivity.result.contains(ABSTAIN) assert fetchBidsActivity.country.every { it == null } assert fetchBidsActivity.region.every { it == null } @@ -343,7 +353,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { assert fetchBidsActivity.ruleConfiguration.contains(new RuleConfiguration( and: [new And(and: ["USNatDefault. Precomputed result: ABSTAIN."])])) assert fetchBidsActivity.allowByDefault.contains(activity.defaultAction) - assert fetchBidsActivity.result.contains("ABSTAIN") + assert fetchBidsActivity.result.contains(ABSTAIN) assert fetchBidsActivity.country.every { it == null } assert fetchBidsActivity.region.every { it == null } @@ -398,10 +408,288 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { allow << [false, true] } - private List getActivityByName(List activityInfrastructures, - ActivityType activity) { + def "PBS auction should log info about activity in response when ext.prebid.trace=verbose and skipRate=#skipRate"() { + given: "Default bid request" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + ext.prebid.trace = VERBOSE + regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + regs.gppSid = [US_NAT_V1.intValue] + } + + and: "Set up activities" + def condition = Condition.baseCondition.tap { + it.gppSid = [US_NAT_V1.intValue] + } + def activityRule = ActivityRule.getDefaultActivityRule(condition).tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + def activity = Activity.getDefaultActivity([activityRule]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, activity) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true, skipRate: skipRate) + + and: "Save account with allow activities setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes auction requests" + def bidResponse = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have empty EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + + verifyAll { + !genericBidderRequest.user.eids + !genericBidderRequest.user?.ext?.eids + } + + and: "Bid response should contain info about triggered activity in debug" + def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure + def ruleConfigurations = findProcessingRule(infrastructure, TRANSMIT_EIDS).ruleConfiguration.and + assert ruleConfigurations.size() == 1 + assert ruleConfigurations.first.and.every { it.contains(DISALLOW.toString()) } + + and: "Should not contain information that module was skipped" + verifyAll(ruleConfigurations.first) { + !it.privacyModule + !it.skipped + !it.result + } + + where: + skipRate << [null, MIN_PERCENT_AB] + } + + def "PBS auction should log info about module skip in response when ext.prebid.trace=verbose and skipRate is max"() { + given: "Default bid request" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + ext.prebid.trace = VERBOSE + regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + regs.gppSid = [US_NAT_V1.intValue] + } + + and: "Set up activities" + def condition = Condition.baseCondition.tap { + it.gppSid = [US_NAT_V1.intValue] + } + def activityRule = ActivityRule.getDefaultActivityRule(condition).tap { + it.privacyRegulation = [ALL] + } + def activity = Activity.getDefaultActivity([activityRule]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, activity) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: code, enabled: true, skipRate: MAX_PERCENT_AB) + + and: "Save account with allow activities setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes auction requests" + def bidResponse = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + + and: "Bid response should not contain info about triggered activity in debug" + def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure + def ruleConfigurations = findProcessingRule(infrastructure, TRANSMIT_EIDS).ruleConfiguration.and + assert ruleConfigurations.size() == 1 + assert ruleConfigurations.first.and.every { it == null } + + and: "Should contain information that module was skipped" + verifyAll(ruleConfigurations.first) { + it.privacyModule == code + it.skipped == true + it.result == ABSTAIN + } + + where: + code | defaultAction + IAB_US_GENERAL | false + IAB_US_GENERAL | true + IAB_US_CUSTOM_LOGIC | false + IAB_US_CUSTOM_LOGIC | true + } + + def "PBS auction should log consistently for each activity about skips modules in response"() { + given: "Default bid request" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + ext.prebid.trace = VERBOSE + regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + regs.gppSid = [US_NAT_V1.intValue] + } + + and: "Set up activities" + def condition = Condition.baseCondition.tap { + it.gppSid = [US_NAT_V1.intValue] + } + def activityRule = ActivityRule.getDefaultActivityRule(condition).tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + def activity = Activity.getDefaultActivity([activityRule]) + def activities = new AllowActivities( + syncUser: activity, + fetchBids: activity, + enrichUfpd: activity, + reportAnalytics: activity, + transmitUfpd: activity, + transmitEids: activity, + transmitPreciseGeo: activity, + transmitTid: activity, + ) + + and: "Account gpp configuration" + def skipRate = PBSUtils.getRandomNumber(MIN_PERCENT_AB, MAX_PERCENT_AB) + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true, skipRate: skipRate) + + and: "Save account with allow activities setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes auction requests" + def bidResponse = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should log consistently for each activity about skips" + def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure + def fetchBidsLogs = findProcessingRule(infrastructure, FETCH_BIDS).ruleConfiguration.and + def transmitUfpdLogs = findProcessingRule(infrastructure, TRANSMIT_UFPD).ruleConfiguration.and + def transmitEidsLogs = findProcessingRule(infrastructure, TRANSMIT_EIDS).ruleConfiguration.and + def transmitPreciseGeoLogs = findProcessingRule(infrastructure, TRANSMIT_PRECISE_GEO).ruleConfiguration.and + def transmitTidLogs = findProcessingRule(infrastructure, TRANSMIT_TID).ruleConfiguration.and + verifyAll ([fetchBidsLogs, transmitUfpdLogs, transmitEidsLogs, transmitPreciseGeoLogs, transmitTidLogs]) { + it.privacyModule.toSet().size() == 1 + it.skipped.toSet().size() == 1 + it.result.toSet().size() == 1 + } + } + + def "PBS auction shouldn't emit errors or warnings when skip rate is at minimum boundary"() { + given: "A bid request with verbose tracing and GPC disallow logic" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + ext.prebid.trace = VERBOSE + regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + regs.gppSid = [US_NAT_V1.intValue] + } + + and: "An activity rule with GPP SID and privacy regulation setup" + def condition = Condition.baseCondition.tap { + it.gppSid = [US_NAT_V1.intValue] + } + def activityRule = ActivityRule.getDefaultActivityRule(condition).tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + def activity = Activity.getDefaultActivity([activityRule]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, activity) + + and: "Account GPP configuration with minimum skip rate" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true, skipRate: Integer.MIN_VALUE) + + and: "Save the account with configured activities and privacy module" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes the auction request" + def bidResponse = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors or warnings" + assert !bidResponse.ext?.errors + assert !bidResponse.ext?.warnings + + and: "Bid response should contain info about triggered activity in debug" + def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure + def ruleConfigurations = findProcessingRule(infrastructure, TRANSMIT_EIDS).ruleConfiguration.and + assert ruleConfigurations.size() == 1 + assert ruleConfigurations.first.and.every { it.contains(DISALLOW.toString()) } + + and: "Should not contain information that module was skipped" + verifyAll(ruleConfigurations.first) { + !it.privacyModule + !it.skipped + !it.result + } + + and: "Generic bidder request should have empty EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + + verifyAll { + !genericBidderRequest.user.eids + !genericBidderRequest.user?.ext?.eids + } + } + + def "PBS auction shouldn't emit errors or warnings when skip rate is at maximum boundary"() { + given: "A bid request with verbose tracing and GPC disallow logic" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + ext.prebid.trace = VERBOSE + regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + regs.gppSid = [US_NAT_V1.intValue] + } + + and: "An activity rule with GPP SID and privacy regulation setup" + def condition = Condition.baseCondition.tap { + it.gppSid = [US_NAT_V1.intValue] + } + def activityRule = ActivityRule.getDefaultActivityRule(condition).tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + def activity = Activity.getDefaultActivity([activityRule]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, activity) + + and: "Account GPP configuration with maximum skip rate" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true, skipRate: Integer.MAX_VALUE) + + and: "Save the account with configured activities and privacy module" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes the auction request" + def bidResponse = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors or warnings" + assert !bidResponse.ext?.errors + assert !bidResponse.ext?.warnings + + and: "Bid response should not contain info about triggered activity in debug" + def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure + def ruleConfigurations = findProcessingRule(infrastructure, TRANSMIT_EIDS).ruleConfiguration.and + assert ruleConfigurations.size() == 1 + assert ruleConfigurations.first.and.every { it == null } + + and: "Should contain information that module was skipped" + verifyAll(ruleConfigurations.first) { + it.privacyModule == IAB_US_GENERAL + it.skipped == true + it.result == ABSTAIN + } + + and: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + } + + private static List getActivityByName(List activityInfrastructures, + ActivityType activity) { def firstIndex = activityInfrastructures.findLastIndexOf { it -> it.activity == activity } def lastIndex = activityInfrastructures.findIndexOf { it -> it.activity == activity } activityInfrastructures[new IntRange(true, firstIndex, lastIndex)] } + + private static ActivityInfrastructure findProcessingRule(List infrastructures, ActivityType activity) { + def matchingActivities = getActivityByName(infrastructures, activity) + .findAll { PROCESSING_ACTIVITY_TRACE.contains(it.description) } + + if (matchingActivities.size() != 1) { + throw new IllegalStateException("Expected a single processing activity, but found ${matchingActivities.size()}") + } + + matchingActivities.first() + } } diff --git a/src/test/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreatorTest.java b/src/test/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreatorTest.java index 661d63eaa4d..bdfa42035da 100644 --- a/src/test/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreatorTest.java +++ b/src/test/java/org/prebid/server/activity/infrastructure/creator/ActivityInfrastructureCreatorTest.java @@ -3,6 +3,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.activity.Activity; @@ -22,6 +23,7 @@ import org.prebid.server.settings.model.PurposeEid; import org.prebid.server.settings.model.Purposes; import org.prebid.server.settings.model.activity.AccountActivityConfiguration; +import org.prebid.server.settings.model.activity.privacy.AccountUSCustomLogicModuleConfig; import org.prebid.server.settings.model.activity.privacy.AccountUSNatModuleConfig; import org.prebid.server.settings.model.activity.rule.AccountActivityConditionsRuleConfig; @@ -36,6 +38,7 @@ import static org.mockito.ArgumentMatchers.same; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; +import static org.prebid.server.activity.infrastructure.privacy.PrivacyModuleQualifier.US_NAT; @ExtendWith(MockitoExtension.class) public class ActivityInfrastructureCreatorTest { @@ -91,8 +94,8 @@ public void parseShouldSkipPrivacyModulesDuplicatesAndEmitWarnings() { .activities(Map.of(Activity.SYNC_USER, AccountActivityConfiguration.of( null, singletonList(AccountActivityConditionsRuleConfig.of(null, null))))) .modules(asList( - AccountUSNatModuleConfig.of(null, null), - AccountUSNatModuleConfig.of(null, null))) + AccountUSNatModuleConfig.of(null, 0, null), + AccountUSNatModuleConfig.of(null, 0, null))) .build()) .build(); @@ -104,6 +107,29 @@ null, singletonList(AccountActivityConditionsRuleConfig.of(null, null))))) verify(metrics).updateAlertsMetrics(eq(MetricName.general)); } + @Test + public void parseShouldPopulateSkipConfigForModules() { + // given + final Account account = Account.builder() + .privacy(AccountPrivacyConfig.builder() + .activities(Map.of(Activity.SYNC_USER, AccountActivityConfiguration.of( + null, singletonList(AccountActivityConditionsRuleConfig.of(null, null))))) + .modules(asList( + AccountUSNatModuleConfig.of(null, 100, null), + AccountUSCustomLogicModuleConfig.of(null, 0, null))) + .build()) + .build(); + + // when + creator.parse(account, null, debug); + + // then + final ArgumentCaptor captor = + ArgumentCaptor.forClass(ActivityControllerCreationContext.class); + verify(activityRuleFactory).from(any(), captor.capture()); + assertThat(captor.getValue().getSkipPrivacyModules()).containsOnly(US_NAT); + } + @Test public void parseShouldReturnExpectedResult() { // given diff --git a/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreatorTest.java b/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreatorTest.java index d18a7b15e4b..3d10dd5b9a4 100644 --- a/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreatorTest.java +++ b/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/uscustomlogic/USCustomLogicModuleCreatorTest.java @@ -248,7 +248,7 @@ private static PrivacyModuleCreationContext givenCreationContext(List s return PrivacyModuleCreationContext.of( Activity.CALL_BIDDER, - AccountUSCustomLogicModuleConfig.of(true, config), + AccountUSCustomLogicModuleConfig.of(true, 0, config), GppContextCreator.from(null, sectionsIds).build().getGppContext()); } diff --git a/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreatorTest.java b/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreatorTest.java index 270f0f47d61..5dce2c5fb18 100644 --- a/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreatorTest.java +++ b/src/test/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreatorTest.java @@ -126,7 +126,7 @@ private static PrivacyModuleCreationContext givenCreationContext(List s return PrivacyModuleCreationContext.of( Activity.CALL_BIDDER, - AccountUSNatModuleConfig.of(true, AccountUSNatModuleConfig.Config.of(skipSectionsIds)), + AccountUSNatModuleConfig.of(true, 0, AccountUSNatModuleConfig.Config.of(skipSectionsIds)), GppContextCreator.from(null, sectionsIds).build().getGppContext()); } } diff --git a/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/ConditionsRuleCreatorTest.java b/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/ConditionsRuleCreatorTest.java index 2bd8609ee8e..b935ccf46c4 100644 --- a/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/ConditionsRuleCreatorTest.java +++ b/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/ConditionsRuleCreatorTest.java @@ -85,6 +85,6 @@ private static BidRequest givenBidRequest(String country, String region, String } private static ActivityControllerCreationContext creationContext(GppContext gppContext) { - return ActivityControllerCreationContext.of(null, null, gppContext); + return ActivityControllerCreationContext.of(null, null, null, gppContext); } } diff --git a/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreatorTest.java b/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreatorTest.java index 2484824be9f..095c52ed14e 100644 --- a/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreatorTest.java +++ b/src/test/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreatorTest.java @@ -16,8 +16,10 @@ import org.prebid.server.settings.model.activity.rule.AccountActivityPrivacyModulesRuleConfig; import java.util.Map; +import java.util.Set; import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; @@ -56,7 +58,7 @@ public void fromShouldCreateDefaultRuleIfNoneOfConfiguredPrivacyModulesMatches() final AccountActivityPrivacyModulesRuleConfig config = AccountActivityPrivacyModulesRuleConfig.of( singletonList("not_configured")); final ActivityControllerCreationContext creationContext = creationContext(Map.of( - PrivacyModuleQualifier.US_NAT, AccountUSNatModuleConfig.of(null, null))); + PrivacyModuleQualifier.US_NAT, AccountUSNatModuleConfig.of(null, 0, null))); // when final Rule rule = target.from(config, creationContext); @@ -70,7 +72,7 @@ public void fromShouldCreateRuleWithAllConfiguredPrivacyModules() { // given final AccountActivityPrivacyModulesRuleConfig config = AccountActivityPrivacyModulesRuleConfig.of( singletonList("*")); - final AccountPrivacyModuleConfig moduleConfig = AccountUSNatModuleConfig.of(null, null); + final AccountPrivacyModuleConfig moduleConfig = AccountUSNatModuleConfig.of(null, 0, null); final ActivityControllerCreationContext creationContext = creationContext( Map.of(PrivacyModuleQualifier.US_NAT, moduleConfig)); @@ -89,7 +91,7 @@ public void fromShouldCreateRuleWithAllConfiguredPrivacyModulesThatMatches() { // given final AccountActivityPrivacyModulesRuleConfig config = AccountActivityPrivacyModulesRuleConfig.of( singletonList("iab.*")); - final AccountPrivacyModuleConfig moduleConfig = AccountUSNatModuleConfig.of(null, null); + final AccountPrivacyModuleConfig moduleConfig = AccountUSNatModuleConfig.of(null, 0, null); final ActivityControllerCreationContext creationContext = creationContext( Map.of(PrivacyModuleQualifier.US_NAT, moduleConfig)); @@ -108,7 +110,7 @@ public void fromShouldCreateRuleAndModifyContextWithUsedPrivacyModules() { // given final AccountActivityPrivacyModulesRuleConfig config = AccountActivityPrivacyModulesRuleConfig.of( singletonList(PrivacyModuleQualifier.US_NAT.moduleName())); - final AccountPrivacyModuleConfig moduleConfig = AccountUSNatModuleConfig.of(null, null); + final AccountPrivacyModuleConfig moduleConfig = AccountUSNatModuleConfig.of(null, 0, null); final ActivityControllerCreationContext creationContext = creationContext( Map.of(PrivacyModuleQualifier.US_NAT, moduleConfig)); @@ -129,7 +131,7 @@ public void fromShouldSkipAlreadyUsedPrivacyModule() { final AccountActivityPrivacyModulesRuleConfig config = AccountActivityPrivacyModulesRuleConfig.of( singletonList(PrivacyModuleQualifier.US_NAT.moduleName())); final ActivityControllerCreationContext creationContext = creationContext(Map.of( - PrivacyModuleQualifier.US_NAT, AccountUSNatModuleConfig.of(true, null))); + PrivacyModuleQualifier.US_NAT, AccountUSNatModuleConfig.of(true, 0, null))); creationContext.use(PrivacyModuleQualifier.US_NAT); // when @@ -145,7 +147,25 @@ public void fromShouldSkipDisabledPrivacyModule() { final AccountActivityPrivacyModulesRuleConfig config = AccountActivityPrivacyModulesRuleConfig.of( singletonList(PrivacyModuleQualifier.US_NAT.moduleName())); final ActivityControllerCreationContext creationContext = creationContext(Map.of( - PrivacyModuleQualifier.US_NAT, AccountUSNatModuleConfig.of(false, null))); + PrivacyModuleQualifier.US_NAT, AccountUSNatModuleConfig.of(false, 0, null))); + + // when + final Rule rule = target.from(config, creationContext); + + // then + assertThat(rule.proceed(null)).isEqualTo(Rule.Result.ABSTAIN); + } + + @Test + public void fromShouldDisableSkippedPrivacyModule() { + // given + final AccountActivityPrivacyModulesRuleConfig config = AccountActivityPrivacyModulesRuleConfig.of( + singletonList(PrivacyModuleQualifier.US_NAT.moduleName())); + final ActivityControllerCreationContext creationContext = ActivityControllerCreationContext.of( + null, + Map.of(PrivacyModuleQualifier.US_NAT, AccountUSNatModuleConfig.of(true, 0, null)), + Set.of(PrivacyModuleQualifier.US_NAT), + null); // when final Rule rule = target.from(config, creationContext); @@ -162,7 +182,7 @@ public void fromShouldSkipPrivacyModuleWithoutCreator() { final AccountActivityPrivacyModulesRuleConfig config = AccountActivityPrivacyModulesRuleConfig.of( singletonList(PrivacyModuleQualifier.US_NAT.moduleName())); final ActivityControllerCreationContext creationContext = creationContext(Map.of( - PrivacyModuleQualifier.US_NAT, AccountUSNatModuleConfig.of(null, null))); + PrivacyModuleQualifier.US_NAT, AccountUSNatModuleConfig.of(null, 0, null))); // when final Rule rule = target.from(config, creationContext); @@ -174,6 +194,6 @@ public void fromShouldSkipPrivacyModuleWithoutCreator() { private static ActivityControllerCreationContext creationContext( Map modulesConfigs) { - return ActivityControllerCreationContext.of(null, modulesConfigs, null); + return ActivityControllerCreationContext.of(null, modulesConfigs, emptySet(), null); } }