Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Priority Bidder Ejection #2952

Merged
merged 23 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f1ce470
Priority Bidder Code
AlexBVolcy Jul 17, 2023
de5e618
Tweak Priority Ejector to update pri groups
AlexBVolcy Jul 19, 2023
73a9daf
Fallback Ejector
AlexBVolcy Jul 19, 2023
b0feb46
Opt out non prioity key support for oldest ejector
AlexBVolcy Jul 24, 2023
f29fafa
Simplified removeElementFromPriorityGroup
AlexBVolcy Jul 25, 2023
bb16689
Add ejector specific tests
AlexBVolcy Jul 25, 2023
6c9b7d1
Ejector refactor, remove fallback, add tests
AlexBVolcy Aug 2, 2023
8f963f4
Merge branch 'master' into final_priority_bidder_ejection
AlexBVolcy Aug 3, 2023
0112ea7
Fix validate merge fail
AlexBVolcy Aug 3, 2023
53c05f5
Address comments
AlexBVolcy Aug 3, 2023
489d34a
Update IsSyncerPriority, update Pri Ejector
AlexBVolcy Aug 3, 2023
b22b404
Update ejector logic
AlexBVolcy Aug 11, 2023
8060ba4
Implement TieEjector logic
AlexBVolcy Aug 15, 2023
4891b4e
Minor tweaks
AlexBVolcy Aug 15, 2023
bc8c72a
Update IsSyncerPriority logic
AlexBVolcy Aug 17, 2023
7d801a8
One line update to pri ejector
AlexBVolcy Aug 17, 2023
54dfaae
Clean up, add tests
AlexBVolcy Aug 18, 2023
8440f54
Sole element > MaxCookieSize edge case
AlexBVolcy Aug 24, 2023
2e84e37
Pass syncerByBidder, add new test
AlexBVolcy Aug 29, 2023
958fcdc
Merge branch 'master' into final_priority_bidder_ejection
AlexBVolcy Sep 12, 2023
10aebbc
Return unaltered cookie and 200
AlexBVolcy Sep 13, 2023
48876a1
Just return 200 and warning, cookie unchanged
AlexBVolcy Sep 14, 2023
50589d9
Merge branch 'master' into final_priority_bidder_ejection
AlexBVolcy Sep 19, 2023
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
2 changes: 2 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,8 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) {

v.SetDefault("event.timeout_ms", 1000)

v.SetDefault("user_sync.priority_groups", [][]string{})

v.SetDefault("accounts.filesystem.enabled", false)
v.SetDefault("accounts.filesystem.directorypath", "./stored_requests/data/by_id")
v.SetDefault("accounts.in_memory_cache.type", "none")
Expand Down
10 changes: 5 additions & 5 deletions config/usersync.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package config

// UserSync specifies the static global user sync configuration.
type UserSync struct {
Cooperative UserSyncCooperative `mapstructure:"coop_sync"`
ExternalURL string `mapstructure:"external_url"`
RedirectURL string `mapstructure:"redirect_url"`
Cooperative UserSyncCooperative `mapstructure:"coop_sync"`
ExternalURL string `mapstructure:"external_url"`
RedirectURL string `mapstructure:"redirect_url"`
PriorityGroups [][]string `mapstructure:"priority_groups"`
AlexBVolcy marked this conversation as resolved.
Show resolved Hide resolved
}

// UserSyncCooperative specifies the static global default cooperative cookie sync
type UserSyncCooperative struct {
EnabledByDefault bool `mapstructure:"default"`
PriorityGroups [][]string `mapstructure:"priority_groups"`
EnabledByDefault bool `mapstructure:"default"`
}
2 changes: 1 addition & 1 deletion endpoints/cookie_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
Bidders: request.Bidders,
Cooperative: usersync.Cooperative{
Enabled: (request.CooperativeSync != nil && *request.CooperativeSync) || (request.CooperativeSync == nil && c.config.UserSync.Cooperative.EnabledByDefault),
PriorityGroups: c.config.UserSync.Cooperative.PriorityGroups,
PriorityGroups: c.config.UserSync.PriorityGroups,
},
Limit: request.Limit,
Privacy: usersyncPrivacy{
Expand Down
27 changes: 13 additions & 14 deletions endpoints/cookie_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -516,9 +516,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"},
givenCCPAEnabled: true,
givenConfig: config.UserSync{
PriorityGroups: [][]string{{"a", "b", "c"}},
Cooperative: config.UserSyncCooperative{
EnabledByDefault: false,
PriorityGroups: [][]string{{"a", "b", "c"}},
},
},
expectedPrivacy: privacy.Policies{
Expand Down Expand Up @@ -563,9 +563,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"},
givenCCPAEnabled: true,
givenConfig: config.UserSync{
PriorityGroups: [][]string{{"a", "b", "c"}},
Cooperative: config.UserSyncCooperative{
EnabledByDefault: false,
PriorityGroups: [][]string{{"a", "b", "c"}},
},
},
expectedPrivacy: privacy.Policies{
Expand Down Expand Up @@ -616,9 +616,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"},
givenCCPAEnabled: true,
givenConfig: config.UserSync{
PriorityGroups: [][]string{{"a", "b", "c"}},
Cooperative: config.UserSyncCooperative{
EnabledByDefault: true,
PriorityGroups: [][]string{{"a", "b", "c"}},
},
},
expectedPrivacy: privacy.Policies{},
Expand All @@ -642,9 +642,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"},
givenCCPAEnabled: true,
givenConfig: config.UserSync{
PriorityGroups: [][]string{{"a", "b", "c"}},
Cooperative: config.UserSyncCooperative{
EnabledByDefault: false,
PriorityGroups: [][]string{{"a", "b", "c"}},
},
},
expectedPrivacy: privacy.Policies{},
Expand All @@ -668,9 +668,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"},
givenCCPAEnabled: true,
givenConfig: config.UserSync{
PriorityGroups: [][]string{{"a", "b", "c"}},
Cooperative: config.UserSyncCooperative{
EnabledByDefault: true,
PriorityGroups: [][]string{{"a", "b", "c"}},
},
},
expectedPrivacy: privacy.Policies{},
Expand All @@ -694,9 +694,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"},
givenCCPAEnabled: true,
givenConfig: config.UserSync{
PriorityGroups: [][]string{{"a", "b", "c"}},
Cooperative: config.UserSyncCooperative{
EnabledByDefault: false,
PriorityGroups: [][]string{{"a", "b", "c"}},
},
},
expectedPrivacy: privacy.Policies{},
Expand All @@ -720,9 +720,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"},
givenCCPAEnabled: true,
givenConfig: config.UserSync{
PriorityGroups: [][]string{{"a", "b", "c"}},
Cooperative: config.UserSyncCooperative{
EnabledByDefault: true,
PriorityGroups: [][]string{{"a", "b", "c"}},
},
},
expectedPrivacy: privacy.Policies{},
Expand All @@ -746,9 +746,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"},
givenCCPAEnabled: true,
givenConfig: config.UserSync{
PriorityGroups: [][]string{{"a", "b", "c"}},
Cooperative: config.UserSyncCooperative{
EnabledByDefault: false,
PriorityGroups: [][]string{{"a", "b", "c"}},
},
},
expectedPrivacy: privacy.Policies{},
Expand Down Expand Up @@ -889,9 +889,8 @@ func TestCookieSyncParseRequest(t *testing.T) {
givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"},
givenCCPAEnabled: true,
givenConfig: config.UserSync{
Cooperative: config.UserSyncCooperative{
PriorityGroups: [][]string{{"a", "b", "c"}},
},
PriorityGroups: [][]string{{"a", "b", "c"}},
Cooperative: config.UserSyncCooperative{},
},
expectedPrivacy: privacy.Policies{},
expectedRequest: usersync.Request{
Expand Down Expand Up @@ -919,9 +918,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"},
givenCCPAEnabled: true,
givenConfig: config.UserSync{
PriorityGroups: [][]string{{"a", "b", "c"}},
Cooperative: config.UserSyncCooperative{
EnabledByDefault: false,
PriorityGroups: [][]string{{"a", "b", "c"}},
},
},
expectedPrivacy: privacy.Policies{},
Expand Down Expand Up @@ -950,9 +949,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"},
givenCCPAEnabled: true,
givenConfig: config.UserSync{
PriorityGroups: [][]string{{"a", "b", "c"}},
Cooperative: config.UserSyncCooperative{
EnabledByDefault: false,
PriorityGroups: [][]string{{"a", "b", "c"}},
},
},
expectedPrivacy: privacy.Policies{},
Expand Down Expand Up @@ -984,9 +983,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "0"},
givenCCPAEnabled: true,
givenConfig: config.UserSync{
PriorityGroups: [][]string{{"a", "b", "c"}},
Cooperative: config.UserSyncCooperative{
EnabledByDefault: false,
PriorityGroups: [][]string{{"a", "b", "c"}},
},
},
expectedPrivacy: privacy.Policies{},
Expand Down
17 changes: 16 additions & 1 deletion endpoints/setuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,12 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use

setSiteCookie := siteCookieCheck(r.UserAgent())

// Priority Ejector Set Up
priorityEjector := &usersync.PriorityBidderEjector{PriorityGroups: cfg.UserSync.PriorityGroups, TieEjector: &usersync.OldestEjector{}}
AlexBVolcy marked this conversation as resolved.
Show resolved Hide resolved
priorityEjector.IsSyncerPriority = isSyncerPriority(bidderName, cfg.UserSync.PriorityGroups)

// Write Cookie
encodedCookie, err := cookie.PrepareCookieForWrite(&cfg.HostCookie, encoder)
encodedCookie, err := cookie.PrepareCookieForWrite(&cfg.HostCookie, encoder, priorityEjector)
AlexBVolcy marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
w.WriteHeader(http.StatusBadRequest)
metricsEngine.RecordSetUid(metrics.SetUidBadRequest)
Expand Down Expand Up @@ -335,6 +339,17 @@ func getSyncer(query url.Values, syncersByBidder map[string]usersync.Syncer) (us
return syncer, bidder, nil
}

func isSyncerPriority(bidderNameFromSyncerQuery string, priorityGroups [][]string) bool {
for _, group := range priorityGroups {
for _, bidder := range group {
if bidderNameFromSyncerQuery == bidder {
return true
}
}
}
return false
}

// getResponseFormat reads the format query parameter or falls back to the syncer's default.
// Returns either "b" (iframe), "i" (redirect), or an empty string "" (legacy behavior of an
// empty response body with no content type).
Expand Down
69 changes: 69 additions & 0 deletions endpoints/setuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,15 @@ func TestSetUIDEndpoint(t *testing.T) {
expectedHeaders: map[string]string{"Content-Type": "text/html", "Content-Length": "0"},
description: "Set uid for valid bidder with valid account provided with invalid user sync activity",
},
{
uri: "/setuid?bidder=nonPriorityBidder&uid=123",
syncersBidderNameToKey: map[string]string{"nonPriorityBidder": "nonPrioritySyncer"},
existingSyncs: nil,
gdprAllowsHostCookies: true,
expectedSyncs: nil,
expectedStatusCode: http.StatusBadRequest,
description: "Syncer is not a priority",
},
}

analytics := analyticsConf.NewPBSAnalytics(&config.Analytics{})
Expand Down Expand Up @@ -1322,6 +1331,56 @@ func TestGetResponseFormat(t *testing.T) {
}
}

func TestIsSyncerPriority(t *testing.T) {
testCases := []struct {
name string
givenBidderNameFromSyncerQuery string
givenPriorityGroups [][]string
expected bool
}{
{
name: "bidder-name-is-priority",
givenBidderNameFromSyncerQuery: "priorityBidder",
givenPriorityGroups: [][]string{
{"priorityBidder"},
{"2", "3"},
},
expected: true,
},
{
name: "bidder-name-is-not-priority",
givenBidderNameFromSyncerQuery: "notPriorityBidderName",
givenPriorityGroups: [][]string{
{"1"},
{"2", "3"},
},
expected: false,
},
{
name: "no-bidder-name-given",
givenBidderNameFromSyncerQuery: "",
givenPriorityGroups: [][]string{
{"1"},
{"2", "3"},
},
expected: false,
},
{
name: "no-priority-groups-given",
givenBidderNameFromSyncerQuery: "bidderName",
givenPriorityGroups: [][]string{},
expected: false,
},
}

for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
isPriority := isSyncerPriority(test.givenBidderNameFromSyncerQuery, test.givenPriorityGroups)
assert.Equal(t, test.expected, isPriority)
})
}
}

func assertHasSyncs(t *testing.T, testCase string, resp *httptest.ResponseRecorder, syncs map[string]string) {
t.Helper()
cookie := parseCookieString(t, resp)
Expand Down Expand Up @@ -1354,6 +1413,11 @@ func doRequest(req *http.Request, analytics analytics.PBSAnalyticsModule, metric
"blocked_acct": true,
},
AccountDefaults: config.Account{},
UserSync: config.UserSync{
PriorityGroups: [][]string{
{},
},
},
}
cfg.MarshalAccountDefaults()

Expand All @@ -1376,6 +1440,11 @@ func doRequest(req *http.Request, analytics analytics.PBSAnalyticsModule, metric
syncersByBidder := make(map[string]usersync.Syncer)
for bidderName, syncerKey := range syncersBidderNameToKey {
syncersByBidder[bidderName] = fakeSyncer{key: syncerKey, defaultSyncType: usersync.SyncTypeIFrame}
if bidderName != "nonPriorityBidder" {
cfg.UserSync.PriorityGroups[0] = append(cfg.UserSync.PriorityGroups[0], bidderName)
} else {
cfg.HostCookie.MaxCookieSizeBytes = 100
}
}

fakeAccountsFetcher := FakeAccountsFetcher{AccountData: map[string]json.RawMessage{
Expand Down
3 changes: 2 additions & 1 deletion pbs/usersync.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type UserSyncDeps struct {
ExternalUrl string
RecaptchaSecret string
HostCookieConfig *config.HostCookie
PriorityGroups [][]string
}

// Struct for parsing json in google's response
Expand Down Expand Up @@ -81,7 +82,7 @@ func (deps *UserSyncDeps) OptOut(w http.ResponseWriter, r *http.Request, _ httpr
pc.SetOptOut(optout != "")

// Write Cookie
encodedCookie, err := pc.PrepareCookieForWrite(deps.HostCookieConfig, encoder)
encodedCookie, err := encoder.Encode(pc)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
Expand Down
1 change: 1 addition & 0 deletions router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
HostCookieConfig: &(cfg.HostCookie),
ExternalUrl: cfg.ExternalURL,
RecaptchaSecret: cfg.RecaptchaSecret,
PriorityGroups: cfg.UserSync.PriorityGroups,
}

r.GET("/setuid", endpoints.NewSetUIDEndpoint(cfg, syncersByBidder, gdprPermsBuilder, tcf2CfgBuilder, pbsAnalytics, accounts, r.MetricsEngine))
Expand Down
30 changes: 5 additions & 25 deletions usersync/cookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"net/http"
"sort"
"time"

"github.com/prebid/prebid-server/config"
Expand Down Expand Up @@ -58,10 +57,7 @@ func ReadCookie(r *http.Request, decoder Decoder, host *config.HostCookie) *Cook
}

// PrepareCookieForWrite ejects UIDs as long as the cookie is too full
func (cookie *Cookie) PrepareCookieForWrite(cfg *config.HostCookie, encoder Encoder) (string, error) {
uuidKeys := sortUIDs(cookie.uids)

i := 0
func (cookie *Cookie) PrepareCookieForWrite(cfg *config.HostCookie, encoder Encoder, ejector Ejector) (string, error) {
for len(cookie.uids) > 0 {
encodedCookie, err := encoder.Encode(cookie)
if err != nil {
AlexBVolcy marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -82,10 +78,11 @@ func (cookie *Cookie) PrepareCookieForWrite(cfg *config.HostCookie, encoder Enco
return encodedCookie, nil
}

uidToDelete := uuidKeys[i]
uidToDelete, err := ejector.Choose(cookie.uids)
if err != nil {
return encodedCookie, err
}
delete(cookie.uids, uidToDelete)

i++
}
return "", nil
}
Expand Down Expand Up @@ -132,23 +129,6 @@ func (cookie *Cookie) Sync(key string, uid string) error {
return nil
}

// sortUIDs is used to get a list of uids sorted from oldest to newest
// This list is used to eject oldest uids from the cookie
// This will be incorporated with a more complex ejection framework in a future PR
func sortUIDs(uids map[string]UIDEntry) []string {
if len(uids) > 0 {
uuidKeys := make([]string, 0, len(uids))
for key := range uids {
uuidKeys = append(uuidKeys, key)
}
sort.SliceStable(uuidKeys, func(i, j int) bool {
return uids[uuidKeys[i]].Expires.Before(uids[uuidKeys[j]].Expires)
})
return uuidKeys
}
return nil
}

// SyncHostCookie syncs the request cookie with the host cookie
func SyncHostCookie(r *http.Request, requestCookie *Cookie, host *config.HostCookie) {
if uid, _, _ := requestCookie.GetUID(host.Family); uid == "" && host.CookieName != "" {
Expand Down
Loading