From 7ac22163ade7e6c6559b504dcac0842002b3323a Mon Sep 17 00:00:00 2001 From: Jaydeep Mohite Date: Tue, 2 Jan 2024 12:00:32 +0530 Subject: [PATCH 1/4] Introduced UseFetchDataRate field to select source of floors from request or dynamic fetched --- floors/fetcher.go | 4 + floors/fetcher_test.go | 27 ++++++ floors/floors.go | 23 +++-- floors/floors_test.go | 200 +++++++++++++++++++++++++++++++++++++++++ openrtb_ext/floors.go | 1 + 5 files changed, 250 insertions(+), 5 deletions(-) diff --git a/floors/fetcher.go b/floors/fetcher.go index 6df43445010..cc1d5360719 100644 --- a/floors/fetcher.go +++ b/floors/fetcher.go @@ -309,6 +309,10 @@ func validateRules(config config.AccountFloorFetch, priceFloors *openrtb_ext.Pri return errors.New("skip rate should be greater than or equal to 0 and less than 100") } + if priceFloors.Data.UseFetchDataRate != nil && (*priceFloors.Data.UseFetchDataRate < dataRateMin || *priceFloors.Data.UseFetchDataRate > dataRateMax) { + return errors.New("useFetchDataRate should be greater than or equal to 0 and less than or equal to 100") + } + for _, modelGroup := range priceFloors.Data.ModelGroups { if len(modelGroup.Values) == 0 || len(modelGroup.Values) > config.MaxRules { return errors.New("invalid number of floor rules, floor rules should be greater than zero and less than MaxRules specified in account config") diff --git a/floors/fetcher_test.go b/floors/fetcher_test.go index 085fd3edd1b..92adf12e7cd 100644 --- a/floors/fetcher_test.go +++ b/floors/fetcher_test.go @@ -16,6 +16,7 @@ import ( "github.com/prebid/prebid-server/v2/metrics" metricsConf "github.com/prebid/prebid-server/v2/metrics/config" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/ptrutil" "github.com/prebid/prebid-server/v2/util/timeutil" "github.com/stretchr/testify/assert" ) @@ -396,6 +397,32 @@ func TestValidatePriceFloorRules(t *testing.T) { }, wantErr: true, }, + { + name: "Invalid useFetchDataRate", + args: args{ + configs: config.AccountFloorFetch{ + Enabled: true, + URL: testURL, + Timeout: 5, + MaxFileSizeKB: 20, + MaxRules: 1, + MaxAge: 20, + Period: 10, + }, + priceFloors: &openrtb_ext.PriceFloorRules{ + Data: &openrtb_ext.PriceFloorData{ + SkipRate: 10, + ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ + Values: map[string]float64{ + "*|*|www.website.com": 15.01, + }, + }}, + UseFetchDataRate: ptrutil.ToPtr(-11), + }, + }, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/floors/floors.go b/floors/floors.go index 3fdeb4c55ae..f8e2a9d3dcd 100644 --- a/floors/floors.go +++ b/floors/floors.go @@ -28,6 +28,8 @@ const ( enforceRateMin int = 0 enforceRateMax int = 100 floorPrecision float64 = 0.01 + dataRateMin int = 0 + dataRateMax int = 100 ) // EnrichWithPriceFloors checks for floors enabled in account and request and selects floors data from dynamic fetched if present @@ -136,10 +138,23 @@ func isPriceFloorsEnabledForRequest(bidRequestWrapper *openrtb_ext.RequestWrappe return true } +// shouldUseFetchedData will check if to use fetched data or request data +func shouldUseFetchedData(rate *int) bool { + if rate == nil { + return true + } + randomNumber := rand.Intn(dataRateMax) + return randomNumber < *rate +} + // resolveFloors does selection of floors fields from request data and dynamic fetched data if dynamic fetch is enabled func resolveFloors(account config.Account, bidRequestWrapper *openrtb_ext.RequestWrapper, conversions currency.Conversions, priceFloorFetcher FloorFetcher) (*openrtb_ext.PriceFloorRules, []error) { - var errList []error - var floorRules *openrtb_ext.PriceFloorRules + var ( + errList []error + floorRules *openrtb_ext.PriceFloorRules + fetchResult *openrtb_ext.PriceFloorRules + fetchStatus string + ) reqFloor := extractFloorsFromRequest(bidRequestWrapper) if reqFloor != nil && reqFloor.Location != nil && len(reqFloor.Location.URL) > 0 { @@ -147,13 +162,11 @@ func resolveFloors(account config.Account, bidRequestWrapper *openrtb_ext.Reques } account.PriceFloors.Fetcher.AccountID = account.ID - var fetchResult *openrtb_ext.PriceFloorRules - var fetchStatus string if priceFloorFetcher != nil && account.PriceFloors.UseDynamicData { fetchResult, fetchStatus = priceFloorFetcher.Fetch(account.PriceFloors) } - if fetchResult != nil && fetchStatus == openrtb_ext.FetchSuccess { + if fetchResult != nil && fetchStatus == openrtb_ext.FetchSuccess && shouldUseFetchedData(fetchResult.Data.UseFetchDataRate) { mergedFloor := mergeFloors(reqFloor, fetchResult, conversions) floorRules, errList = createFloorsFrom(mergedFloor, account, fetchStatus, openrtb_ext.FetchLocation) } else if reqFloor != nil { diff --git a/floors/floors_test.go b/floors/floors_test.go index 047b6738cd3..df50d8b2649 100644 --- a/floors/floors_test.go +++ b/floors/floors_test.go @@ -11,6 +11,7 @@ import ( "github.com/prebid/prebid-server/v2/currency" "github.com/prebid/prebid-server/v2/openrtb_ext" "github.com/prebid/prebid-server/v2/util/jsonutil" + "github.com/prebid/prebid-server/v2/util/ptrutil" "github.com/stretchr/testify/assert" ) @@ -760,6 +761,205 @@ func TestResolveFloors(t *testing.T) { } } +type MockFetchDataRate0 struct{} + +func (m *MockFetchDataRate0) Fetch(configs config.AccountPriceFloors) (*openrtb_ext.PriceFloorRules, string) { + + if !configs.UseDynamicData { + return nil, openrtb_ext.FetchNone + } + priceFloors := openrtb_ext.PriceFloorRules{ + Enabled: getTrue(), + PriceFloorLocation: openrtb_ext.RequestLocation, + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: getTrue(), + EnforceRate: 100, + FloorDeals: getTrue(), + }, + Data: &openrtb_ext.PriceFloorData{ + Currency: "USD", + ModelGroups: []openrtb_ext.PriceFloorModelGroup{ + { + ModelVersion: "model from fetched", + Currency: "USD", + Values: map[string]float64{ + "banner|300x600|www.website5.com": 15, + "*|*|*": 25, + }, + Schema: openrtb_ext.PriceFloorSchema{ + Fields: []string{"mediaType", "size", "domain"}, + }, + }, + }, + UseFetchDataRate: ptrutil.ToPtr(0), + }, + } + return &priceFloors, openrtb_ext.FetchSuccess +} + +func (m *MockFetchDataRate0) Stop() { + +} + +type MockFetchDataRate100 struct{} + +func (m *MockFetchDataRate100) Fetch(configs config.AccountPriceFloors) (*openrtb_ext.PriceFloorRules, string) { + + if !configs.UseDynamicData { + return nil, openrtb_ext.FetchNone + } + priceFloors := openrtb_ext.PriceFloorRules{ + Enabled: getTrue(), + PriceFloorLocation: openrtb_ext.RequestLocation, + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: getTrue(), + EnforceRate: 100, + FloorDeals: getTrue(), + }, + Data: &openrtb_ext.PriceFloorData{ + Currency: "USD", + ModelGroups: []openrtb_ext.PriceFloorModelGroup{ + { + ModelVersion: "model from fetched", + Currency: "USD", + Values: map[string]float64{ + "banner|300x600|www.website5.com": 15, + "*|*|*": 25, + }, + Schema: openrtb_ext.PriceFloorSchema{ + Fields: []string{"mediaType", "size", "domain"}, + }, + }, + }, + UseFetchDataRate: ptrutil.ToPtr(100), + }, + } + return &priceFloors, openrtb_ext.FetchSuccess +} + +func (m *MockFetchDataRate100) Stop() { + +} + +func TestResolveFloorsWithUseDataRate(t *testing.T) { + rates := map[string]map[string]float64{ + "USD": { + "INR": 70, + "EUR": 0.9, + "JPY": 5.09, + }, + } + + testCases := []struct { + name string + bidRequestWrapper *openrtb_ext.RequestWrapper + account config.Account + conversions currency.Conversions + expErr []error + expFloors *openrtb_ext.PriceFloorRules + fetcher FloorFetcher + }{ + { + name: "Dynamic fetch enabled, floors from request selected as data rate 0", + fetcher: &MockFetchDataRate0{}, + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}}, + Ext: json.RawMessage(`{"prebid":{"floors":{"data":{"currency":"USD","modelgroups":[{"modelversion":"model 1 from req","currency":"USD","values":{"banner|300x600|www.website5.com":5,"*|*|*":7},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"enabled":true,"enforcement":{"enforcepbs":true,"floordeals":true,"enforcerate":100}}}}`), + }, + }, + account: config.Account{ + PriceFloors: config.AccountPriceFloors{ + Enabled: true, + UseDynamicData: true, + }, + }, + expFloors: &openrtb_ext.PriceFloorRules{ + Enabled: getTrue(), + FetchStatus: openrtb_ext.FetchNone, + PriceFloorLocation: openrtb_ext.RequestLocation, + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: getTrue(), + FloorDeals: getTrue(), + EnforceRate: 100, + }, + Data: &openrtb_ext.PriceFloorData{ + Currency: "USD", + ModelGroups: []openrtb_ext.PriceFloorModelGroup{ + { + ModelVersion: "model 1 from req", + Currency: "USD", + Values: map[string]float64{ + "banner|300x600|www.website5.com": 5, + "*|*|*": 7, + }, + Schema: openrtb_ext.PriceFloorSchema{ + Fields: []string{"mediaType", "size", "domain"}, + Delimiter: "|", + }, + }, + }, + }, + }, + }, + { + name: "Dynamic fetch enabled, floors from fetched selected as data rate is 100", + fetcher: &MockFetchDataRate100{}, + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}}, + }, + }, + account: config.Account{ + PriceFloors: config.AccountPriceFloors{ + Enabled: true, + UseDynamicData: true, + }, + }, + expFloors: &openrtb_ext.PriceFloorRules{ + Enabled: getTrue(), + FetchStatus: openrtb_ext.FetchSuccess, + PriceFloorLocation: openrtb_ext.FetchLocation, + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: getTrue(), + FloorDeals: getTrue(), + EnforceRate: 100, + }, + Data: &openrtb_ext.PriceFloorData{ + Currency: "USD", + ModelGroups: []openrtb_ext.PriceFloorModelGroup{ + { + ModelVersion: "model from fetched", + Currency: "USD", + Values: map[string]float64{ + "banner|300x600|www.website5.com": 15, + "*|*|*": 25, + }, + Schema: openrtb_ext.PriceFloorSchema{ + Fields: []string{"mediaType", "size", "domain"}, + }, + }, + }, + UseFetchDataRate: ptrutil.ToPtr(100), + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolvedFloors, _ := resolveFloors(tc.account, tc.bidRequestWrapper, getCurrencyRates(rates), tc.fetcher) + assert.Equal(t, resolvedFloors, tc.expFloors, tc.name) + }) + } +} + func printFloors(floors *openrtb_ext.PriceFloorRules) string { fbytes, _ := jsonutil.Marshal(floors) return string(fbytes) diff --git a/openrtb_ext/floors.go b/openrtb_ext/floors.go index 0e773c65899..a429cbd9201 100644 --- a/openrtb_ext/floors.go +++ b/openrtb_ext/floors.go @@ -88,6 +88,7 @@ type PriceFloorData struct { ModelTimestamp int `json:"modeltimestamp,omitempty"` ModelGroups []PriceFloorModelGroup `json:"modelgroups,omitempty"` FloorProvider string `json:"floorprovider,omitempty"` + UseFetchDataRate *int `json:"usefetchdatarate,omitempty"` } type PriceFloorModelGroup struct { From 161b0a88aed0d88fe461d1fbafd0cfd4876021fb Mon Sep 17 00:00:00 2001 From: Jaydeep Mohite Date: Tue, 2 Jan 2024 14:06:32 +0530 Subject: [PATCH 2/4] Added UT --- floors/floors_test.go | 90 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/floors/floors_test.go b/floors/floors_test.go index df50d8b2649..e9ead57205b 100644 --- a/floors/floors_test.go +++ b/floors/floors_test.go @@ -841,14 +841,47 @@ func (m *MockFetchDataRate100) Stop() { } -func TestResolveFloorsWithUseDataRate(t *testing.T) { - rates := map[string]map[string]float64{ - "USD": { - "INR": 70, - "EUR": 0.9, - "JPY": 5.09, +type MockFetchDataRateNotProvided struct{} + +func (m *MockFetchDataRateNotProvided) Fetch(configs config.AccountPriceFloors) (*openrtb_ext.PriceFloorRules, string) { + + if !configs.UseDynamicData { + return nil, openrtb_ext.FetchNone + } + priceFloors := openrtb_ext.PriceFloorRules{ + Enabled: getTrue(), + PriceFloorLocation: openrtb_ext.RequestLocation, + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: getTrue(), + EnforceRate: 100, + FloorDeals: getTrue(), + }, + Data: &openrtb_ext.PriceFloorData{ + Currency: "USD", + ModelGroups: []openrtb_ext.PriceFloorModelGroup{ + { + ModelVersion: "model from fetched", + Currency: "USD", + Values: map[string]float64{ + "banner|300x600|www.website5.com": 5, + "*|*|*": 15, + }, + Schema: openrtb_ext.PriceFloorSchema{ + Fields: []string{"mediaType", "size", "domain"}, + }, + }, + }, }, } + return &priceFloors, openrtb_ext.FetchSuccess +} + +func (m *MockFetchDataRateNotProvided) Stop() { + +} + +func TestResolveFloorsWithUseDataRate(t *testing.T) { + rates := map[string]map[string]float64{} testCases := []struct { name string @@ -950,6 +983,51 @@ func TestResolveFloorsWithUseDataRate(t *testing.T) { }, }, }, + { + name: "Dynamic fetch enabled, floors from fetched selected as data rate not provided as default value = 100", + fetcher: &MockFetchDataRateNotProvided{}, + bidRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}}, + Ext: json.RawMessage(`{"prebid":{"floors":{"data":{"currency":"USD","modelgroups":[{"modelversion":"model 1 from req","currency":"USD","values":{"banner|300x600|www.website5.com":5,"*|*|*":7},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"enabled":true,"enforcement":{"enforcepbs":true,"floordeals":true,"enforcerate":100}}}}`), + }, + }, + account: config.Account{ + PriceFloors: config.AccountPriceFloors{ + Enabled: true, + UseDynamicData: true, + }, + }, + expFloors: &openrtb_ext.PriceFloorRules{ + Enabled: getTrue(), + FetchStatus: openrtb_ext.FetchSuccess, + PriceFloorLocation: openrtb_ext.FetchLocation, + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: getTrue(), + FloorDeals: getTrue(), + EnforceRate: 100, + }, + Data: &openrtb_ext.PriceFloorData{ + Currency: "USD", + ModelGroups: []openrtb_ext.PriceFloorModelGroup{ + { + ModelVersion: "model from fetched", + Currency: "USD", + Values: map[string]float64{ + "banner|300x600|www.website5.com": 5, + "*|*|*": 15, + }, + Schema: openrtb_ext.PriceFloorSchema{ + Fields: []string{"mediaType", "size", "domain"}, + }, + }, + }, + }, + }, + }, } for _, tc := range testCases { From e0593e396871bd80beeb07556e713bc033bad966 Mon Sep 17 00:00:00 2001 From: Jaydeep Mohite Date: Tue, 9 Jan 2024 12:13:04 +0530 Subject: [PATCH 3/4] Addressed review comments for variable and function rename --- floors/fetcher.go | 4 ++-- floors/fetcher_test.go | 2 +- floors/floors.go | 6 +++--- floors/floors_test.go | 6 +++--- openrtb_ext/floors.go | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/floors/fetcher.go b/floors/fetcher.go index cc1d5360719..5ed96b9ec36 100644 --- a/floors/fetcher.go +++ b/floors/fetcher.go @@ -309,8 +309,8 @@ func validateRules(config config.AccountFloorFetch, priceFloors *openrtb_ext.Pri return errors.New("skip rate should be greater than or equal to 0 and less than 100") } - if priceFloors.Data.UseFetchDataRate != nil && (*priceFloors.Data.UseFetchDataRate < dataRateMin || *priceFloors.Data.UseFetchDataRate > dataRateMax) { - return errors.New("useFetchDataRate should be greater than or equal to 0 and less than or equal to 100") + if priceFloors.Data.FetchRate != nil && (*priceFloors.Data.FetchRate < dataRateMin || *priceFloors.Data.FetchRate > dataRateMax) { + return errors.New("FetchRate should be greater than or equal to 0 and less than or equal to 100") } for _, modelGroup := range priceFloors.Data.ModelGroups { diff --git a/floors/fetcher_test.go b/floors/fetcher_test.go index 92adf12e7cd..356e171212b 100644 --- a/floors/fetcher_test.go +++ b/floors/fetcher_test.go @@ -417,7 +417,7 @@ func TestValidatePriceFloorRules(t *testing.T) { "*|*|www.website.com": 15.01, }, }}, - UseFetchDataRate: ptrutil.ToPtr(-11), + FetchRate: ptrutil.ToPtr(-11), }, }, }, diff --git a/floors/floors.go b/floors/floors.go index f8e2a9d3dcd..af7422e4c52 100644 --- a/floors/floors.go +++ b/floors/floors.go @@ -138,8 +138,8 @@ func isPriceFloorsEnabledForRequest(bidRequestWrapper *openrtb_ext.RequestWrappe return true } -// shouldUseFetchedData will check if to use fetched data or request data -func shouldUseFetchedData(rate *int) bool { +// useFetchedData will check if to use fetched data or request data +func useFetchedData(rate *int) bool { if rate == nil { return true } @@ -166,7 +166,7 @@ func resolveFloors(account config.Account, bidRequestWrapper *openrtb_ext.Reques fetchResult, fetchStatus = priceFloorFetcher.Fetch(account.PriceFloors) } - if fetchResult != nil && fetchStatus == openrtb_ext.FetchSuccess && shouldUseFetchedData(fetchResult.Data.UseFetchDataRate) { + if fetchResult != nil && fetchStatus == openrtb_ext.FetchSuccess && useFetchedData(fetchResult.Data.FetchRate) { mergedFloor := mergeFloors(reqFloor, fetchResult, conversions) floorRules, errList = createFloorsFrom(mergedFloor, account, fetchStatus, openrtb_ext.FetchLocation) } else if reqFloor != nil { diff --git a/floors/floors_test.go b/floors/floors_test.go index e9ead57205b..9e2f411c1c6 100644 --- a/floors/floors_test.go +++ b/floors/floors_test.go @@ -791,7 +791,7 @@ func (m *MockFetchDataRate0) Fetch(configs config.AccountPriceFloors) (*openrtb_ }, }, }, - UseFetchDataRate: ptrutil.ToPtr(0), + FetchRate: ptrutil.ToPtr(0), }, } return &priceFloors, openrtb_ext.FetchSuccess @@ -831,7 +831,7 @@ func (m *MockFetchDataRate100) Fetch(configs config.AccountPriceFloors) (*openrt }, }, }, - UseFetchDataRate: ptrutil.ToPtr(100), + FetchRate: ptrutil.ToPtr(100), }, } return &priceFloors, openrtb_ext.FetchSuccess @@ -979,7 +979,7 @@ func TestResolveFloorsWithUseDataRate(t *testing.T) { }, }, }, - UseFetchDataRate: ptrutil.ToPtr(100), + FetchRate: ptrutil.ToPtr(100), }, }, }, diff --git a/openrtb_ext/floors.go b/openrtb_ext/floors.go index a429cbd9201..92dab2acd90 100644 --- a/openrtb_ext/floors.go +++ b/openrtb_ext/floors.go @@ -88,7 +88,7 @@ type PriceFloorData struct { ModelTimestamp int `json:"modeltimestamp,omitempty"` ModelGroups []PriceFloorModelGroup `json:"modelgroups,omitempty"` FloorProvider string `json:"floorprovider,omitempty"` - UseFetchDataRate *int `json:"usefetchdatarate,omitempty"` + FetchRate *int `json:"fetchrate,omitempty"` } type PriceFloorModelGroup struct { From a394f69278f49d7c1f1b1f164684feda0571c728 Mon Sep 17 00:00:00 2001 From: Jaydeep Mohite Date: Wed, 10 Jan 2024 09:37:08 +0530 Subject: [PATCH 4/4] Updated UT name --- floors/fetcher_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/floors/fetcher_test.go b/floors/fetcher_test.go index 356e171212b..cdf0537c9ed 100644 --- a/floors/fetcher_test.go +++ b/floors/fetcher_test.go @@ -398,7 +398,7 @@ func TestValidatePriceFloorRules(t *testing.T) { wantErr: true, }, { - name: "Invalid useFetchDataRate", + name: "Invalid FetchRate", args: args{ configs: config.AccountFloorFetch{ Enabled: true,