diff --git a/floors/fetcher.go b/floors/fetcher.go index 6df43445010..5ed96b9ec36 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.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 { 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..cdf0537c9ed 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 FetchRate", + 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, + }, + }}, + FetchRate: 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..af7422e4c52 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 } +// useFetchedData will check if to use fetched data or request data +func useFetchedData(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 && 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 047b6738cd3..9e2f411c1c6 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,283 @@ 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"}, + }, + }, + }, + FetchRate: 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"}, + }, + }, + }, + FetchRate: ptrutil.ToPtr(100), + }, + } + return &priceFloors, openrtb_ext.FetchSuccess +} + +func (m *MockFetchDataRate100) Stop() { + +} + +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 + 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"}, + }, + }, + }, + FetchRate: ptrutil.ToPtr(100), + }, + }, + }, + { + 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 { + 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..92dab2acd90 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"` + FetchRate *int `json:"fetchrate,omitempty"` } type PriceFloorModelGroup struct {