Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions config/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions endpoints/cookie_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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,
Expand Down
56 changes: 46 additions & 10 deletions endpoints/cookie_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
}
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
})
}
}
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 6 additions & 5 deletions usersync/bidderchooser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -35,12 +35,13 @@ 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Can you please keep the // available comment? In my last comment, I intended to say the comment is appropriate, but the extra description added to the end describing the if conditional wasn't necessary.

bidders = c.shuffledAppend(bidders, available)
if !cooperative.PriorityGroupsOnly {
bidders = c.shuffledAppend(bidders, available)
}

return bidders
}
Expand Down
95 changes: 55 additions & 40 deletions usersync/bidderchooser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
})
}
}

Expand Down
5 changes: 3 additions & 2 deletions usersync/chooser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading