From d11b1cc5fc294e696cfc65ca41b70653a6966f75 Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Thu, 30 Apr 2026 15:34:36 +0300 Subject: [PATCH 1/3] Added new events.enabled toggle that takes precedence. --- .../server/auction/BidResponseCreator.java | 13 +++---- .../auction/BidResponseCreatorTest.java | 39 ++++++++++++++++++- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index f1c3d1f0d82..b2c364d6a54 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -1719,7 +1719,11 @@ private static boolean eventsEnabledForAccount(AuctionContext auctionContext) { } private static boolean eventsEnabledForRequest(AuctionContext auctionContext) { - return eventsEnabledForChannel(auctionContext) || eventsAllowedByRequest(auctionContext); + return Optional.ofNullable(auctionContext.getBidRequest().getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getEvents) + .map(eventsNode -> eventsNode.at("/enabled").asBoolean(true)) + .orElseGet(() -> eventsEnabledForChannel(auctionContext)); } private static boolean eventsEnabledForChannel(AuctionContext auctionContext) { @@ -1754,13 +1758,6 @@ private static String recogniseChannelName(String channelName) { return channelName; } - private static boolean eventsAllowedByRequest(AuctionContext auctionContext) { - final ExtRequest ext = auctionContext.getBidRequest().getExt(); - final ExtRequestPrebid prebid = ext != null ? ext.getPrebid() : null; - - return prebid != null && prebid.getEvents() != null; - } - private long auctionTimestamp(AuctionContext auctionContext) { final ExtRequest ext = auctionContext.getBidRequest().getExt(); final ExtRequestPrebid prebid = ext != null ? ext.getPrebid() : null; diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 33c9747d70d..e0607b6264f 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -43,9 +43,9 @@ import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.CachedDebugLog; import org.prebid.server.auction.model.CategoryMappingResult; +import org.prebid.server.auction.model.ImpRejection; import org.prebid.server.auction.model.MultiBidConfig; import org.prebid.server.auction.model.PaaFormat; -import org.prebid.server.auction.model.ImpRejection; import org.prebid.server.auction.model.TargetingInfo; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.auction.model.debug.DebugContext; @@ -2755,6 +2755,43 @@ public void shouldNotAddExtPrebidEventsIfExtRequestPrebidEventsNull() { .containsNull(); } + @Test + public void shouldNotAddExtPrebidEventsIfExtRequestPrebidEventsEnabledIsFalse() { + // given + final Account account = Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build(); + + final Bid bid = Bid.builder() + .id("bidId1") + .price(BigDecimal.valueOf(5.67)) + .impid(IMP_ID) + .build(); + final List bidderResponses = singletonList( + BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(bid, banner, "seat", "USD")), 100)); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest( + identity(), + extBuilder -> extBuilder.events(mapper.createObjectNode().put("enabled", true)), + givenImp()), + contextBuilder -> contextBuilder + .account(account) + .auctionParticipations(toAuctionParticipant(bidderResponses))); + + // when + final BidResponse bidResponse = target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); + + // then + assertThat(bidResponse.getSeatbid()).hasSize(1) + .flatExtracting(SeatBid::getBid) + .extracting(responseBid -> toExtBidPrebid(responseBid.getExt()).getEvents()) + .containsNull(); + } + @Test public void shouldNotAddExtPrebidEventsIfAccountDoesNotSupportEventsForChannel() { // given From 79e7c77a8ccdc8f6961510400e7828f7aab2b54d Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Thu, 30 Apr 2026 20:16:54 +0300 Subject: [PATCH 2/3] Fixed event unit tests. --- .../prebid/server/auction/BidResponseCreatorTest.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index e0607b6264f..de671b88440 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -163,6 +163,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.prebid.server.auction.model.BidRejectionReason.NO_BID; import static org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAdservertargetingRule.Source.xStatic; import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; @@ -2719,6 +2720,8 @@ public void shouldNotAddExtPrebidEventsIfEventsAreNotEnabled() { .flatExtracting(SeatBid::getBid) .extracting(responseBid -> toExtBidPrebid(responseBid.getExt()).getEvents()) .containsNull(); + + verifyNoInteractions(eventsService); } @Test @@ -2753,6 +2756,8 @@ public void shouldNotAddExtPrebidEventsIfExtRequestPrebidEventsNull() { .flatExtracting(SeatBid::getBid) .extracting(responseBid -> toExtBidPrebid(responseBid.getExt()).getEvents()) .containsNull(); + + verifyNoInteractions(eventsService); } @Test @@ -2776,7 +2781,7 @@ public void shouldNotAddExtPrebidEventsIfExtRequestPrebidEventsEnabledIsFalse() final AuctionContext auctionContext = givenAuctionContext( givenBidRequest( identity(), - extBuilder -> extBuilder.events(mapper.createObjectNode().put("enabled", true)), + extBuilder -> extBuilder.events(mapper.createObjectNode().put("enabled", false)), givenImp()), contextBuilder -> contextBuilder .account(account) @@ -2790,6 +2795,8 @@ public void shouldNotAddExtPrebidEventsIfExtRequestPrebidEventsEnabledIsFalse() .flatExtracting(SeatBid::getBid) .extracting(responseBid -> toExtBidPrebid(responseBid.getExt()).getEvents()) .containsNull(); + + verifyNoInteractions(eventsService); } @Test @@ -2831,6 +2838,8 @@ public void shouldNotAddExtPrebidEventsIfAccountDoesNotSupportEventsForChannel() .flatExtracting(SeatBid::getBid) .extracting(responseBid -> toExtBidPrebid(responseBid.getExt()).getEvents()) .containsNull(); + + verifyNoInteractions(eventsService); } @Test From da7d4c92c005cc029a7f52488ab43ec1c0a5704f Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Thu, 30 Apr 2026 21:41:30 +0300 Subject: [PATCH 3/3] Tests: Add new request.ext.prebid.events.enabled toggle (#4480) --- .../config/AccountAnalyticsConfig.groovy | 3 +- .../model/request/auction/Events.groovy | 1 + .../server/functional/tests/EventsSpec.groovy | 131 +++++++++++++++++- 3 files changed, 130 insertions(+), 5 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy index 0a15cead562..79f976737cc 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy @@ -4,12 +4,13 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString +import org.prebid.server.functional.model.ChannelType @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) class AccountAnalyticsConfig { - Map auctionEvents + Map auctionEvents Boolean allowClientDetails AnalyticsModule modules diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Events.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Events.groovy index a9d3abf219d..77202f99399 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Events.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Events.groovy @@ -5,4 +5,5 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize @JsonSerialize class Events { + Boolean enabled } diff --git a/src/test/groovy/org/prebid/server/functional/tests/EventsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/EventsSpec.groovy index 3b18f4ea720..52754e8a6bf 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/EventsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/EventsSpec.groovy @@ -1,12 +1,17 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.ChannelType +import org.prebid.server.functional.model.config.AccountAnalyticsConfig +import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Events import org.prebid.server.functional.model.request.auction.PrebidStoredRequest import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE class EventsSpec extends BaseSpec { @@ -31,7 +36,7 @@ class EventsSpec extends BaseSpec { assert bidResponse.seatbid[0].bid[0].ext.prebid.events.imp where: - distributionChannel << [SITE, APP] + distributionChannel << [SITE, APP, DOOH] } def "PBS should not generate event tracker URLs when events are disabled for account"() { @@ -54,7 +59,7 @@ class EventsSpec extends BaseSpec { assert !bidResponse.seatbid[0].bid[0].ext.prebid.events?.imp where: - distributionChannel << [SITE, APP] + distributionChannel << [SITE, APP, DOOH] } def "PBS should resolve publisher id for events when events are enabled for account"() { @@ -78,7 +83,7 @@ class EventsSpec extends BaseSpec { assert bidResponseEvents.imp.contains("a=${accountId}") where: - distributionChannel << [SITE, APP] + distributionChannel << [SITE, APP, DOOH] } def "PBS should resolve publisher id from stored request for events when events enabled"() { @@ -110,6 +115,124 @@ class EventsSpec extends BaseSpec { assert bidResponseEvents.imp.contains("a=${accountId}") where: - distributionChannel << [SITE, APP] + distributionChannel << [SITE, APP, DOOH] + } + + def "Account-level analytics settings should apply when request events config is absent"() { + given: "BidRequest without events config" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.getDefaultBidRequest(requestType).tap { + setAccountId(accountId) + } + + and: "Account with analytics events disabled for corresponding channel" + def analyticsConfig = new AccountAnalyticsConfig(auctionEvents: [(accountConfigChannelType): false]) + def account = new Account(uuid: accountId, eventsEnabled: true, config: new AccountConfig(analytics: analyticsConfig)) + accountDao.save(account) + + when: "Auction request is processed" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Events should not be present in response due to stored request disablement" + assert !bidResponse.seatbid[0].bid[0].ext?.prebid?.events + + where: + requestType | accountConfigChannelType + SITE | ChannelType.WEB + APP | ChannelType.APP + DOOH | ChannelType.DOOH + } + + def "Request level events config should override account-level analytics settings"() { + given: "BidRequest with events config" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.getDefaultBidRequest(requestType).tap { + setAccountId(accountId) + ext.prebid.events = new Events(enabled: requestEventEnablement) + } + + and: "Account with analytics events disabled for corresponding channel" + def analyticsConfig = new AccountAnalyticsConfig(auctionEvents: [(accountConfigChannelType): false]) + def account = new Account(uuid: accountId, eventsEnabled: true, config: new AccountConfig(analytics: analyticsConfig)) + accountDao.save(account) + + when: "Auction request is processed" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Events should be present in response despite account-level disablement" + def bidResponseEvents = bidResponse.seatbid[0].bid[0].ext.prebid.events + assert bidResponseEvents.win.contains("a=${accountId}") + assert bidResponseEvents.imp.contains("a=${accountId}") + + where: + requestEventEnablement | requestType | accountConfigChannelType + null | SITE | ChannelType.WEB + null | APP | ChannelType.APP + null | DOOH | ChannelType.DOOH + + true | SITE | ChannelType.WEB + true | APP | ChannelType.APP + true | DOOH | ChannelType.DOOH + } + + def "Request-level events disabled should override account-level analytics settings"() { + given: "BidRequest with events explicitly disabled at request level" + def accountId = PBSUtils.randomNumber as String + + def bidRequest = BidRequest.getDefaultBidRequest(requestType).tap { + setAccountId(accountId) + ext.prebid.events = new Events(enabled: false) + } + + and: "Account with analytics events enabled for corresponding channel" + def analyticsConfig = new AccountAnalyticsConfig(auctionEvents: [(accountConfigChannelType): true]) + def account = new Account(uuid: accountId, eventsEnabled: true, config: new AccountConfig(analytics: analyticsConfig)) + accountDao.save(account) + + when: "Auction request is processed" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Events should not be present in response due to request-level disablement" + assert !bidResponse.seatbid[0].bid[0].ext?.prebid?.events + + where: + requestType | accountConfigChannelType + SITE | ChannelType.WEB + APP | ChannelType.APP + DOOH | ChannelType.DOOH + } + + def "Stored request events config should override account-level analytics settings when request config is absent"() { + given: "BidRequest referencing stored request without events config" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest(distributionChannel).tap { + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + setAccountId(null) + } + + and: "Stored request with account id and events disabled" + def accountId = PBSUtils.randomNumber as String + def storedRequest = BidRequest.getDefaultBidRequest(distributionChannel).tap { + setAccountId(accountId) + ext.prebid.events = new Events(enabled: false) + } + storedRequestDao.save(StoredRequest.getStoredRequest(accountId, storedRequestId, storedRequest)) + + and: "Account with analytics events enabled for corresponding channel" + def analyticsConfig = new AccountAnalyticsConfig(auctionEvents: [(accountConfigChannelType): true]) + def account = new Account(uuid: accountId, eventsEnabled: true, config: new AccountConfig(analytics: analyticsConfig)) + accountDao.save(account) + + when: "Auction request is processed" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Events should not be present in response due to stored request disablement" + assert !bidResponse.seatbid[0].bid[0].ext?.prebid?.events + + where: + distributionChannel | accountConfigChannelType + SITE | ChannelType.WEB + APP | ChannelType.APP + DOOH | ChannelType.DOOH } }