From 8fa1345abfec00057c91f241784d751d4fff7dff Mon Sep 17 00:00:00 2001 From: anishb-ms <122366082+anishb-ms@users.noreply.github.com> Date: Wed, 20 May 2026 01:20:15 +0530 Subject: [PATCH] Coop Cookie Sync: Add option to skip appending remaining available bidders (#4764) --- config/account.go | 9 ++-- endpoints/cookie_sync.go | 40 +++++++------- endpoints/cookie_sync_test.go | 56 ++++++++++++++++---- usersync/bidderchooser.go | 10 ++-- usersync/bidderchooser_test.go | 95 ++++++++++++++++++++-------------- usersync/chooser.go | 5 +- 6 files changed, 136 insertions(+), 79 deletions(-) diff --git a/config/account.go b/config/account.go index 2d2bfb5cd..d1d276ed3 100644 --- a/config/account.go +++ b/config/account.go @@ -58,10 +58,11 @@ type Account struct { // CookieSync represents the account-level defaults for the cookie sync endpoint. type CookieSync struct { - DefaultLimit *int `mapstructure:"default_limit" json:"default_limit"` - MaxLimit *int `mapstructure:"max_limit" json:"max_limit"` - DefaultCoopSync *bool `mapstructure:"default_coop_sync" json:"default_coop_sync"` - PriorityGroups [][]string `mapstructure:"priority_groups" json:"priority_groups"` + DefaultLimit *int `mapstructure:"default_limit" json:"default_limit"` + MaxLimit *int `mapstructure:"max_limit" json:"max_limit"` + DefaultCoopSync *bool `mapstructure:"default_coop_sync" json:"default_coop_sync"` + PriorityGroups [][]string `mapstructure:"priority_groups" json:"priority_groups"` + PriorityGroupsOnly *bool `mapstructure:"priority_groups_only" json:"priority_groups_only"` } // AccountCCPA represents account-specific CCPA configuration diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go index a90bc6117..dfb646cc0 100644 --- a/endpoints/cookie_sync.go +++ b/endpoints/cookie_sync.go @@ -15,23 +15,24 @@ import ( "github.com/julienschmidt/httprouter" gpplib "github.com/prebid/go-gpp" gppConstants "github.com/prebid/go-gpp/constants" - accountService "github.com/prebid/prebid-server/v3/account" - "github.com/prebid/prebid-server/v3/analytics" - "github.com/prebid/prebid-server/v3/config" - "github.com/prebid/prebid-server/v3/errortypes" - "github.com/prebid/prebid-server/v3/gdpr" - "github.com/prebid/prebid-server/v3/logger" - "github.com/prebid/prebid-server/v3/macros" - "github.com/prebid/prebid-server/v3/metrics" - "github.com/prebid/prebid-server/v3/openrtb_ext" - "github.com/prebid/prebid-server/v3/privacy" - "github.com/prebid/prebid-server/v3/privacy/ccpa" - gppPrivacy "github.com/prebid/prebid-server/v3/privacy/gpp" - "github.com/prebid/prebid-server/v3/stored_requests" - "github.com/prebid/prebid-server/v3/usersync" - "github.com/prebid/prebid-server/v3/util/jsonutil" - stringutil "github.com/prebid/prebid-server/v3/util/stringutil" - "github.com/prebid/prebid-server/v3/util/timeutil" + accountService "github.com/prebid/prebid-server/v4/account" + "github.com/prebid/prebid-server/v4/analytics" + "github.com/prebid/prebid-server/v4/config" + "github.com/prebid/prebid-server/v4/errortypes" + "github.com/prebid/prebid-server/v4/gdpr" + "github.com/prebid/prebid-server/v4/logger" + "github.com/prebid/prebid-server/v4/macros" + "github.com/prebid/prebid-server/v4/metrics" + "github.com/prebid/prebid-server/v4/openrtb_ext" + "github.com/prebid/prebid-server/v4/privacy" + "github.com/prebid/prebid-server/v4/privacy/ccpa" + gppPrivacy "github.com/prebid/prebid-server/v4/privacy/gpp" + "github.com/prebid/prebid-server/v4/stored_requests" + "github.com/prebid/prebid-server/v4/usersync" + "github.com/prebid/prebid-server/v4/util/jsonutil" + "github.com/prebid/prebid-server/v4/util/ptrutil" + stringutil "github.com/prebid/prebid-server/v4/util/stringutil" + "github.com/prebid/prebid-server/v4/util/timeutil" ) const receiveCookieDeprecation = "receive-cookie-deprecation" @@ -183,8 +184,9 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma rx := usersync.Request{ Bidders: request.Bidders, Cooperative: usersync.Cooperative{ - Enabled: (request.CooperativeSync != nil && *request.CooperativeSync) || (request.CooperativeSync == nil && c.config.UserSync.Cooperative.EnabledByDefault), - PriorityGroups: c.findPriorityGroups(account.CookieSync), + Enabled: (request.CooperativeSync != nil && *request.CooperativeSync) || (request.CooperativeSync == nil && c.config.UserSync.Cooperative.EnabledByDefault), + PriorityGroups: c.findPriorityGroups(account.CookieSync), + PriorityGroupsOnly: ptrutil.ValueOrDefault(account.CookieSync.PriorityGroupsOnly), }, Debug: request.Debug, Limit: limit, diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index 6fc56f797..182888fcd 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -2603,7 +2603,7 @@ func TestCookieSyncFindPriorityGroups(t *testing.T) { } // createAccountJSON creates a JSON representation of account config for testing -func createAccountJSON(priorityGroups [][]string, defaultCoopSync *bool) json.RawMessage { +func createAccountJSON(priorityGroups [][]string, defaultCoopSync *bool, priorityGroupsOnly *bool) json.RawMessage { account := map[string]interface{}{ "cookie_sync": map[string]interface{}{ "priority_groups": priorityGroups, @@ -2614,6 +2614,10 @@ func createAccountJSON(priorityGroups [][]string, defaultCoopSync *bool) json.Ra account["cookie_sync"].(map[string]interface{})["default_coop_sync"] = *defaultCoopSync } + if priorityGroupsOnly != nil { + account["cookie_sync"].(map[string]interface{})["priority_groups_only"] = *priorityGroupsOnly + } + jsonData, _ := json.Marshal(account) return json.RawMessage(jsonData) } @@ -2652,13 +2656,15 @@ func TestCookieSyncPriorityGroupsIntegration(t *testing.T) { } testCases := []struct { - description string - givenRequestBody string - givenAccountPriorityGroups [][]string - givenAccountDefaultCoopSync *bool - givenGlobalPriorityGroups [][]string - givenCooperativeEnabledByDef bool - shouldContainBidders []string + description string + givenRequestBody string + givenAccountPriorityGroups [][]string + givenAccountDefaultCoopSync *bool + givenAccountPriorityGroupsOnly *bool + givenGlobalPriorityGroups [][]string + givenCooperativeEnabledByDef bool + shouldContainBidders []string + shouldNotContainBidders []string }{ { description: "Account-level priority groups used with cooperative sync enabled", @@ -2713,6 +2719,31 @@ func TestCookieSyncPriorityGroupsIntegration(t *testing.T) { givenCooperativeEnabledByDef: false, shouldContainBidders: []string{"appnexus", "rubicon", "pubmatic"}, }, + { + description: "Priority groups only — remaining bidders excluded", + givenRequestBody: `{"bidders":["appnexus"], "coopSync": true, "limit": 10, "account": "test_account"}`, + givenAccountPriorityGroups: [][]string{ + {"rubicon"}, + }, + givenAccountDefaultCoopSync: ptrutil.ToPtr(true), + givenAccountPriorityGroupsOnly: ptrutil.ToPtr(true), + givenGlobalPriorityGroups: [][]string{{"ignored"}}, + givenCooperativeEnabledByDef: false, + shouldContainBidders: []string{"appnexus", "rubicon"}, + shouldNotContainBidders: []string{"pubmatic"}, + }, + { + description: "Priority groups only false — remaining bidders included", + givenRequestBody: `{"bidders":["appnexus"], "coopSync": true, "limit": 10, "account": "test_account"}`, + givenAccountPriorityGroups: [][]string{ + {"rubicon"}, + }, + givenAccountDefaultCoopSync: ptrutil.ToPtr(true), + givenAccountPriorityGroupsOnly: ptrutil.ToPtr(false), + givenGlobalPriorityGroups: [][]string{{"ignored"}}, + givenCooperativeEnabledByDef: false, + shouldContainBidders: []string{"appnexus", "rubicon", "pubmatic"}, + }, } for _, tc := range testCases { @@ -2750,7 +2781,7 @@ func TestCookieSyncPriorityGroupsIntegration(t *testing.T) { &mockAnalytics, &FakeAccountsFetcher{ AccountData: map[string]json.RawMessage{ - "test_account": createAccountJSON(tc.givenAccountPriorityGroups, tc.givenAccountDefaultCoopSync), + "test_account": createAccountJSON(tc.givenAccountPriorityGroups, tc.givenAccountDefaultCoopSync, tc.givenAccountPriorityGroupsOnly), }, }, bidders, @@ -2780,6 +2811,11 @@ func TestCookieSyncPriorityGroupsIntegration(t *testing.T) { for _, expected := range tc.shouldContainBidders { assert.Contains(t, actualBidders, expected, "Expected bidder %s to be present in response", expected) } + + // Check that excluded bidders are not present + for _, excluded := range tc.shouldNotContainBidders { + assert.NotContains(t, actualBidders, excluded, "Bidder %s should not be present in response", excluded) + } }) } } @@ -2923,7 +2959,7 @@ func TestCookieSyncPriorityGroupsEdgeCases(t *testing.T) { &mockAnalytics, &FakeAccountsFetcher{ AccountData: map[string]json.RawMessage{ - "test_account": createAccountJSON(tc.givenAccountPriorityGroups, tc.givenAccountDefaultCoopSync), + "test_account": createAccountJSON(tc.givenAccountPriorityGroups, tc.givenAccountDefaultCoopSync, nil), }, }, bidders, diff --git a/usersync/bidderchooser.go b/usersync/bidderchooser.go index 1b01e5116..046d5c3a1 100644 --- a/usersync/bidderchooser.go +++ b/usersync/bidderchooser.go @@ -13,7 +13,7 @@ type standardBidderChooser struct { func (c standardBidderChooser) choose(requested, available []string, cooperative Cooperative) []string { if cooperative.Enabled { - return c.chooseCooperative(requested, available, cooperative.PriorityGroups) + return c.chooseCooperative(requested, available, cooperative) } if len(requested) == 0 { @@ -23,7 +23,7 @@ func (c standardBidderChooser) choose(requested, available []string, cooperative return c.shuffledCopy(requested) } -func (c standardBidderChooser) chooseCooperative(requested, available []string, priorityGroups [][]string) []string { +func (c standardBidderChooser) chooseCooperative(requested, available []string, cooperative Cooperative) []string { // allocate enough memory for the slice to try to avoid re-allocation. the 50% overhead is a guess // at a satisfactory value. since all available bidders are included in the slice, along with // requested and prioritized bidders, expect there to be be many duplicates. the duplicate are @@ -35,12 +35,14 @@ func (c standardBidderChooser) chooseCooperative(requested, available []string, bidders = c.shuffledAppend(bidders, requested) // priority groups - for _, group := range priorityGroups { + for _, group := range cooperative.PriorityGroups { bidders = c.shuffledAppend(bidders, group) } // available - bidders = c.shuffledAppend(bidders, available) + if !cooperative.PriorityGroupsOnly { + bidders = c.shuffledAppend(bidders, available) + } return bidders } diff --git a/usersync/bidderchooser_test.go b/usersync/bidderchooser_test.go index fe7d296f1..cdf007aa3 100644 --- a/usersync/bidderchooser_test.go +++ b/usersync/bidderchooser_test.go @@ -62,6 +62,12 @@ func TestBidderChooserChoose(t *testing.T) { givenCooperative: Cooperative{Enabled: true, PriorityGroups: [][]string{{"pr1A", "pr1B"}, {"pr2A", "pr2B"}}}, expected: []string{"r2", "r1", "pr1B", "pr1A", "pr2B", "pr2A", "a2", "a1"}, }, + { + description: "Coop - PriorityGroupsOnly", + givenRequested: []string{"r1", "r2"}, + givenCooperative: Cooperative{Enabled: true, PriorityGroups: [][]string{{"pr1A", "pr1B"}}, PriorityGroupsOnly: true}, + expected: []string{"r2", "r1", "pr1B", "pr1A"}, + }, } for _, test := range testCases { @@ -76,55 +82,64 @@ func TestBidderChooserCooperative(t *testing.T) { shuffler := reverseShuffler{} available := []string{"a1", "a2"} - testCases := []struct { - description string - givenRequested []string - givenPriorityGroups [][]string - expected []string + testCases := map[string]struct { + givenRequested []string + givenCoop Cooperative + expected []string }{ - { - description: "Nil", - givenRequested: nil, - givenPriorityGroups: nil, - expected: []string{"a2", "a1"}, + "nil": { + givenRequested: nil, + givenCoop: Cooperative{Enabled: true}, + expected: []string{"a2", "a1"}, }, - { - description: "Empty", - givenRequested: []string{}, - givenPriorityGroups: [][]string{}, - expected: []string{"a2", "a1"}, + "empty": { + givenRequested: []string{}, + givenCoop: Cooperative{Enabled: true, PriorityGroups: [][]string{}}, + expected: []string{"a2", "a1"}, }, - { - description: "Requested", - givenRequested: []string{"r1", "r2"}, - givenPriorityGroups: nil, - expected: []string{"r2", "r1", "a2", "a1"}, + "requested": { + givenRequested: []string{"r1", "r2"}, + givenCoop: Cooperative{Enabled: true}, + expected: []string{"r2", "r1", "a2", "a1"}, }, - { - description: "Priority Groups - One", - givenRequested: nil, - givenPriorityGroups: [][]string{{"pr1A", "pr1B"}}, - expected: []string{"pr1B", "pr1A", "a2", "a1"}, + "priority_groups_one": { + givenRequested: nil, + givenCoop: Cooperative{Enabled: true, PriorityGroups: [][]string{{"pr1A", "pr1B"}}}, + expected: []string{"pr1B", "pr1A", "a2", "a1"}, }, - { - description: "Priority Groups - Many", - givenRequested: nil, - givenPriorityGroups: [][]string{{"pr1A", "pr1B"}, {"pr2A", "pr2B"}}, - expected: []string{"pr1B", "pr1A", "pr2B", "pr2A", "a2", "a1"}, + "priority_groups_many": { + givenRequested: nil, + givenCoop: Cooperative{Enabled: true, PriorityGroups: [][]string{{"pr1A", "pr1B"}, {"pr2A", "pr2B"}}}, + expected: []string{"pr1B", "pr1A", "pr2B", "pr2A", "a2", "a1"}, }, - { - description: "Requested + Priority Groups", - givenRequested: []string{"r1", "r2"}, - givenPriorityGroups: [][]string{{"pr1A", "pr1B"}, {"pr2A", "pr2B"}}, - expected: []string{"r2", "r1", "pr1B", "pr1A", "pr2B", "pr2A", "a2", "a1"}, + "requested_and_priority_groups": { + givenRequested: []string{"r1", "r2"}, + givenCoop: Cooperative{Enabled: true, PriorityGroups: [][]string{{"pr1A", "pr1B"}, {"pr2A", "pr2B"}}}, + expected: []string{"r2", "r1", "pr1B", "pr1A", "pr2B", "pr2A", "a2", "a1"}, + }, + "priority_groups_only_no_remaining": { + givenRequested: []string{"r1", "r2"}, + givenCoop: Cooperative{Enabled: true, PriorityGroups: [][]string{{"pr1A", "pr1B"}}, PriorityGroupsOnly: true}, + expected: []string{"r2", "r1", "pr1B", "pr1A"}, + }, + "priority_groups_only_no_requested": { + givenRequested: nil, + givenCoop: Cooperative{Enabled: true, PriorityGroups: [][]string{{"pr1A", "pr1B"}, {"pr2A", "pr2B"}}, PriorityGroupsOnly: true}, + expected: []string{"pr1B", "pr1A", "pr2B", "pr2A"}, + }, + "priority_groups_only_empty_groups": { + givenRequested: []string{"r1", "r2"}, + givenCoop: Cooperative{Enabled: true, PriorityGroups: [][]string{}, PriorityGroupsOnly: true}, + expected: []string{"r2", "r1"}, }, } - for _, test := range testCases { - chooser := standardBidderChooser{shuffler: shuffler} - result := chooser.chooseCooperative(test.givenRequested, available, test.givenPriorityGroups) - - assert.Equal(t, test.expected, result, test.description) + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + chooser := standardBidderChooser{shuffler: shuffler} + result := chooser.chooseCooperative(test.givenRequested, available, test.givenCoop) + assert.Equal(t, test.expected, result) + }) } } diff --git a/usersync/chooser.go b/usersync/chooser.go index 31a14a4d4..bda2163ea 100644 --- a/usersync/chooser.go +++ b/usersync/chooser.go @@ -46,8 +46,9 @@ type Request struct { // Cooperative specifies the settings for cooperative syncing for a given request, where bidders // other than those used by the publisher are considered for syncing. type Cooperative struct { - Enabled bool - PriorityGroups [][]string + Enabled bool + PriorityGroups [][]string + PriorityGroupsOnly bool } // Result specifies which bidders were included in the evaluation and which syncers were chosen.