diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java index 933ba724f0f..e8409ecb359 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java @@ -10,7 +10,6 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdTcfMask; -import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; @@ -38,19 +37,16 @@ public class TcfEnforcement implements PrivacyEnforcement { private final TcfDefinerService tcfDefinerService; private final UserFpdTcfMask userFpdTcfMask; - private final BidderCatalog bidderCatalog; private final Metrics metrics; private final boolean lmtEnforce; public TcfEnforcement(TcfDefinerService tcfDefinerService, UserFpdTcfMask userFpdTcfMask, - BidderCatalog bidderCatalog, Metrics metrics, boolean lmtEnforce) { this.tcfDefinerService = Objects.requireNonNull(tcfDefinerService); this.userFpdTcfMask = Objects.requireNonNull(userFpdTcfMask); - this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.metrics = Objects.requireNonNull(metrics); this.lmtEnforce = lmtEnforce; } @@ -67,6 +63,7 @@ public Future> enforce(AuctionContext auctionContext, final MetricName requestType = auctionContext.getRequestTypeMetric(); final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure(); + final Account account = auctionContext.getAccount(); final Set bidders = results.stream() .map(BidderPrivacyResult::getRequestBidder) .collect(Collectors.toSet()); @@ -75,9 +72,15 @@ public Future> enforce(AuctionContext auctionContext, bidders, VendorIdResolver.of(aliases), auctionContext.getPrivacyContext().getTcfContext(), - accountGdprConfig(auctionContext.getAccount())) + accountGdprConfig(account)) .map(TcfResponse::getActions) - .map(enforcements -> updateMetrics(activityInfrastructure, enforcements, aliases, requestType, results)) + .map(enforcements -> updateMetrics( + activityInfrastructure, + enforcements, + aliases, + requestType, + results, + account)) .map(enforcements -> applyEnforcements(enforcements, results)); } @@ -86,11 +89,16 @@ private static AccountGdprConfig accountGdprConfig(Account account) { return privacyConfig != null ? privacyConfig.getGdpr() : null; } + private static boolean hasBuyerUid(User user) { + return user != null && user.getBuyeruid() != null; + } + private Map updateMetrics(ActivityInfrastructure activityInfrastructure, Map enforcements, BidderAliases aliases, MetricName requestType, - List results) { + List results, + Account account) { // Metrics should represent real picture of the bidding process, so if bidder request is blocked // by privacy then no reason to increment another metrics, like geo masked, etc. @@ -120,6 +128,10 @@ private Map updateMetrics(ActivityInfrastructu requestBlocked, isLmtEnforcedAndEnabled); + if (hasBuyerUid(user) && ufpdRemoved) { + metrics.updateAdapterRequestBuyerUidScrubbedMetrics(bidder, account); + } + if (ufpdRemoved) { logger.warn("The UFPD fields have been removed due to a consent check."); } diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index 9ea3d15f0fc..02df8cafa81 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -72,6 +72,7 @@ public enum MetricName { unknown_error, err, networkerr, + buyeruid_scrubbed, // bids validation warn, diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index 52964a9f8b0..68085cb5556 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -285,6 +285,13 @@ public void updateAdapterRequestTypeAndNoCookieMetrics(String bidder, MetricName } } + public void updateAdapterRequestBuyerUidScrubbedMetrics(String bidder, Account account) { + forAdapter(bidder).request().incCounter(MetricName.buyeruid_scrubbed); + if (accountMetricsVerbosityResolver.forAccount(account).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { + forAccount(account.getId()).adapter().forAdapter(bidder).request().incCounter(MetricName.buyeruid_scrubbed); + } + } + public void updateAdapterResponseTime(String bidder, Account account, int responseTime) { final AdapterTypeMetrics adapterTypeMetrics = forAdapter(bidder); adapterTypeMetrics.updateTimer(MetricName.request_time, responseTime); diff --git a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java index c7cac1ecf64..601de9e5f11 100644 --- a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java @@ -393,11 +393,10 @@ CoppaEnforcement coppaEnforcement(UserFpdCoppaMask userFpdCoppaMask, Metrics met @Bean TcfEnforcement tcfEnforcement(TcfDefinerService tcfDefinerService, UserFpdTcfMask userFpdTcfMask, - BidderCatalog bidderCatalog, Metrics metrics, @Value("${lmt.enforce}") boolean lmtEnforce) { - return new TcfEnforcement(tcfDefinerService, userFpdTcfMask, bidderCatalog, metrics, lmtEnforce); + return new TcfEnforcement(tcfDefinerService, userFpdTcfMask, metrics, lmtEnforce); } @Data diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy index 351875e5f90..7302ad79aed 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy @@ -3,6 +3,8 @@ package org.prebid.server.functional.tests.privacy import org.mockserver.model.Delay import org.prebid.server.functional.model.ChannelType import org.prebid.server.functional.model.config.AccountGdprConfig +import org.prebid.server.functional.model.config.AccountMetricsConfig +import org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel import org.prebid.server.functional.model.config.PurposeConfig import org.prebid.server.functional.model.config.PurposeEnforcement import org.prebid.server.functional.model.pricefloors.Country @@ -21,6 +23,8 @@ import java.time.Instant import static org.prebid.server.functional.model.ChannelType.PBJS import static org.prebid.server.functional.model.ChannelType.WEB import static org.prebid.server.functional.model.bidder.BidderName.GENERIC + +import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.DETAILED import static org.prebid.server.functional.model.config.Purpose.P1 import static org.prebid.server.functional.model.config.Purpose.P2 import static org.prebid.server.functional.model.config.Purpose.P4 @@ -960,4 +964,139 @@ class GdprAuctionSpec extends PrivacyBaseSpec { null | BULGARIA | null | null | [:] null | null | null | null | [:] } + + def "PBS auction shouldn't update buyeruid scrubbed metrics when user.buyeruid not requested"() { + given: "Default bid requests with personal data" + def bidRequest = bidRequestWithPersonalData.tap { + regs.gdpr = 1 + user.buyeruid = null + user.ext.consent = new TcfConsent.Builder().build() + ext.prebid.trace = VERBOSE + } + + and: "Save account config with requireConsent into DB" + def purposes = [(P2): new PurposeConfig(enforcePurpose: NO, enforceVendors: false)] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig).tap { + config.metrics = new AccountMetricsConfig(verbosityLevel: verbosityLevel) + } + accountDao.save(account) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction requests" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should mask user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics buyeruid scrubbed shouldn't be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert !metrics["adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] + assert !metrics["account.${account.uuid}.adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] + + where: + verbosityLevel << [DETAILED, AccountMetricsVerbosityLevel.BASIC] + } + + def "PBS auction should update buyeruid scrubbed general metrics when user.buyeruid requested and verbosityLevel BASIC"() { + given: "Default bid requests with personal data" + def bidRequest = bidRequestWithPersonalData.tap { + regs.gdpr = 1 + user.ext.consent = new TcfConsent.Builder().build() + ext.prebid.trace = VERBOSE + } + + and: "Save account config with requireConsent into DB" + def purposes = [(P2): new PurposeConfig(enforcePurpose: NO, enforceVendors: false)] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig).tap { + config.metrics = new AccountMetricsConfig(verbosityLevel: AccountMetricsVerbosityLevel.BASIC) + } + accountDao.save(account) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction requests" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should mask user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics buyeruid scrubbed should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics["adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] == 1 + + and: "Account metric shouldn't be populated" + assert !metrics["account.${account.uuid}.adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] + } + + def "PBS auction should update buyeruid scrubbed general and account metrics when user.buyeruid requested and verbosityLevel DETAILED"() { + given: "Default bid requests with personal data" + def bidRequest = bidRequestWithPersonalData.tap { + regs.gdpr = 1 + user.ext.consent = new TcfConsent.Builder().build() + ext.prebid.trace = VERBOSE + } + + and: "Save account config with requireConsent into DB" + def purposes = [(P2): new PurposeConfig(enforcePurpose: NO, enforceVendors: false)] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig).tap { + config.metrics = new AccountMetricsConfig(verbosityLevel: DETAILED) + } + accountDao.save(account) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction requests" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should mask user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics buyeruid scrubbed should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics["adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] == 1 + assert metrics["account.${account.uuid}.adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] == 1 + } } diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java index 0ed33b4cce3..dcecd091334 100644 --- a/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java @@ -16,7 +16,6 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdTcfMask; -import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.gdpr.TcfDefinerService; @@ -44,6 +43,7 @@ import static org.mockito.ArgumentMatchers.same; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @@ -54,8 +54,6 @@ public class TcfEnforcementTest { @Mock(strictness = LENIENT) private UserFpdTcfMask userFpdTcfMask; @Mock - private BidderCatalog bidderCatalog; - @Mock private Metrics metrics; private TcfEnforcement target; @@ -73,7 +71,7 @@ public void setUp() { given(userFpdTcfMask.maskDevice(any(), anyBoolean(), anyBoolean(), anyBoolean())) .willAnswer(invocation -> invocation.getArgument(0)); - target = new TcfEnforcement(tcfDefinerService, userFpdTcfMask, bidderCatalog, metrics, true); + target = new TcfEnforcement(tcfDefinerService, userFpdTcfMask, metrics, true); given(aliases.resolveBidder(anyString())) .willAnswer(invocation -> invocation.getArgument(0)); @@ -169,6 +167,44 @@ public void enforceShouldEmitExpectedMetricsWhenUserHasPrivacyData() { verifyMetric("bidder2", true, false, false, false, false, false); } + @Test + public void enforceShouldEmitBuyerUidScrubbedMetricsWhenUserHasBuyerUid() { + // give + givenPrivacyEnforcementActions(Map.of( + "bidder0", givenEnforcementAction(PrivacyEnforcementAction::setMaskGeo), + "bidder1", givenEnforcementAction(PrivacyEnforcementAction::setMaskDeviceInfo), + "bidder2", givenEnforcementAction(PrivacyEnforcementAction::setRemoveUserFpd), + "bidder3", givenEnforcementAction(PrivacyEnforcementAction::setMaskDeviceInfo), + "bidder4", givenEnforcementAction(PrivacyEnforcementAction::setRemoveUserFpd), + "bidder5", givenEnforcementAction(PrivacyEnforcementAction::setMaskDeviceInfo))); + + final User givenUserWithoutBuyerUid = givenUserWithPrivacyData(); + + final User givenUserWithBuyerUid = givenUserWithoutBuyerUid.toBuilder() + .buyeruid("buyeruid") + .build(); + + final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithNoPrivacyData()); + final List initialResults = List.of( + givenBidderPrivacyResult("bidder0", givenUserWithBuyerUid, givenDeviceWithNoPrivacyData()), + givenBidderPrivacyResult("bidder1", givenUserWithBuyerUid, givenDeviceWithNoPrivacyData()), + givenBidderPrivacyResult("bidder2", givenUserWithoutBuyerUid, givenDeviceWithNoPrivacyData()), + givenBidderPrivacyResult("bidder3", givenUserWithoutBuyerUid, givenDeviceWithPrivacyData()), + givenBidderPrivacyResult("bidder4", givenUserWithBuyerUid, givenDeviceWithNoPrivacyData()), + givenBidderPrivacyResult("bidder5", givenUserWithBuyerUid, givenDeviceWithPrivacyData())); + + // when + target.enforce(auctionContext, aliases, initialResults); + + // then + verify(metrics, never()).updateAdapterRequestBuyerUidScrubbedMetrics(eq("bidder0"), any()); + verify(metrics, never()).updateAdapterRequestBuyerUidScrubbedMetrics(eq("bidder1"), any()); + verify(metrics, never()).updateAdapterRequestBuyerUidScrubbedMetrics(eq("bidder2"), any()); + verify(metrics, never()).updateAdapterRequestBuyerUidScrubbedMetrics(eq("bidder3"), any()); + verify(metrics).updateAdapterRequestBuyerUidScrubbedMetrics(eq("bidder4"), any()); + verify(metrics).updateAdapterRequestBuyerUidScrubbedMetrics(eq("bidder5"), any()); + } + @Test public void enforceShouldEmitExpectedMetricsWhenDeviceHavePrivacyData() { // give @@ -265,7 +301,7 @@ public void enforceShouldNotEmitPrivacyLmtMetricWhenLmtNotEnforced() { final List initialResults = List.of( givenBidderPrivacyResult("bidder", givenUserWithPrivacyData(), device)); - target = new TcfEnforcement(tcfDefinerService, userFpdTcfMask, bidderCatalog, metrics, false); + target = new TcfEnforcement(tcfDefinerService, userFpdTcfMask, metrics, false); // when target.enforce(auctionContext, aliases, initialResults); diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index 47b1da61b37..6658d8dd721 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -538,6 +538,28 @@ public void updateAdapterResponseTimeShouldUpdateMetrics() { assertThat(metricRegistry.timer("account.accountId.adapter.conversant.request_time").getCount()).isEqualTo(2); } + @Test + public void updateAdapterRequestBuyerUidScrubbedMetricsShouldIncrementMetrics() { + // when + metrics.updateAdapterRequestBuyerUidScrubbedMetrics(RUBICON, Account.empty(ACCOUNT_ID)); + metrics.updateAdapterRequestBuyerUidScrubbedMetrics(CONVERSANT, Account.empty(ACCOUNT_ID)); + metrics.updateAdapterRequestBuyerUidScrubbedMetrics(CONVERSANT, Account.empty(ACCOUNT_ID)); + + // then + assertThat(metricRegistry.counter("adapter.rubicon.requests.buyeruid_scrubbed") + .getCount()) + .isOne(); + assertThat(metricRegistry.counter("account.accountId.adapter.rubicon.requests.buyeruid_scrubbed") + .getCount()) + .isOne(); + assertThat(metricRegistry.counter("adapter.conversant.requests.buyeruid_scrubbed") + .getCount()) + .isEqualTo(2); + assertThat(metricRegistry.counter("account.accountId.adapter.conversant.requests.buyeruid_scrubbed") + .getCount()) + .isEqualTo(2); + } + @Test public void updateAdapterRequestNobidMetricsShouldIncrementMetrics() { // when