diff --git a/config/account.go b/config/account.go index a773e6fbbd8..cc403f962b5 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 0c1c1fb274c..8b90cb2ff89 100644 --- a/endpoints/cookie_sync.go +++ b/endpoints/cookie_sync.go @@ -30,6 +30,7 @@ import ( "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" ) @@ -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 3da85fb71a4..503eb9c35b2 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 1b01e5116e9..046d5c3a19a 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 fe7d296f1f0..cdf007aa3c7 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 bc2d0812a7f..88913a76ccb 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.