diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..0ea4f4c4318 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @viral-vala @shriprasad-marathe @ganesh-salpure diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000000..d124f544bd2 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +# Description + +Please add change description or link to ticket, docs, etc. + +# Checklist: + +- [ ] PR commit list is unique (rebase/pull with the origin branch to keep master clean). +- [ ] JIRA number is added in the PR title and the commit message. +- [ ] Updated the `header-bidding` repo with appropiate commit id. +- [ ] Documented the new changes. + +For Prebid upgrade, refer: https://inside.pubmatic.com:8443/confluence/display/Products/Prebid-server+upgrade diff --git a/README.md b/README.md index dab5523e037..7c61a88bad0 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ Download and prepare Prebid Server: ```bash cd YOUR_DIRECTORY -git clone https://github.com/prebid/prebid-server src/github.com/prebid/prebid-server -cd src/github.com/prebid/prebid-server +git clone https://github.com/PubMatic-OpenWrap/prebid-server src/github.com/PubMatic-OpenWrap/prebid-server +cd src/github.com/PubMatic-OpenWrap/prebid-server ``` Run the automated tests: @@ -60,11 +60,10 @@ of exported types. Want to [add an adapter](https://docs.prebid.org/prebid-server/developers/add-new-bidder-go.html)? Found a bug? Great! -Report bugs, request features, and suggest improvements [on Github](https://github.com/prebid/prebid-server/issues). - -Or better yet, [open a pull request](https://github.com/prebid/prebid-server/compare) with the changes you'd like to see. +Or better yet, [open a pull request](https://github.com/PubMatic-OpenWrap/prebid-server/compare) with the changes you'd like to see. ## IDE Recommendations The quickest way to start developing Prebid Server in a reproducible environment isolated from your host OS is by using Visual Studio Code with [Remote Container Setup](devcontainer.md). + diff --git a/adapters/adbuttler/adbuttler.go b/adapters/adbuttler/adbuttler.go new file mode 100644 index 00000000000..eb37928a564 --- /dev/null +++ b/adapters/adbuttler/adbuttler.go @@ -0,0 +1,37 @@ +package adbuttler + +import ( + "fmt" + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/macros" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type AdButtlerAdapter struct { + endpoint *template.Template +} + +// Builder builds a new instance of the AdButtler adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { + + endpointtemplate, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + bidder := &AdButtlerAdapter{ + endpoint: endpointtemplate, + } + return bidder, nil +} + +func (a *AdButtlerAdapter) buildEndpointURL(accountID, zoneID string) (string, error) { + endpointParams := macros.EndpointTemplateParams{ + AccountID: accountID, + ZoneID: zoneID, + } + return macros.ResolveMacros(a.endpoint, endpointParams) +} diff --git a/adapters/adbuttler/adbuttler_request.go b/adapters/adbuttler/adbuttler_request.go new file mode 100644 index 00000000000..54fd92c0767 --- /dev/null +++ b/adapters/adbuttler/adbuttler_request.go @@ -0,0 +1,239 @@ +package adbuttler + +import ( + "encoding/json" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" +) + +type AdButlerRequest struct { + SearchString string `json:"search,omitempty"` + SearchType string `json:"search_type,omitempty"` + Params map[string][]string `json:"params,omitempty"` + Identifiers []string `json:"identifiers,omitempty"` + Target map[string]interface{} `json:"_abdk_json,omitempty"` + Limit int `json:"limit,omitempty"` + Source string `json:"source,omitempty"` + UserID string `json:"adb_uid,omitempty"` + IP string `json:"ip,omitempty"` + UserAgent string `json:"ua,omitempty"` + Referrer string `json:"referrer,omitempty"` + FloorCPC float64 `json:"bid_floor_cpc,omitempty"` + IsTestRequest bool `json:"test_request,omitempty"` + +} + +func isLowercaseNumbersDashes(s string) bool { + // Define a regular expression pattern to match lowercase letters, numbers, and dashes + pattern := "^[a-z0-9-]+$" + re := regexp.MustCompile(pattern) + + // Use the MatchString function to check if the string matches the pattern + return re.MatchString(s) +} + + +func (a *AdButtlerAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + + commerceExt, siteExt, _,errors := adapters.ValidateCommRequest(request) + if len(errors) > 0 { + return nil, errors + } + + var configValueMap = make(map[string]string) + var configTypeMap = make(map[string]int) + for _,obj := range commerceExt.Bidder.CustomConfig { + configValueMap[obj.Key] = obj.Value + configTypeMap[obj.Key] = obj.Type + } + + var adButlerReq AdButlerRequest + //Assign Page Source if Present + if siteExt != nil { + if isLowercaseNumbersDashes(siteExt.Page) { + adButlerReq.Source = siteExt.Page + } + } + + //Retrieve AccountID and ZoneID from Request and Build endpoint Url + var accountID, zoneID string + val, ok := configValueMap[BIDDERDETAILS_PREFIX + BD_ACCOUNT_ID] + if ok { + accountID = val + } + + val, ok = configValueMap[BIDDERDETAILS_PREFIX + BD_ZONE_ID] + if ok { + zoneID = val + } + + endPoint, err := a.buildEndpointURL(accountID, zoneID) + if err != nil { + return nil, []error{err} + } + + adButlerReq.Target = make(map[string]interface{}) + //Add User Targeting + if request.User != nil { + if(request.User.Yob > 0) { + now := time.Now() + age := int64(now.Year()) - request.User.Yob + adButlerReq.Target[USER_AGE] = age + } + + if request.User.Gender != "" { + if strings.EqualFold(request.User.Gender, "M") { + adButlerReq.Target[USER_GENDER] = GENDER_MALE + } else if strings.EqualFold(request.User.Gender, "F") { + adButlerReq.Target[USER_GENDER] = GENDER_FEMALE + } else if strings.EqualFold(request.User.Gender, "O") { + adButlerReq.Target[USER_GENDER] = GENDER_OTHER + } + } + } + + //Add Geo Targeting + if request.Device != nil && request.Device.Geo != nil { + if request.Device.Geo.Country != "" { + adButlerReq.Target[COUNTRY] = request.Device.Geo.Country + } + if request.Device.Geo.Region != "" { + adButlerReq.Target[REGION] = request.Device.Geo.Region + } + if request.Device.Geo.City != "" { + adButlerReq.Target[CITY] = request.Device.Geo.City + } + } + //Add Geo Targeting + if request.Device != nil { + switch request.Device.DeviceType { + case 1: + adButlerReq.Target[DEVICE] = DEVICE_COMPUTER + case 2: + adButlerReq.Target[DEVICE] = DEVICE_PHONE + case 3: + adButlerReq.Target[DEVICE] = DEVICE_TABLET + case 4: + adButlerReq.Target[DEVICE] = DEVICE_CONNECTEDDEVICE + } + } + + //Add Page Source Targeting + if adButlerReq.Source != "" { + adButlerReq.Target[PAGE_SOURCE] = adButlerReq.Source + } + + //Add Dynamic Targeting from AdRequest + for _,targetObj := range commerceExt.ComParams.Targeting { + key := targetObj.Name + adButlerReq.Target[key] = targetObj.Value + } + //Add Identifiers from AdRequest + for _,prefObj := range commerceExt.ComParams.Preferred { + adButlerReq.Identifiers = append(adButlerReq.Identifiers, prefObj.ProductID) + } + + //Add Category Params from AdRequest + if len(adButlerReq.Identifiers) <= 0 && commerceExt.ComParams.Filtering != nil { + adButlerReq.Params = make(map[string][]string) + if commerceExt.ComParams.Filtering.Category != nil && len(commerceExt.ComParams.Filtering.Category) > 0 { + //Retailer Specific Category Name is present from Product Feed Template + val, ok = configValueMap[PRODUCTTEMPLATE_PREFIX + PD_TEMPLATE_CATEGORY] + if ok { + adButlerReq.Params[val] = commerceExt.ComParams.Filtering.Category + } else { + adButlerReq.Params[DEFAULT_CATEGORY] = commerceExt.ComParams.Filtering.Category + } + } + + if commerceExt.ComParams.Filtering.Brand != nil && len(commerceExt.ComParams.Filtering.Brand) > 0 { + //Retailer Specific Brand Name is present from Product Feed Template + val, ok = configValueMap[PRODUCTTEMPLATE_PREFIX + PD_TEMPLATE_BRAND] + if ok { + adButlerReq.Params[val] = commerceExt.ComParams.Filtering.Brand + } else { + adButlerReq.Params[DEFAULT_BRAND] = commerceExt.ComParams.Filtering.Brand + } + } + + if commerceExt.ComParams.Filtering.SubCategory != nil { + for _,subCategory := range commerceExt.ComParams.Filtering.SubCategory { + key := subCategory.Name + value := subCategory.Value + adButlerReq.Params[key] = value + } + } + } + + + //Assign Search Term if present along with searchType + if len(adButlerReq.Identifiers) <= 0 && commerceExt.ComParams.Filtering == nil && commerceExt.ComParams.SearchTerm != "" { + adButlerReq.SearchString = commerceExt.ComParams.SearchTerm + if commerceExt.ComParams.SearchType == SEARCHTYPE_EXACT || + commerceExt.ComParams.SearchType == SEARCHTYPE_BROAD { + adButlerReq.SearchType = commerceExt.ComParams.SearchType + } else { + val, ok := configValueMap[SEARCHTYPE] + if ok { + adButlerReq.SearchType = val + } else { + adButlerReq.SearchType = SEARCHTYPE_DEFAULT + } + } + } + + adButlerReq.IP = request.Device.IP + // Domain Name from Site Object if Prsent or App Obj + if request.Site != nil { + adButlerReq.Referrer = request.Site.Domain + } else { + adButlerReq.Referrer = request.App.Domain + } + + // Take BidFloor from BidRequest - High Priority, Otherwise from Auction Config + if request.Imp[0].BidFloor > 0 { + adButlerReq.FloorCPC = request.Imp[0].BidFloor + } else { + val, ok := configValueMap[AUCTIONDETAILS_PREFIX + AD_FLOOR_PRICE] + if ok { + if floorPrice, err := strconv.ParseFloat(val, 64); err == nil { + adButlerReq.FloorCPC = floorPrice + } + } + } + + //Test Request + if commerceExt.ComParams.TestRequest { + adButlerReq.IsTestRequest = true + } + adButlerReq.UserID = request.User.ID + adButlerReq.UserAgent = request.Device.UA + adButlerReq.Limit = commerceExt.ComParams.SlotsRequested + + //Temporarily for Debugging + //u, _ := json.Marshal(adButlerReq) + //fmt.Println(string(u)) + + reqJSON, err := json.Marshal(adButlerReq) + if err != nil { + return nil, []error{err} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json") + + return []*adapters.RequestData{{ + Method: "POST", + Uri: endPoint, + Body: reqJSON, + Headers: headers, + }}, nil + +} + diff --git a/adapters/adbuttler/adbuttler_response.go b/adapters/adbuttler/adbuttler_response.go new file mode 100644 index 00000000000..28fbe4a031c --- /dev/null +++ b/adapters/adbuttler/adbuttler_response.go @@ -0,0 +1,202 @@ +package adbuttler + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type AdButlerBeacon struct { + Type string `json:"type,omitempty"` + TrackingUrl string `json:"url,omitempty"` +} + +type AdButlerBid struct { + CPCBid float64 `json:"cpc_bid,omitempty"` + CPCSpend float64 `json:"cpc_spend,omitempty"` + CampaignID int64 `json:"campaign_id,omitempty"` + ProductData map[string]string `json:"item,omitempty"` + Beacons []*AdButlerBeacon `json:"beacons,omitempty"` +} + +type AdButlerResponse struct { + Status string `json:"status,omitempty"` + Code int32 `json:"code,omitempty"` + Bids []*AdButlerBid `json:"items,omitempty"` +} + +func (a *AdButtlerAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errors []error + + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + + if response.StatusCode == http.StatusBadRequest { + err := &errortypes.BadInput{ + Message: "Unexpected status code: 400. Bad request from Adbutler.", + } + return nil, []error{err} + } + + if response.StatusCode != http.StatusOK { + err := &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d", response.StatusCode), + } + return nil, []error{err} + } + + var adButlerResp AdButlerResponse + if err := json.Unmarshal(response.Body, &adButlerResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Bad Server Response", + }} + } + + //Temporarily for Debugging + //u, _ := json.Marshal(adButlerResp) + //fmt.Println(string(u)) + + if adButlerResp.Status == RESPONSE_NOADS { + return nil, []error{&errortypes.BidderFailedSchemaValidation{ + Message: fmt.Sprintf("Error Occured at Adbutler for the given request with ErrorCode %d", adButlerResp.Code), + }} + } + + if adButlerResp.Status == RESPONSE_SUCCESS && (adButlerResp.Bids == nil || + len(adButlerResp.Bids) <= 0) { + return nil, []error{&errortypes.NoBidPrice{ + Message: "No Bid For the given Request", + }} + } + + if adButlerResp.Status == RESPONSE_SUCCESS && (adButlerResp.Bids != nil && + len(adButlerResp.Bids) > 0) { + impID := internalRequest.Imp[0].ID + responseF := a.GetBidderResponse(internalRequest, &adButlerResp, impID) + return responseF, errors + } + + err := fmt.Errorf("unknown error occcured for the given request from adbutler") + errors = append(errors, err) + + return nil, errors + +} + +func (a *AdButtlerAdapter) GetBidderResponse(request *openrtb2.BidRequest, adButlerResp *AdButlerResponse, requestImpID string) *adapters.BidderResponse { + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(adButlerResp.Bids)) + var commerceExt *openrtb_ext.ExtImpCommerce + var adbutlerID, zoneID, adbUID, keyToRemove string + var configValueMap = make(map[string]string) + + if len(request.Imp) > 0 { + commerceExt, _ = adapters.GetImpressionExtComm(&(request.Imp[0])) + for _, obj := range commerceExt.Bidder.CustomConfig { + configValueMap[obj.Key] = obj.Value + } + + val, ok := configValueMap[BIDDERDETAILS_PREFIX+BD_ACCOUNT_ID] + if ok { + adbutlerID = val + } + + val, ok = configValueMap[BIDDERDETAILS_PREFIX+BD_ZONE_ID] + if ok { + zoneID = val + } + adbUID = request.User.ID + + } + + for index, adButlerBid := range adButlerResp.Bids { + + bidID := adapters.GenerateUniqueBidIDComm() + impID := requestImpID + "_" + strconv.Itoa(index+1) + bidPrice := adButlerBid.CPCBid + campaignID := strconv.FormatInt(adButlerBid.CampaignID, 10) + clickPrice := adButlerBid.CPCSpend + + var productid string + //Retailer Specific ProductID is present from Product Feed Template + val, ok := configValueMap[PRODUCTTEMPLATE_PREFIX + PD_TEMPLATE_PRODUCTID] + if ok { + productid = adButlerBid.ProductData[val] + keyToRemove = val + } + if productid == "" { + productid = adButlerBid.ProductData[DEFAULT_PRODUCTID] + keyToRemove = DEFAULT_PRODUCTID + } + + productDetails := make(map[string]interface{}) + for key, value := range adButlerBid.ProductData { + productDetails[key] = value + } + + // Delete the "Product Id" key if present + if _, ok := productDetails[keyToRemove]; ok { + delete(productDetails, keyToRemove) + } + + var impressionUrl, clickUrl, conversionUrl string + for _, beacon := range adButlerBid.Beacons { + switch beacon.Type { + case BEACONTYPE_IMP: + impressionUrl = IMP_KEY + adapters.EncodeURL(beacon.TrackingUrl) + case BEACONTYPE_CLICK: + clickUrl = CLICK_KEY + adapters.EncodeURL(beacon.TrackingUrl) + } + } + + conversionUrl = GenerateConversionUrl(adbutlerID, zoneID, adbUID, productid) + + bidExt := &openrtb_ext.ExtBidCommerce{ + ProductId: productid, + ClickUrl: clickUrl, + ClickPrice: clickPrice, + ConversionUrl: conversionUrl, + ProductDetails: productDetails, + } + + bid := &openrtb2.Bid{ + ID: bidID, + ImpID: impID, + Price: bidPrice, + CID: campaignID, + IURL: impressionUrl, + } + + adapters.AddDefaultFieldsComm(bid) + + bidExtJSON, err1 := json.Marshal(bidExt) + if nil == err1 { + bid.Ext = json.RawMessage(bidExtJSON) + } + + typedbid := &adapters.TypedBid{ + Bid: bid, + Seat: openrtb_ext.BidderName(SEAT_ADBUTLER), + } + bidResponse.Bids = append(bidResponse.Bids, typedbid) + } + return bidResponse +} + +func GenerateConversionUrl(adbutlerID, zoneID, adbUID, productID string) string { + conversionUrl := strings.Replace(CONVERSION_URL, CONV_ADBUTLERID, adbutlerID, 1) + conversionUrl = strings.Replace(conversionUrl, CONV_ZONEID, zoneID, 1) + conversionUrl = strings.Replace(conversionUrl, CONV_ADBUID, adbUID, 1) + conversionUrl = strings.Replace(conversionUrl, CONV_IDENTIFIER, productID, 1) + + return conversionUrl +} + diff --git a/adapters/adbuttler/constant.go b/adapters/adbuttler/constant.go new file mode 100644 index 00000000000..c84f8756cd1 --- /dev/null +++ b/adapters/adbuttler/constant.go @@ -0,0 +1,54 @@ +package adbuttler + +const ( + BIDDERDETAILS_PREFIX = "BD_" + AUCTIONDETAILS_PREFIX = "AD_" + PRODUCTTEMPLATE_PREFIX = "PT_" + AD_FLOOR_PRICE = "floor_price" + BD_ZONE_ID = "catalogZone" + BD_ACCOUNT_ID = "accountID" + SEARCHTYPE_DEFAULT = "exact" + SEARCHTYPE_EXACT = "exact" + SEARCHTYPE_BROAD = "broad" + SEARCHTYPE = "search_type" + PAGE_SOURCE = "page_source" + USER_AGE = "target_age" + GENDER_MALE = "Male" + GENDER_FEMALE = "Female" + GENDER_OTHER = "Others" + DEVICE_COMPUTER = "Personal Computer" + DEVICE_PHONE = "Phone" + DEVICE_TABLET = "Tablet" + DEVICE_CONNECTEDDEVICE = "Connected Devices" + USER_GENDER = "target_gender" + COUNTRY = "target_country" + REGION = "target_region" + CITY = "target_city" + DEVICE = "target_device" + DEFAULT_CATEGORY = "Category" + DEFAULT_BRAND = "Brand Name" + DEFAULT_PRODUCTID = "Product Id" + RESPONSE_SUCCESS = "success" + RESPONSE_NOADS = "NO_ADS" + SEAT_ADBUTLER = "adbuttler" + BEACONTYPE_IMP = "impression" + BEACONTYPE_CLICK = "click" + IMP_KEY = "tps_impurl=" + CLICK_KEY = "tps_clkurl=" + CONV_HOSTNAME = "conv_host" + CONVERSION_URL = `tps_ID=conv_adbutlerID&tps_setID=conv_zoneID&tps_adb_uid=conv_adbUID&tps_identifier=conv_Identifier` + CONV_ADBUTLERID = "conv_adbutlerID" + CONV_ZONEID = "conv_zoneID" + CONV_ADBUID = "conv_adbUID" + CONV_IDENTIFIER = "conv_Identifier" + PD_TEMPLATE_BRAND = "brandName" + PD_TEMPLATE_CATEGORY = "categories" + PD_TEMPLATE_PRODUCTID = "productId" + DATATYE_NUMBER = 1 + DATATYE_STRING = 2 + DATATYE_ARRAY = 3 + DATATYE_DATE = 4 + DATATYE_TIME = 5 + DATATYE_DATETIME = 6 +) + diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go index 1c45904258b..c7d08b59415 100644 --- a/adapters/appnexus/appnexus.go +++ b/adapters/appnexus/appnexus.go @@ -169,13 +169,15 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E imps := request.Imp + // Commenting out the following piece of code to avoid populating adpod_id in the Appnexus request (ref: https://inside.pubmatic.com:9443/jira/browse/UOE-6196) + // For long form requests if adpodId feature enabled, adpod_id must be sent downstream. // Adpod id is a unique identifier for pod // All impressions in the same pod must have the same pod id in request extension // For this all impressions in request should belong to the same pod // If impressions number per pod is more than maxImpsPerReq - divide those imps to several requests but keep pod id the same // If adpodId feature disabled and impressions number per pod is more than maxImpsPerReq - divide those imps to several requests but do not include ad pod id - if isVIDEO == 1 && *adPodId { + /*if isVIDEO == 1 && *adPodId { podImps := groupByPods(imps) requests := make([]*adapters.RequestData, 0, len(podImps)) @@ -187,7 +189,7 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E errs = append(errs, errors...) } return requests, errs - } + }*/ return splitRequests(imps, request, reqExt, thisURI, errs) } @@ -474,7 +476,11 @@ func resolvePlatformID(platformID string) int { func loadCategoryMapFromFileSystem() map[string]string { // Load custom options for our adapter (currently just a lookup table to convert appnexus => iab categories) - opts, err := ioutil.ReadFile("./static/adapter/appnexus/opts.json") + opts, err := ioutil.ReadFile("./home/http/GO_SERVER/dmhbserver/static/adapter/appnexus/opts.json") + //this is for tests + if err != nil { + opts, err = ioutil.ReadFile("./static/adapter/appnexus/opts.json") + } if err == nil { var adapterOptions appnexusAdapterOptions diff --git a/adapters/appnexus/appnexus_test.go b/adapters/appnexus/appnexus_test.go index 164c9ce6768..fc475d695d3 100644 --- a/adapters/appnexus/appnexus_test.go +++ b/adapters/appnexus/appnexus_test.go @@ -1,17 +1,11 @@ package appnexus import ( - "encoding/json" - "regexp" "testing" - "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/adapters/adapterstest" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" - - "github.com/mxmCherry/openrtb/v16/openrtb2" - "github.com/stretchr/testify/assert" ) func TestJsonSamples(t *testing.T) { @@ -45,7 +39,8 @@ func TestMemberQueryParam(t *testing.T) { } } -func TestVideoSinglePod(t *testing.T) { +// Commenting out the test cases around populating adpod_id in the Appnexus request (ref: https://inside.pubmatic.com:9443/jira/browse/UOE-6196) +/*func TestVideoSinglePod(t *testing.T) { var a adapter a.URI = "http://test.com/openrtb2" a.hbSource = 5 @@ -269,3 +264,4 @@ func TestVideoTwoPodsManyImps(t *testing.T) { assert.Len(t, podIds, 2, "Incorrect number of unique pod ids") } +*/ diff --git a/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json b/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json index 5a453979f7c..d0940a2345d 100644 --- a/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json +++ b/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json @@ -47,7 +47,6 @@ "id": "test-request-id", "ext": { "appnexus": { - "adpod_id": "5577006791947779410", "hb_source": 6 }, "prebid": {} diff --git a/adapters/bidder.go b/adapters/bidder.go index 6a4df52515d..7301d487e70 100644 --- a/adapters/bidder.go +++ b/adapters/bidder.go @@ -99,6 +99,7 @@ type TypedBid struct { BidMeta *openrtb_ext.ExtBidPrebidMeta BidType openrtb_ext.BidType BidVideo *openrtb_ext.ExtBidPrebidVideo + BidTargets map[string]string DealPriority int Seat openrtb_ext.BidderName } @@ -114,12 +115,20 @@ type ResponseData struct { Headers http.Header } +type BidRequestParams struct { + ImpIndex int + VASTTagIndex int +} + // RequestData packages together the fields needed to make an http.Request. type RequestData struct { + Params *BidRequestParams Method string Uri string Body []byte Headers http.Header + + BidderName openrtb_ext.BidderName `json:"-"` } // ExtImpBidder can be used by Bidders to unmarshal any request.imp[i].ext. diff --git a/adapters/conversant/conversant.go b/adapters/conversant/conversant.go index 738a7fc6345..8989793fb4f 100644 --- a/adapters/conversant/conversant.go +++ b/adapters/conversant/conversant.go @@ -73,7 +73,7 @@ func (c ConversantAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *a } func parseCnvrParams(imp *openrtb2.Imp, cnvrExt openrtb_ext.ExtImpConversant) { - imp.DisplayManager = "prebid-s2s" + imp.DisplayManager = "pubmatic-openwrap" imp.DisplayManagerVer = "2.0.0" if imp.BidFloor <= 0 && cnvrExt.BidFloor > 0 { diff --git a/adapters/conversant/conversanttest/exemplary/banner.json b/adapters/conversant/conversanttest/exemplary/banner.json index 472e18f712d..9c3569eb40d 100644 --- a/adapters/conversant/conversanttest/exemplary/banner.json +++ b/adapters/conversant/conversanttest/exemplary/banner.json @@ -41,7 +41,7 @@ "tagid": "mytag", "secure": 1, "bidfloor": 0.01, - "displaymanager": "prebid-s2s", + "displaymanager": "pubmatic-openwrap", "displaymanagerver": "2.0.0", "banner": { "format": [{"w": 300, "h": 250}] diff --git a/adapters/conversant/conversanttest/exemplary/simple_app.json b/adapters/conversant/conversanttest/exemplary/simple_app.json index 303c60f75a9..d094f1f2c7f 100644 --- a/adapters/conversant/conversanttest/exemplary/simple_app.json +++ b/adapters/conversant/conversanttest/exemplary/simple_app.json @@ -45,7 +45,7 @@ "tagid": "mytag", "secure": 1, "bidfloor": 0.01, - "displaymanager": "prebid-s2s", + "displaymanager": "pubmatic-openwrap", "displaymanagerver": "2.0.0", "banner": { "format": [{"w": 300, "h": 250}] diff --git a/adapters/conversant/conversanttest/exemplary/video.json b/adapters/conversant/conversanttest/exemplary/video.json index 475dd796262..56606aab461 100644 --- a/adapters/conversant/conversanttest/exemplary/video.json +++ b/adapters/conversant/conversanttest/exemplary/video.json @@ -49,7 +49,7 @@ "tagid": "mytag", "secure": 1, "bidfloor": 0.01, - "displaymanager": "prebid-s2s", + "displaymanager": "pubmatic-openwrap", "displaymanagerver": "2.0.0", "video": { "w": 300, diff --git a/adapters/conversant/conversanttest/supplemental/server_badresponse.json b/adapters/conversant/conversanttest/supplemental/server_badresponse.json index 96cb4b46452..0cab9523ba6 100644 --- a/adapters/conversant/conversanttest/supplemental/server_badresponse.json +++ b/adapters/conversant/conversanttest/supplemental/server_badresponse.json @@ -30,7 +30,7 @@ "imp": [ { "id": "1", - "displaymanager": "prebid-s2s", + "displaymanager": "pubmatic-openwrap", "displaymanagerver": "2.0.0", "banner": { "format": [{"w": 300, "h": 250}] diff --git a/adapters/conversant/conversanttest/supplemental/server_nocontent.json b/adapters/conversant/conversanttest/supplemental/server_nocontent.json index ad86d19d6b2..15707c757e3 100644 --- a/adapters/conversant/conversanttest/supplemental/server_nocontent.json +++ b/adapters/conversant/conversanttest/supplemental/server_nocontent.json @@ -30,7 +30,7 @@ "imp": [ { "id": "1", - "displaymanager": "prebid-s2s", + "displaymanager": "pubmatic-openwrap", "displaymanagerver": "2.0.0", "banner": { "format": [{"w": 300, "h": 250}] diff --git a/adapters/conversant/conversanttest/supplemental/server_unknownstatus.json b/adapters/conversant/conversanttest/supplemental/server_unknownstatus.json index 85586f066c6..cabfeaf321c 100644 --- a/adapters/conversant/conversanttest/supplemental/server_unknownstatus.json +++ b/adapters/conversant/conversanttest/supplemental/server_unknownstatus.json @@ -30,7 +30,7 @@ "imp": [ { "id": "1", - "displaymanager": "prebid-s2s", + "displaymanager": "pubmatic-openwrap", "displaymanagerver": "2.0.0", "banner": { "format": [{"w": 300, "h": 250}] diff --git a/adapters/conversant/conversanttest/supplemental/test_params.json b/adapters/conversant/conversanttest/supplemental/test_params.json index 403bcc42226..cf71299df0f 100644 --- a/adapters/conversant/conversanttest/supplemental/test_params.json +++ b/adapters/conversant/conversanttest/supplemental/test_params.json @@ -107,7 +107,7 @@ "bidfloor": 7, "secure": 1, "tagid": "mytag", - "displaymanager": "prebid-s2s", + "displaymanager": "pubmatic-openwrap", "displaymanagerver": "2.0.0", "video": { "api": [1,2], @@ -126,7 +126,7 @@ "bidfloor": 1, "secure": 1, "tagid": "mytag", - "displaymanager": "prebid-s2s", + "displaymanager": "pubmatic-openwrap", "displaymanagerver": "2.0.0", "video": { "api": [1,2], @@ -154,7 +154,7 @@ "bidfloor": 7, "secure": 1, "tagid": "mytag", - "displaymanager": "prebid-s2s", + "displaymanager": "pubmatic-openwrap", "displaymanagerver": "2.0.0", "video": { "api": [1,2], @@ -182,7 +182,7 @@ "bidfloor": -3, "secure": 1, "tagid": "mytag", - "displaymanager": "prebid-s2s", + "displaymanager": "pubmatic-openwrap", "displaymanagerver": "2.0.0", "video": { "api": [1,2], diff --git a/adapters/criteoretail/constant.go b/adapters/criteoretail/constant.go new file mode 100644 index 00000000000..59d7317d99a --- /dev/null +++ b/adapters/criteoretail/constant.go @@ -0,0 +1,16 @@ +package criteoretail + +const ( + AUCTIONDETAILS_PREFIX = "AD_" + AD_ACCOUNT_ID = "account_id" + RESPONSE_OK = "OK" + SEAT_CRITEORETAIL = "criteoretail" + IMP_KEY = "tps_impurl=" + CLICK_KEY = "tps_clkurl=" + FORMAT_SPONSORED = "sponsored_products" + CLICK_PRICE = "ComparePrice" + BID_PRICE = "Price" + PRODUCT_ID = "ProductID" + VIEW_BEACON = "OnViewBeacon" + CLICK_BEACON = "OnClickBeacon" +) diff --git a/adapters/criteoretail/criteoretail.go b/adapters/criteoretail/criteoretail.go new file mode 100644 index 00000000000..18673558e22 --- /dev/null +++ b/adapters/criteoretail/criteoretail.go @@ -0,0 +1,20 @@ +package criteoretail + +import ( + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type CriteoRetailAdapter struct { + endpoint string +} + +// Builder builds a new instance of the AdButtler adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { + bidder := &CriteoRetailAdapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + diff --git a/adapters/criteoretail/criteoretail_mockresponse.go b/adapters/criteoretail/criteoretail_mockresponse.go new file mode 100644 index 00000000000..4c1202d1791 --- /dev/null +++ b/adapters/criteoretail/criteoretail_mockresponse.go @@ -0,0 +1,119 @@ +package criteoretail + +import ( + "encoding/json" + "math/rand" + "strconv" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/openrtb_ext" +) + +var mockProductDetails = map[string]interface{}{ + "ProductName": "Comet Professional Multi Purpose Disinfecting - Sanitizing Liquid Bathroom Cleaner Spray, 32 fl oz. (19214)", + "Image": "https://www.staples-3p.com/s7/is/image/Staples/7246D7B1-E4B9-4AA7-9261723D16E5023B_sc7?$std$", + "ProductPage": "//b.va.us.criteo.com/rm?dest=https%3a%2f%2fwww.staples.com%2fcomet-sanitizing-bathroom-cleaner-32-oz%2fproduct_24442430%3fcid%3dBNR%3a24442430%26ci_src%3d17588969%26ci_sku%3d24442430%26KPID%3d24442430&sig=1-4QbIhl7enF8z5Znj9uK8DV4gv_b2ZKe2Lh5HS3-bdpk&rm_e=YwVUlwDHD1yRccXhY_Ge0ja0KIBxHmS8Afyav-5cSdx0QvY4RquAAXx_8LEJg4urqqOpSIf06OGnwvoRI2oQbLyeSkU-r4ssFHxwbZJBEQDNB8ba9rr-NE9n9Ba2-hUZoUEGeOueYCRUznfSVOsfaYFyIxiM9DG2D_YpIOyLi0OXr6M-x7Os8_l01yD-Ckf9SG-8b_dPTfo_Jvlp2wHRWRbz_JexsXMrF1nDg7Gg6cYrZs3fMPF2wbGgNN9ijxC2zGQ2SiWy0kiEOofJAjp8F6Bxa445dQnGhLJNdlmZMeMrzIVSLb4elmaQvlcCVHF14PG_kabsptpJsPc7W8x7ONQgkwKL1gfXgU4IDjCRMZoVckV0RZY8v3t8a_QlkF9BHss4t5TtH4u4_tRgqdwKVBhl9OV5fSEsMJ1P-ir3ddIceuzyZX8WXSbxOebRf4i15xls9t6s9-zIxSFiVr_AT_HwU5SnIeVPlCES7CBUSxy-_NqnSRTTGdqCvVi_ElReUyghh7w7_TwwOUm4qMeAd-cEhhBQMPQHRXDElAVOIHyhWp7R1u-GzgSWfF93jxdR0Z7kN2bvhvPzi5Cqf3QSxA&ev=4", + "Rating": "5.0", + "RenderingAttributes": "{\"additional_producturl\":\"https://www.staples.com/comet-sanitizing-bathroom-cleaner-32-oz/product_24442430?cid=BNR:24442430&ci_src=17588969&ci_sku=24442430&KPID=24442430\",\"brand\":\"comet\",\"delivery_message\":\"1 Business Day\",\"google_product_type\":\"Cleaning Supplies > Cleaning Chemicals & Wipes > Cleaning Chemicals\",\"issellersku\":\"0\",\"mapViolation\":\"0\",\"numberOfReviews\":\"0\",\"pid\":\"24442430\",\"pr_count\":\"6\",\"price\":\"12.59\",\"price_in_cart_flag\":\"0\",\"producturl\":\"http://www.staples.com/product_24442430\",\"sale\":\"12.59\",\"sellingpacksize\":\"Each\",\"shippingCost\":\"0\",\"shipping_cost\":\"Free if order is over $49.99\",\"staples_brand\":\"Comet\",\"taxonomy_text\":\"cleaning supplies>cleaning chemicals & wipes>cleaning chemicals & wipes>cleaning chemicals\"}", + "adid": "1", + "shortDescription": "Disinfecting/sanitizing bathroom cleaner for infection prevention and control", + "MatchType": "sku", + "ParentSKU": "24442430", +} + +const ( + MAX_COUNT = 9 + IMP_URL = "//b.va.us.criteo.com/rm?rm_e=V371SUC6Q0sB1j7Qlln9yr4HmnAeOoZf79-n9l7MezTyqJTRG9e0i5nSRGOMvkJVIy916h08clCmFSUIeYuJI1pNc1Skg2jiJnnLr9_IDUiIGwaE1-r6TQZtzwz2CuFXuc5ZNn86gzWlM3ciRwey0bumgKtGwi3zX1NWgsg0HmKEVLkwjHslFwdVSg6phjt29NuQ-fxPnp4SOschyHefI0HNM0AvREMQkLcYMGKkADJ07E0FomZhagxPFhBE4Qrk0m3AB46CHNsu3E7IVJ2RwAatybYFDQP2rgTr--mFQ0jlZUggctRZvMAQkd1e17YPW-x8mQd_NqPVSYHvo6peUfSUhgeRyvBf2NtZWMG4NBaaXCHZZuNFDT2_DYCcQfeFanp1wOYNii3_-WLBdiEhWgvIVH5psM-xgV8hEnZSlz-__UtOMVTaXdZiRDEouaHnEaaZTV7Vi8clDSz04v2nbTSL2ta1sl7-EzDEtQAfftjVd42k3OzofBbad57JXSZ9hbJaomW5r8yibxkbdjEv1u-Nldm-HkIIc0xsWMLCUog&ev=4" + CLICK_URL = "//b.va.us.criteo.com/rm?rm_e=4Cwm-UKrl2ok74hbwFbixP5V17YxlhWthucJj_ny-DoqhUxhNLckmIt2T8Xpc3U0pq3aIZBPY4tp8MQQ53PKfRascJnYUUkz3RI-z5JZB5nlNOq9_lPZxCjtd0YDHZvWw2Bu05u1SvnOiHNpBQpqBw68R4Fsbsed78vQ4DnOtnntXzS9xCqVHOKS6sIQIInjAiFvcEIa4MU7PrOTa1KoHGM-qmUQlaCZxKzADq0MTA3Nr5xaFZFBiZhyCjWxss27YVCbNjXHS8N4xSiCaCkrN9tZcRVtKzc-CdmKcUss-ZfHog_Q_1X76qc59nklcPznF6ax-EDx8xTQQ_cZnja_c_QnSrH732fuHG9vbuEhHl-tbRo3XCEI39PRupAcMB75k_a--VnxgOjgdJryWp6Sl2Qme23qHv2YdZkBBcxt4Z78KnjFeBGwPPA98wNSig5g15QN_Dae3K-XEmSrAFDjWnZmXb4PuVVeP72VOSaG9_YLwx_L4vsXzeDdnjM6w3WRP6yIqc_ZVBnloH8I1T31KayJfxKkh2Fs4a5-1NDgcgZoxmqfbtpd5LIbgPBexD8Sw7H9efxx8ql_EpgmnxMBTA&ev=4" +) +func (a *CriteoRetailAdapter) GetMockResponse(internalRequest *openrtb2.BidRequest) *adapters.BidderResponse { + requestCount := GetRequestSlotCount(internalRequest) + impiD := internalRequest.Imp[0].ID + + responseF := GetMockBids(requestCount, impiD) + return responseF +} + +func GetRequestSlotCount(internalRequest *openrtb2.BidRequest) int { + impArray := internalRequest.Imp + reqCount := 0 + for _, eachImp := range impArray { + var commerceExt openrtb_ext.ExtImpCommerce + json.Unmarshal(eachImp.Ext, &commerceExt) + reqCount += commerceExt.ComParams.SlotsRequested + } + return reqCount +} + +func GetRandomProductID() string { + min := 100000 + max := 600000 + randomN := rand.Intn(max-min+1) + min + t := strconv.Itoa(randomN) + return t +} + +func GetRandomBidPrice() float64 { + min := 1.0 + max := 15.0 + untruncated := min + rand.Float64()*(max-min) + truncated := float64(int(untruncated*100)) / 100 + return truncated +} + +func GetRandomClickPrice(max float64) float64 { + min := 1.0 + untruncated := min + rand.Float64()*(max-min) + truncated := float64(int(untruncated*100)) / 100 + return truncated +} + +func GetMockBids(requestCount int, ImpID string) *adapters.BidderResponse { + var typedArray []*adapters.TypedBid + + if requestCount > MAX_COUNT { + requestCount = MAX_COUNT + } + + for i := 1; i <= requestCount; i++ { + productid := GetRandomProductID() + bidPrice := GetRandomBidPrice() + clickPrice := GetRandomClickPrice(bidPrice) + bidID := adapters.GenerateUniqueBidIDComm() + impID := ImpID + "_" + strconv.Itoa(i) + + bidExt := &openrtb_ext.ExtBidCommerce{ + ProductId: productid, + ClickPrice: clickPrice, + ClickUrl: CLICK_URL, + ProductDetails: mockProductDetails, + } + + bid := &openrtb2.Bid{ + ID: bidID, + ImpID: impID, + Price: bidPrice, + IURL: IMP_URL, + } + + adapters.AddDefaultFieldsComm(bid) + + bidExtJSON, err1 := json.Marshal(bidExt) + if nil == err1 { + bid.Ext = json.RawMessage(bidExtJSON) + } + + typedbid := &adapters.TypedBid{ + Bid: bid, + Seat: openrtb_ext.BidderName(SEAT_CRITEORETAIL), + } + typedArray = append(typedArray, typedbid) + } + + responseF := &adapters.BidderResponse{ + Bids: typedArray, + } + return responseF +} + diff --git a/adapters/criteoretail/criteoretail_request.go b/adapters/criteoretail/criteoretail_request.go new file mode 100644 index 00000000000..53568278d65 --- /dev/null +++ b/adapters/criteoretail/criteoretail_request.go @@ -0,0 +1,84 @@ +package criteoretail + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func getProductList(commerceExt *openrtb_ext.ExtImpCommerce) string { + // Check if there are preferred products + if commerceExt != nil && commerceExt.ComParams != nil && len(commerceExt.ComParams.Preferred) > 0 { + // Initialize a slice to hold the product IDs + productIDs := make([]string, 0) + + // Iterate through the preferred products and collect their IDs + for _, preferredProduct := range commerceExt.ComParams.Preferred { + productIDs = append(productIDs, preferredProduct.ProductID) + } + + // Join the product IDs with a pipe separator + return strings.Join(productIDs, "|") + } + + // Return an empty string if no preferred products are found + return "" +} + +func (a *CriteoRetailAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + commerceExt, siteExt, bidderParams, errors := adapters.ValidateCommRequest(request) + if len(errors) > 0 { + return nil, errors + } + + var configValueMap = make(map[string]string) + var configTypeMap = make(map[string]int) + for _,obj := range commerceExt.Bidder.CustomConfig { + configValueMap[obj.Key] = obj.Value + configTypeMap[obj.Key] = obj.Type + } + + _, err := url.Parse(a.endpoint) + if err != nil { + return nil, []error{fmt.Errorf("failed to parse yieldlab endpoint: %v", err)} + } + + var criteoPartnerID string + val, ok := configValueMap[AUCTIONDETAILS_PREFIX + AD_ACCOUNT_ID] + if ok { + criteoPartnerID = val + } + + values := url.Values{} + + // Add the fields to the query string + values.Add("criteo-partner-id", criteoPartnerID) + values.Add("retailer-visitor-id", request.User.ID) + values.Add("page-id", siteExt.Page) + + productList := getProductList(commerceExt) + if productList != ""{ + values.Add("item-whitelist",productList) + } + // Add other fields as needed + + for key, value := range bidderParams { + values.Add(key, fmt.Sprintf("%v", value)) + } + + criteoQueryString := values.Encode() + requestURL := a.endpoint + "?" + criteoQueryString + + return []*adapters.RequestData{{ + Method: "GET", + Uri: requestURL, + Headers: http.Header{}, + }}, nil +} + + diff --git a/adapters/criteoretail/criteoretail_response.go b/adapters/criteoretail/criteoretail_response.go new file mode 100644 index 00000000000..a0500d9140e --- /dev/null +++ b/adapters/criteoretail/criteoretail_response.go @@ -0,0 +1,171 @@ +package criteoretail + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type Placement struct { + Format string `json:"format"` + Products []map[string]interface{} `json:"products"` + OnLoadBeacon string `json:"OnLoadBeacon,omitempty"` + OnViewBeacon string `json:"OnViewBeacon,omitempty"` +} + +type CriteoResponse struct { + Status string `json:"status"` + OnAvailabilityUpdate interface{} `json:"OnAvailabilityUpdate"` + Placements []map[string][]Placement `json:"placements"` +} + +func (a *CriteoRetailAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + + var errors []error + + commerceExt, err := adapters.GetImpressionExtComm(&(internalRequest.Imp[0])) + if err != nil { + errors := append(errors, err) + return nil, errors + } + + if commerceExt.ComParams.TestRequest { + + dummyResponse := a.GetMockResponse(internalRequest) + return dummyResponse, nil + } + + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + + if response.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d", response.StatusCode), + }} + } + + criteoResponse, err := newcriteoretailResponseFromBytes(response.Body) + if err != nil { + return nil, []error{err} + } + + if criteoResponse.Status != RESPONSE_OK { + return nil, []error{&errortypes.BidderFailedSchemaValidation{ + Message: "Error Occured at Criteo for the given request ", + }} + } + + if criteoResponse.Placements == nil || len(criteoResponse.Placements) <= 0 { + return nil, []error{&errortypes.NoBidPrice{ + Message: "No Bid For the given Request", + }} + } + + impID := internalRequest.Imp[0].ID + bidderResponse := a.getBidderResponse(internalRequest, &criteoResponse, impID) + return bidderResponse, nil +} + +func (a *CriteoRetailAdapter) getBidderResponse(request *openrtb2.BidRequest, criteoResponse *CriteoResponse, requestImpID string) *adapters.BidderResponse { + + noOfBids := countSponsoredProducts(criteoResponse) + bidResponse := adapters.NewBidderResponseWithBidsCapacity(noOfBids) + index := 1 + for _, placementMap := range criteoResponse.Placements { + for _, placements := range placementMap { + for _, placement := range placements { + if placement.Format == FORMAT_SPONSORED { + for _, productMap := range placement.Products { + bidID := adapters.GenerateUniqueBidIDComm() + impID := requestImpID + "_" + strconv.Itoa(index) + bidPrice, _ := strconv.ParseFloat(strings.TrimSpace(productMap[BID_PRICE].(string)), 64) + clickPrice, _ := strconv.ParseFloat(strings.TrimSpace(productMap[CLICK_PRICE].(string)), 64) + productID := productMap[PRODUCT_ID].(string) + + impressionURL := IMP_KEY + adapters.EncodeURL(productMap[VIEW_BEACON].(string)) + clickURL := CLICK_KEY + adapters.EncodeURL(productMap[CLICK_BEACON].(string)) + index++ + + // Add ProductDetails to bidExtension + productDetails := make(map[string]interface{}) + for key, value := range productMap { + productDetails[key] = value + } + + delete(productDetails, PRODUCT_ID) + delete(productDetails, BID_PRICE) + delete(productDetails, CLICK_PRICE) + delete(productDetails, VIEW_BEACON) + delete(productDetails, CLICK_BEACON) + + bidExt := &openrtb_ext.ExtBidCommerce{ + ProductId: productID, + ClickUrl: clickURL, + ClickPrice: clickPrice, + ProductDetails: productDetails, + } + + bid := &openrtb2.Bid{ + ID: bidID, + ImpID: impID, + Price: bidPrice, + IURL: impressionURL, + } + + adapters.AddDefaultFieldsComm(bid) + bidExtJSON, err1 := json.Marshal(bidExt) + if nil == err1 { + bid.Ext = json.RawMessage(bidExtJSON) + } + + seat := openrtb_ext.BidderName(SEAT_CRITEORETAIL) + + typedbid := &adapters.TypedBid{ + Bid: bid, + Seat: seat, + } + bidResponse.Bids = append(bidResponse.Bids, typedbid) + } + } + } + } + } + return bidResponse +} + +func newcriteoretailResponseFromBytes(bytes []byte) (CriteoResponse, error) { + var err error + var bidResponse CriteoResponse + + if err = json.Unmarshal(bytes, &bidResponse); err != nil { + return bidResponse, err + } + + return bidResponse, nil +} + +func countSponsoredProducts(adResponse *CriteoResponse) int { + count := 0 + + // Iterate through placements + for _, placementMap := range adResponse.Placements { + for _, placements := range placementMap { + for _, placement := range placements { + if placement.Format == FORMAT_SPONSORED { + count += len(placement.Products) + } + } + } + } + + return count +} + diff --git a/adapters/koddi/koddi.go b/adapters/koddi/koddi.go new file mode 100644 index 00000000000..0a37852f274 --- /dev/null +++ b/adapters/koddi/koddi.go @@ -0,0 +1,281 @@ +package koddi + +import ( + "encoding/json" + "fmt" + "math/rand" + "net/http" + "strconv" + "text/template" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/macros" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type KoddiAdapter struct { + endpoint *template.Template + impurl *template.Template + clickurl *template.Template + conversionurl *template.Template +} + +const MAX_COUNT = 10 +const COMMERCE_DEFAULT_HOSTNAME = "pubMatic" + +func GetRequestSlotCount(internalRequest *openrtb2.BidRequest)int { + impArray := internalRequest.Imp + reqCount := 0 + for _, eachImp := range impArray { + var commerceExt openrtb_ext.ExtImpCommerce + json.Unmarshal(eachImp.Ext, &commerceExt) + reqCount += commerceExt.ComParams.SlotsRequested + } + return reqCount +} + +func GetRandomProductID() string { + randomN :=rand.Intn(200000) + t := strconv.Itoa(randomN) + return t +} + +func GetRandomCampaignID() string { + randomN :=rand.Intn(9000000) + t := strconv.Itoa(randomN) + return t +} + + +func GetRandomBidPrice() float64 { + min := 0.1 + max := 1.0 + untruncated := min + rand.Float64() * (max - min) + truncated := float64(int(untruncated * 100)) / 100 + return truncated +} + +func GetRandomClickPrice() float64 { + min := 1.0 + max := 5.0 + untruncated := min + rand.Float64() * (max - min) + truncated := float64(int(untruncated * 100)) / 100 + return truncated +} + +func GetHostName(internalRequest *openrtb2.BidRequest) string { + var extension map[string]json.RawMessage + var preBidExt openrtb_ext.ExtRequestPrebid + var commerceExt openrtb_ext.ExtImpCommerce + + json.Unmarshal(internalRequest.Ext, &extension) + json.Unmarshal(extension["prebid"], &preBidExt) + json.Unmarshal(internalRequest.Imp[0].Ext, &commerceExt) + return commerceExt.Bidder.BidderCode +} + + +func GetDummyBids(impUrl , clickUrl , conversionUrl, seatName string, requestCount int, ImpID string) (*adapters.BidderResponse) { + var typedArray []*adapters.TypedBid + + if requestCount > MAX_COUNT { + requestCount = MAX_COUNT + } + for i := 1; i <= requestCount; i++ { + productid := GetRandomProductID() + campaignID := GetRandomCampaignID() + bidPrice := GetRandomBidPrice() + clickPrice := GetRandomClickPrice() + bidID := adapters.GenerateUniqueBidIDComm() + impID := ImpID + "_" + strconv.Itoa(i) + + bidExt := &openrtb_ext.ExtBidCommerce{ + ProductId: productid, + ClickPrice: clickPrice, + } + + bid := &openrtb2.Bid { + ID: bidID, + ImpID: impID, + Price: bidPrice, + CID: campaignID, + } + + adapters.AddDefaultFieldsComm(bid) + + bidExtJSON, err1 := json.Marshal(bidExt) + if nil == err1 { + bid.Ext = json.RawMessage(bidExtJSON) + } + + typedbid := &adapters.TypedBid { + Bid: bid, + Seat: openrtb_ext.BidderName(seatName), + } + typedArray = append(typedArray, typedbid) + } + + responseF := &adapters.BidderResponse{ + Bids: typedArray, + } + return responseF +} + +func GetDummyBids_NoBid(impUrl , clickUrl , conversionUrl, seatName string, requestCount int) (*adapters.BidderResponse) { + var typedArray []*adapters.TypedBid + + if requestCount > MAX_COUNT { + requestCount = MAX_COUNT + } + for i := 0; i < requestCount; i++ { + productid := GetRandomProductID() + campaignID := GetRandomCampaignID() + bidPrice := GetRandomBidPrice() + clickPrice := GetRandomClickPrice() + bidID := adapters.GenerateUniqueBidIDComm() + newIurl := impUrl + "_ImpID=" +bidID + newCurl := clickUrl + "_ImpID=" +bidID + newPurl := conversionUrl + "_ImpID=" +bidID + + bidExt := &openrtb_ext.ExtBidCommerce{ + ProductId: productid, + ClickUrl: newCurl, + ConversionUrl: newPurl, + ClickPrice: clickPrice, + + } + + bid := &openrtb2.Bid { + ID: bidID, + ImpID: bidID, + Price: bidPrice, + CID: campaignID, + IURL: newIurl, + Tactic: "Dummy", + } + + adapters.AddDefaultFieldsComm(bid) + + bidExtJSON, err1 := json.Marshal(bidExt) + if nil == err1 { + bid.Ext = json.RawMessage(bidExtJSON) + } + + typedbid := &adapters.TypedBid { + Bid: bid, + Seat: openrtb_ext.BidderName(seatName), + } + typedArray = append(typedArray, typedbid) + } + + responseF := &adapters.BidderResponse{ + Bids: typedArray, + } + return responseF +} + + +func (a *KoddiAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + host := "localhost" + var extension map[string]json.RawMessage + var preBidExt openrtb_ext.ExtRequestPrebid + var commerceExt openrtb_ext.ExtImpCommerce + json.Unmarshal(request.Ext, &extension) + json.Unmarshal(extension["prebid"], &preBidExt) + json.Unmarshal(request.Imp[0].Ext, &commerceExt) + endPoint,_ := a.buildEndpointURL(host) + errs := make([]error, 0, len(request.Imp)) + + reqJSON, err := json.Marshal(request) + if err != nil { + errs = append(errs, err) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json") + + return []*adapters.RequestData{{ + Method: "POST", + Uri: endPoint, + Body: reqJSON, + Headers: headers, + }}, errs + +} +func (a *KoddiAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errors []error + + hostName := GetHostName(internalRequest) + if len(hostName) == 0 { + hostName = COMMERCE_DEFAULT_HOSTNAME + } + iurl, _ := a.buildImpressionURL(hostName) + curl, _ := a.buildClickURL(hostName) + purl, _ := a.buildConversionURL(hostName) + requestCount := GetRequestSlotCount(internalRequest) + impiD := internalRequest.Imp[0].ID + + responseF := GetDummyBids(iurl, curl, purl, "koddi", requestCount, impiD) + //responseF := commerce.GetDummyBids_NoBid(iurl, curl, purl, "koddi", 1) + //err := fmt.Errorf("No Bids available for the given request from Koddi") + //errors = append(errors,err ) + return responseF, errors + +} + +// Builder builds a new instance of the Koddi adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { + + endpointtemplate, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + impurltemplate, err := template.New("impurlTemplate").Parse(config.ComParams.ImpTracker) + if err != nil { + return nil, fmt.Errorf("unable to parse imp url template: %v", err) + } + + clickurltemplate, err := template.New("clickurlTemplate").Parse(config.ComParams.ClickTracker) + if err != nil { + return nil, fmt.Errorf("unable to parse click url template: %v", err) + } + + conversionurltemplate, err := template.New("endpointTemplate").Parse(config.ComParams.ConversionTracker) + if err != nil { + return nil, fmt.Errorf("unable to parse conversion url template: %v", err) + } + + bidder := &KoddiAdapter{ + endpoint: endpointtemplate, + impurl: impurltemplate, + clickurl: clickurltemplate, + conversionurl: conversionurltemplate, + } + + return bidder, nil +} + +func (a *KoddiAdapter) buildEndpointURL(hostName string) (string, error) { + endpointParams := macros.EndpointTemplateParams{ Host: hostName} + return macros.ResolveMacros(a.endpoint, endpointParams) +} + +func (a *KoddiAdapter) buildImpressionURL(hostName string) (string, error) { + endpointParams := macros.EndpointTemplateParams{ Host: hostName} + return macros.ResolveMacros(a.impurl, endpointParams) +} + +func (a *KoddiAdapter) buildClickURL(hostName string) (string, error) { + endpointParams := macros.EndpointTemplateParams{ Host: hostName} + return macros.ResolveMacros(a.clickurl, endpointParams) +} + +func (a *KoddiAdapter) buildConversionURL(hostName string) (string, error) { + endpointParams := macros.EndpointTemplateParams{ Host: hostName} + return macros.ResolveMacros(a.conversionurl, endpointParams) +} + diff --git a/adapters/openrtb_commerce.go b/adapters/openrtb_commerce.go new file mode 100644 index 00000000000..b10203b1bcd --- /dev/null +++ b/adapters/openrtb_commerce.go @@ -0,0 +1,126 @@ +package adapters + +import ( + "encoding/base64" + "encoding/json" + + "github.com/google/uuid" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func EncodeURL(url string) string { + str := base64.StdEncoding.EncodeToString([]byte(url)) + return str +} + +func GetImpressionExtComm(imp *openrtb2.Imp) (*openrtb_ext.ExtImpCommerce, error) { + var commerceExt openrtb_ext.ExtImpCommerce + if err := json.Unmarshal(imp.Ext, &commerceExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Impression extension not provided or can't be unmarshalled", + } + } + + return &commerceExt, nil + +} + +func GetSiteExtComm(request *openrtb2.BidRequest) (*openrtb_ext.ExtSiteCommerce, error) { + var siteExt openrtb_ext.ExtSiteCommerce + + if request.Site.Ext != nil { + if err := json.Unmarshal(request.Site.Ext, &siteExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Impression extension not provided or can't be unmarshalled", + } + } + } + + return &siteExt, nil + +} + +func GetRequestExtComm(request *openrtb2.BidRequest) (*openrtb_ext.ExtOWRequest, error) { + var requestExt openrtb_ext.ExtOWRequest + + if request.Ext != nil { + if err := json.Unmarshal(request.Ext, &requestExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Impression extension not provided or can't be unmarshalled", + } + } + } + + return &requestExt, nil +} + +func GetBidderParamsComm(prebidExt *openrtb_ext.ExtOWRequest) (map[string]interface{}, error) { + var bidderParams map[string]interface{} + + if prebidExt.Prebid.BidderParams != nil { + if err := json.Unmarshal(prebidExt.Prebid.BidderParams, &bidderParams); err != nil { + return nil, &errortypes.BadInput{ + Message: "Impression extension not provided or can't be unmarshalled", + } + } + } + + return bidderParams, nil +} + +func ValidateCommRequest(request *openrtb2.BidRequest) (*openrtb_ext.ExtImpCommerce, + *openrtb_ext.ExtSiteCommerce, map[string]interface{}, []error) { + var commerceExt *openrtb_ext.ExtImpCommerce + var siteExt *openrtb_ext.ExtSiteCommerce + var requestExt *openrtb_ext.ExtOWRequest + var bidderParams map[string]interface{} + + var err error + var errors []error + + if len(request.Imp) > 0 { + commerceExt, err = GetImpressionExtComm(&(request.Imp[0])) + if err != nil { + errors = append(errors, err) + } + } else { + errors = append(errors, &errortypes.BadInput{ + Message: "Missing Imp Object", + }) + } + + siteExt, err = GetSiteExtComm(request) + if err != nil { + errors = append(errors, err) + } + + requestExt, err = GetRequestExtComm(request) + if err != nil { + errors = append(errors, err) + } + + bidderParams, err = GetBidderParamsComm(requestExt) + if err != nil { + errors = append(errors, err) + } + + if len(errors) > 0 { + return nil, nil, nil, errors + } + + return commerceExt, siteExt, bidderParams, nil +} + +func AddDefaultFieldsComm(bid *openrtb2.Bid) { + if bid != nil { + bid.CrID = "DefaultCRID" + } +} + +func GenerateUniqueBidIDComm() string { + id := uuid.New() + return id.String() +} + diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go index 50fed3505c2..e7d920448eb 100644 --- a/adapters/pubmatic/pubmatic.go +++ b/adapters/pubmatic/pubmatic.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "strconv" "strings" @@ -19,6 +20,18 @@ import ( const MAX_IMPRESSIONS_PUBMATIC = 30 +const ( + PUBMATIC = "[PUBMATIC]" + buyId = "buyid" + buyIdTargetingKey = "hb_buyid_" + skAdnetworkKey = "skadn" + rewardKey = "reward" + dctrKeywordName = "dctr" + urlEncodedEqualChar = "%3D" + AdServerKey = "adserver" + PBAdslotKey = "pbadslot" +) + type PubmaticAdapter struct { URI string } @@ -32,6 +45,8 @@ type pubmaticBidExt struct { type pubmaticWrapperExt struct { ProfileID int `json:"profile,omitempty"` VersionID int `json:"version,omitempty"` + + WrapperImpID string `json:"wiid,omitempty"` } type pubmaticBidExtVideo struct { @@ -40,12 +55,8 @@ type pubmaticBidExtVideo struct { type ExtImpBidderPubmatic struct { adapters.ExtImpBidder - Data *ExtData `json:"data,omitempty"` -} - -type ExtData struct { - AdServer *ExtAdServer `json:"adserver"` - PBAdSlot string `json:"pbadslot"` + Data json.RawMessage `json:"data,omitempty"` + SKAdnetwork json.RawMessage `json:"skadn,omitempty"` } type ExtAdServer struct { @@ -54,11 +65,11 @@ type ExtAdServer struct { } const ( - dctrKeyName = "key_val" - pmZoneIDKeyName = "pmZoneId" - pmZoneIDKeyNameOld = "pmZoneID" - ImpExtAdUnitKey = "dfp_ad_unit_code" - AdServerGAM = "gam" + dctrKeyName = "key_val" + pmZoneIDKeyName = "pmZoneId" + pmZoneIDRequestParamName = "pmzoneid" + ImpExtAdUnitKey = "dfp_ad_unit_code" + AdServerGAM = "gam" ) func (a *PubmaticAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { @@ -69,7 +80,7 @@ func (a *PubmaticAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ad extractWrapperExtFromImp := true extractPubIDFromImp := true - wrapperExt, acat, err := extractPubmaticExtFromRequest(request) + wrapperExt, acat, cookies, err := extractPubmaticExtFromRequest(request) if err != nil { return nil, []error{err} } @@ -100,6 +111,10 @@ func (a *PubmaticAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ad wrapperExt.VersionID = wrapperExtFromImp.VersionID } + if wrapperExt.WrapperImpID == "" { + wrapperExt.WrapperImpID = wrapperExtFromImp.WrapperImpID + } + if wrapperExt != nil && wrapperExt.ProfileID != 0 && wrapperExt.VersionID != 0 { extractWrapperExtFromImp = false } @@ -154,6 +169,48 @@ func (a *PubmaticAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ad request.App = &appCopy } + // move user.ext.eids to user.eids + if request.User != nil && request.User.Ext != nil { + var userExt *openrtb_ext.ExtUser + if err = json.Unmarshal(request.User.Ext, &userExt); err == nil { + if userExt != nil && userExt.Eids != nil { + var eidArr []openrtb2.EID + for _, eid := range userExt.Eids { + newEid := &openrtb2.EID{ + ID: eid.ID, + Source: eid.Source, + Ext: eid.Ext, + } + var uidArr []openrtb2.UID + for _, uid := range eid.UIDs { + newUID := &openrtb2.UID{ + ID: uid.ID, + AType: uid.AType, + Ext: uid.Ext, + } + uidArr = append(uidArr, *newUID) + } + newEid.UIDs = uidArr + eidArr = append(eidArr, *newEid) + } + + user := *request.User + user.EIDs = eidArr + userExt.Eids = nil + updatedUserExt, err1 := json.Marshal(userExt) + if err1 == nil { + user.Ext = updatedUserExt + } + request.User = &user + } + } + } + + //adding hack to support DNT, since hbopenbid does not support lmt + if request.Device != nil && request.Device.Lmt != nil && *request.Device.Lmt != 0 { + request.Device.DNT = request.Device.Lmt + } + reqJSON, err := json.Marshal(request) if err != nil { errs = append(errs, err) @@ -163,6 +220,9 @@ func (a *PubmaticAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ad headers := http.Header{} headers.Add("Content-Type", "application/json;charset=utf-8") headers.Add("Accept", "application/json") + for _, line := range cookies { + headers.Add("Cookie", line) + } return []*adapters.RequestData{{ Method: "POST", Uri: a.URI, @@ -191,26 +251,26 @@ func validateAdSlot(adslot string, imp *openrtb2.Imp) error { adSize := strings.Split(strings.ToLower(adSlot[1]), "x") if len(adSize) != 2 { - return fmt.Errorf("Invalid size provided in adSlot %v", adSlotStr) + return errors.New(fmt.Sprintf("Invalid size provided in adSlot %v", adSlotStr)) } width, err := strconv.Atoi(strings.TrimSpace(adSize[0])) if err != nil { - return fmt.Errorf("Invalid width provided in adSlot %v", adSlotStr) + return errors.New(fmt.Sprintf("Invalid width provided in adSlot %v", adSlotStr)) } heightStr := strings.Split(adSize[1], ":") height, err := strconv.Atoi(strings.TrimSpace(heightStr[0])) if err != nil { - return fmt.Errorf("Invalid height provided in adSlot %v", adSlotStr) + return errors.New(fmt.Sprintf("Invalid height provided in adSlot %v", adSlotStr)) } //In case of video, size could be derived from the player size - if imp.Banner != nil { + if imp.Banner != nil && width != 0 && height != 0 { imp.Banner = assignBannerWidthAndHeight(imp.Banner, int64(width), int64(height)) } } else { - return fmt.Errorf("Invalid adSlot %v", adSlotStr) + return errors.New(fmt.Sprintf("Invalid adSlot %v", adSlotStr)) } return nil @@ -221,6 +281,10 @@ func assignBannerSize(banner *openrtb2.Banner) (*openrtb2.Banner, error) { return banner, nil } + if len(banner.Format) == 0 { + return nil, errors.New(fmt.Sprintf("No sizes provided for Banner %v", banner.Format)) + } + return assignBannerWidthAndHeight(banner, banner.Format[0].W, banner.Format[0].H), nil } @@ -299,14 +363,20 @@ func parseImpressionObject(imp *openrtb2.Imp, extractWrapperExtFromImp, extractP extMap[pmZoneIDKeyName] = pubmaticExt.PmZoneID } - if bidderExt.Data != nil { - if bidderExt.Data.AdServer != nil && bidderExt.Data.AdServer.Name == AdServerGAM && bidderExt.Data.AdServer.AdSlot != "" { - extMap[ImpExtAdUnitKey] = bidderExt.Data.AdServer.AdSlot - } else if bidderExt.Data.PBAdSlot != "" { - extMap[ImpExtAdUnitKey] = bidderExt.Data.PBAdSlot + if bidderExt.SKAdnetwork != nil { + extMap[skAdnetworkKey] = bidderExt.SKAdnetwork + } + + if bidderExt.Prebid != nil { + if bidderExt.Prebid.IsRewardedInventory == 1 { + extMap[rewardKey] = bidderExt.Prebid.IsRewardedInventory } } + if len(bidderExt.Data) > 0 { + populateFirstPartyDataImpAttributes(bidderExt.Data, extMap) + } + imp.Ext = nil if len(extMap) > 0 { ext, err := json.Marshal(extMap) @@ -319,12 +389,12 @@ func parseImpressionObject(imp *openrtb2.Imp, extractWrapperExtFromImp, extractP } // extractPubmaticExtFromRequest parse the req.ext to fetch wrapper and acat params -func extractPubmaticExtFromRequest(request *openrtb2.BidRequest) (*pubmaticWrapperExt, []string, error) { - var acat []string +func extractPubmaticExtFromRequest(request *openrtb2.BidRequest) (*pubmaticWrapperExt, []string, []string, error) { + var acat, cookies []string var wrpExt *pubmaticWrapperExt reqExtBidderParams, err := adapters.ExtractReqExtBidderParamsMap(request) if err != nil { - return nil, acat, err + return nil, acat, cookies, err } //get request ext bidder params @@ -332,7 +402,7 @@ func extractPubmaticExtFromRequest(request *openrtb2.BidRequest) (*pubmaticWrapp wrpExt = &pubmaticWrapperExt{} err = json.Unmarshal(wrapperObj, wrpExt) if err != nil { - return nil, acat, err + return nil, acat, cookies, err } } @@ -343,7 +413,23 @@ func extractPubmaticExtFromRequest(request *openrtb2.BidRequest) (*pubmaticWrapp } } - return wrpExt, acat, err + if err != nil { + return wrpExt, acat, cookies, err + } + + if wiid, ok := reqExtBidderParams["wiid"]; ok { + if wrpExt == nil { + wrpExt = &pubmaticWrapperExt{} + } + wrpExt.WrapperImpID, _ = strconv.Unquote(string(wiid)) + } + + //get request ext bidder params + if wrapperObj, present := reqExtBidderParams["Cookie"]; present && len(wrapperObj) != 0 { + err = json.Unmarshal(wrapperObj, &cookies) + } + + return wrpExt, acat, cookies, err } func addKeywordsToExt(keywords []*openrtb_ext.ExtImpPubmaticKeyVal, extMap map[string]interface{}) { @@ -353,10 +439,20 @@ func addKeywordsToExt(keywords []*openrtb_ext.ExtImpPubmaticKeyVal, extMap map[s continue } else { key := keyVal.Key - if keyVal.Key == pmZoneIDKeyNameOld { + val := strings.Join(keyVal.Values[:], ",") + if strings.EqualFold(key, pmZoneIDRequestParamName) { key = pmZoneIDKeyName + } else if key == dctrKeywordName { + key = dctrKeyName + // URL-decode dctr value if it is url-encoded + if strings.Contains(val, urlEncodedEqualChar) { + urlDecodedVal, err := url.QueryUnescape(val) + if err == nil { + val = urlDecodedVal + } + } } - extMap[key] = strings.Join(keyVal.Values[:], ",") + extMap[key] = val } } } @@ -385,8 +481,12 @@ func (a *PubmaticAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa var errs []error for _, sb := range bidResp.SeatBid { + targets := getTargetingKeys(sb.Ext, string(externalRequest.BidderName)) for i := 0; i < len(sb.Bid); i++ { bid := sb.Bid[i] + // Copy SeatBid Ext to Bid.Ext + bid.Ext = copySBExtToBidExt(sb.Ext, bid.Ext) + impVideo := &openrtb_ext.ExtBidPrebidVideo{} if len(bid.Cat) > 1 { @@ -415,14 +515,18 @@ func (a *PubmaticAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa } bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ - Bid: &bid, - BidType: bidType, - BidVideo: impVideo, - Seat: openrtb_ext.BidderName(seat), + Bid: &bid, + BidType: bidType, + BidVideo: impVideo, + Seat: openrtb_ext.BidderName(seat), + BidTargets: targets, }) } } + if bidResp.Cur != "" { + bidResponse.Currency = bidResp.Cur + } return bidResponse, errs } @@ -447,6 +551,118 @@ func getNativeAdm(adm string) (string, error) { return adm, nil } +//getMapFromJSON converts JSON to map +func getMapFromJSON(source json.RawMessage) map[string]interface{} { + if source != nil { + dataMap := make(map[string]interface{}) + err := json.Unmarshal(source, &dataMap) + if err == nil { + return dataMap + } + } + return nil +} + +//populateFirstPartyDataImpAttributes will parse imp.ext.data and populate imp extMap +func populateFirstPartyDataImpAttributes(data json.RawMessage, extMap map[string]interface{}) { + + dataMap := getMapFromJSON(data) + + if dataMap == nil { + return + } + + populateAdUnitKey(data, dataMap, extMap) + populateDctrKey(dataMap, extMap) +} + +//populateAdUnitKey parses data object to read and populate DFP adunit key +func populateAdUnitKey(data json.RawMessage, dataMap, extMap map[string]interface{}) { + + if name, err := jsonparser.GetString(data, "adserver", "name"); err == nil && name == AdServerGAM { + if adslot, err := jsonparser.GetString(data, "adserver", "adslot"); err == nil && adslot != "" { + extMap[ImpExtAdUnitKey] = adslot + } + } + + //imp.ext.dfp_ad_unit_code is not set, then check pbadslot in imp.ext.data + if extMap[ImpExtAdUnitKey] == nil && dataMap[PBAdslotKey] != nil { + extMap[ImpExtAdUnitKey] = dataMap[PBAdslotKey].(string) + } +} + +//populateDctrKey reads key-val pairs from imp.ext.data and add it in imp.ext.key_val +func populateDctrKey(dataMap, extMap map[string]interface{}) { + var dctr strings.Builder + + //append dctr key if already present in extMap + if extMap[dctrKeyName] != nil { + dctr.WriteString(extMap[dctrKeyName].(string)) + } + + for key, val := range dataMap { + + //ignore 'pbaslot' and 'adserver' key as they are not targeting keys + if key == PBAdslotKey || key == AdServerKey { + continue + } + + //separate key-val pairs in dctr string by pipe(|) + if dctr.String() != "" { + dctr.WriteString("|") + } + + //trimming spaces from key + key = strings.TrimSpace(key) + + switch typedValue := val.(type) { + case string: + fmt.Fprintf(&dctr, "%s=%s", key, strings.TrimSpace(typedValue)) + + case float64, bool: + fmt.Fprintf(&dctr, "%s=%v", key, typedValue) + + case []interface{}: + if isStringArray(typedValue) { + if valStrArr := getStringArray(typedValue); valStrArr != nil && len(valStrArr) > 0 { + valStr := strings.Join(valStrArr[:], ",") + fmt.Fprintf(&dctr, "%s=%s", key, valStr) + } + } + } + } + + if dctrStr := dctr.String(); dctrStr != "" { + extMap[dctrKeyName] = strings.TrimSuffix(dctrStr, "|") + } +} + +//isStringArray check if []interface is a valid string array +func isStringArray(array []interface{}) bool { + for _, val := range array { + if _, ok := val.(string); !ok { + return false + } + } + return true +} + +//getStringArray converts interface of type string array to string array +func getStringArray(val interface{}) []string { + aInterface, ok := val.([]interface{}) + if !ok { + return nil + } + aString := make([]string, len(aInterface)) + for i, v := range aInterface { + if str, ok := v.(string); ok { + aString[i] = strings.TrimSpace(str) + } + } + + return aString +} + // getBidType returns the bid type specified in the response bid.ext func getBidType(bidExt *pubmaticBidExt) openrtb_ext.BidType { // setting "banner" as the default bid type diff --git a/adapters/pubmatic/pubmatic_ow.go b/adapters/pubmatic/pubmatic_ow.go new file mode 100644 index 00000000000..42868d53879 --- /dev/null +++ b/adapters/pubmatic/pubmatic_ow.go @@ -0,0 +1,35 @@ +package pubmatic + +import ( + "encoding/json" +) + +func getTargetingKeys(bidExt json.RawMessage, bidderName string) map[string]string { + targets := map[string]string{} + if bidExt != nil { + bidExtMap := make(map[string]interface{}) + err := json.Unmarshal(bidExt, &bidExtMap) + if err == nil && bidExtMap[buyId] != nil { + targets[buyIdTargetingKey+bidderName] = string(bidExtMap[buyId].(string)) + } + } + return targets +} + +func copySBExtToBidExt(sbExt json.RawMessage, bidExt json.RawMessage) json.RawMessage { + if sbExt != nil { + sbExtMap := getMapFromJSON(sbExt) + bidExtMap := make(map[string]interface{}) + if bidExt != nil { + bidExtMap = getMapFromJSON(bidExt) + } + if bidExtMap != nil && sbExtMap != nil { + if sbExtMap[buyId] != nil && bidExtMap[buyId] == nil { + bidExtMap[buyId] = sbExtMap[buyId] + } + } + byteAra, _ := json.Marshal(bidExtMap) + return json.RawMessage(byteAra) + } + return bidExt +} diff --git a/adapters/pubmatic/pubmatic_ow_test.go b/adapters/pubmatic/pubmatic_ow_test.go new file mode 100644 index 00000000000..7c2112c8ab4 --- /dev/null +++ b/adapters/pubmatic/pubmatic_ow_test.go @@ -0,0 +1,72 @@ +package pubmatic + +import ( + "encoding/json" + "testing" +) + +func TestGetAdServerTargetingForEmptyExt(t *testing.T) { + ext := json.RawMessage(`{}`) + targets := getTargetingKeys(ext, "pubmatic") + // banner is the default bid type when no bidType key is present in the bid.ext + if targets != nil && targets["hb_buyid_pubmatic"] != "" { + t.Errorf("It should not contained AdserverTageting") + } +} + +func TestGetAdServerTargetingForValidExt(t *testing.T) { + ext := json.RawMessage("{\"buyid\":\"testBuyId\"}") + targets := getTargetingKeys(ext, "pubmatic") + // banner is the default bid type when no bidType key is present in the bid.ext + if targets == nil { + t.Error("It should have targets") + t.FailNow() + } + if targets != nil && targets["hb_buyid_pubmatic"] != "testBuyId" { + t.Error("It should have testBuyId as targeting") + t.FailNow() + } +} + +func TestGetAdServerTargetingForPubmaticAlias(t *testing.T) { + ext := json.RawMessage("{\"buyid\":\"testBuyId-alias\"}") + targets := getTargetingKeys(ext, "dummy-alias") + // banner is the default bid type when no bidType key is present in the bid.ext + if targets == nil { + t.Error("It should have targets") + t.FailNow() + } + if targets != nil && targets["hb_buyid_dummy-alias"] != "testBuyId-alias" { + t.Error("It should have testBuyId as targeting") + t.FailNow() + } +} + +func TestCopySBExtToBidExtWithBidExt(t *testing.T) { + sbext := json.RawMessage("{\"buyid\":\"testBuyId\"}") + bidext := json.RawMessage("{\"dspId\":\"9\"}") + // expectedbid := json.RawMessage("{\"dspId\":\"9\",\"buyid\":\"testBuyId\"}") + bidextnew := copySBExtToBidExt(sbext, bidext) + if bidextnew == nil { + t.Errorf("it should not be nil") + } +} + +func TestCopySBExtToBidExtWithNoBidExt(t *testing.T) { + sbext := json.RawMessage("{\"buyid\":\"testBuyId\"}") + bidext := json.RawMessage("{\"dspId\":\"9\"}") + // expectedbid := json.RawMessage("{\"dspId\":\"9\",\"buyid\":\"testBuyId\"}") + bidextnew := copySBExtToBidExt(sbext, bidext) + if bidextnew == nil { + t.Errorf("it should not be nil") + } +} + +func TestCopySBExtToBidExtWithNoSeatExt(t *testing.T) { + bidext := json.RawMessage("{\"dspId\":\"9\"}") + // expectedbid := json.RawMessage("{\"dspId\":\"9\",\"buyid\":\"testBuyId\"}") + bidextnew := copySBExtToBidExt(nil, bidext) + if bidextnew == nil { + t.Errorf("it should not be nil") + } +} diff --git a/adapters/pubmatic/pubmatic_test.go b/adapters/pubmatic/pubmatic_test.go index 1c99d994367..77063c9422a 100644 --- a/adapters/pubmatic/pubmatic_test.go +++ b/adapters/pubmatic/pubmatic_test.go @@ -2,6 +2,8 @@ package pubmatic import ( "encoding/json" + "sort" + "strings" "testing" "github.com/mxmCherry/openrtb/v16/openrtb2" @@ -161,6 +163,7 @@ func TestExtractPubmaticExtFromRequest(t *testing.T) { args args expectedWrapperExt *pubmaticWrapperExt expectedAcat []string + expectedCookie []string wantErr bool }{ { @@ -220,10 +223,11 @@ func TestExtractPubmaticExtFromRequest(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotWrapperExt, gotAcat, err := extractPubmaticExtFromRequest(tt.args.request) + gotWrapperExt, gotAcat, gotCookie, err := extractPubmaticExtFromRequest(tt.args.request) assert.Equal(t, tt.wantErr, err != nil) assert.Equal(t, tt.expectedWrapperExt, gotWrapperExt) assert.Equal(t, tt.expectedAcat, gotAcat) + assert.Equal(t, tt.expectedCookie, gotCookie) }) } } @@ -264,3 +268,236 @@ func TestPubmaticAdapter_MakeRequests(t *testing.T) { }) } } + +func TestPopulateFirstPartyDataImpAttributes(t *testing.T) { + type args struct { + data json.RawMessage + impExtMap map[string]interface{} + } + tests := []struct { + name string + args args + expectedImpExt map[string]interface{} + }{ + { + name: "Only Targeting present in imp.ext.data", + args: args{ + data: json.RawMessage(`{"sport":["rugby","cricket"]}`), + impExtMap: map[string]interface{}{}, + }, + expectedImpExt: map[string]interface{}{ + "key_val": "sport=rugby,cricket", + }, + }, + { + name: "Targeting and adserver object present in imp.ext.data", + args: args{ + data: json.RawMessage(`{"adserver": {"name": "gam","adslot": "/1111/home"},"pbadslot": "/2222/home","sport":["rugby","cricket"]}`), + impExtMap: map[string]interface{}{}, + }, + expectedImpExt: map[string]interface{}{ + "dfp_ad_unit_code": "/1111/home", + "key_val": "sport=rugby,cricket", + }, + }, + { + name: "Targeting and pbadslot key present in imp.ext.data ", + args: args{ + data: json.RawMessage(`{"pbadslot": "/2222/home","sport":["rugby","cricket"]}`), + impExtMap: map[string]interface{}{}, + }, + expectedImpExt: map[string]interface{}{ + "dfp_ad_unit_code": "/2222/home", + "key_val": "sport=rugby,cricket", + }, + }, + { + name: "Targeting and Invalid Adserver object in imp.ext.data", + args: args{ + data: json.RawMessage(`{"adserver": "invalid","sport":["rugby","cricket"]}`), + impExtMap: map[string]interface{}{}, + }, + expectedImpExt: map[string]interface{}{ + "key_val": "sport=rugby,cricket", + }, + }, + { + name: "key_val already present in imp.ext.data", + args: args{ + data: json.RawMessage(`{"sport":["rugby","cricket"]}`), + impExtMap: map[string]interface{}{ + "key_val": "k1=v1|k2=v2", + }, + }, + expectedImpExt: map[string]interface{}{ + "key_val": "k1=v1|k2=v2|sport=rugby,cricket", + }, + }, + { + name: "int data present in imp.ext.data", + args: args{ + data: json.RawMessage(`{"age": 25}`), + impExtMap: map[string]interface{}{}, + }, + expectedImpExt: map[string]interface{}{ + "key_val": "age=25", + }, + }, + { + name: "float data present in imp.ext.data", + args: args{ + data: json.RawMessage(`{"floor": 0.15}`), + impExtMap: map[string]interface{}{}, + }, + expectedImpExt: map[string]interface{}{ + "key_val": "floor=0.15", + }, + }, + { + name: "bool data present in imp.ext.data", + args: args{ + data: json.RawMessage(`{"k1": true}`), + impExtMap: map[string]interface{}{}, + }, + expectedImpExt: map[string]interface{}{ + "key_val": "k1=true", + }, + }, + { + name: "imp.ext.data is not present", + args: args{ + data: nil, + impExtMap: map[string]interface{}{}, + }, + expectedImpExt: map[string]interface{}{}, + }, + { + name: "string with spaces present in imp.ext.data", + args: args{ + data: json.RawMessage(`{" category ": " cinema "}`), + impExtMap: map[string]interface{}{}, + }, + expectedImpExt: map[string]interface{}{ + "key_val": "category=cinema", + }, + }, + { + name: "string array with spaces present in imp.ext.data", + args: args{ + data: json.RawMessage(`{" country\t": [" India", "\tChina "]}`), + impExtMap: map[string]interface{}{}, + }, + expectedImpExt: map[string]interface{}{ + "key_val": "country=India,China", + }, + }, + { + name: "Invalid data present in imp.ext.data", + args: args{ + data: json.RawMessage(`{"country": [1, "India"],"category":"movies"}`), + impExtMap: map[string]interface{}{}, + }, + expectedImpExt: map[string]interface{}{ + "key_val": "category=movies", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + populateFirstPartyDataImpAttributes(tt.args.data, tt.args.impExtMap) + assert.Equal(t, tt.expectedImpExt, tt.args.impExtMap) + }) + } +} + +func TestPopulateFirstPartyDataImpAttributesForMultipleAttributes(t *testing.T) { + impExtMap := map[string]interface{}{ + "key_val": "k1=v1|k2=v2", + } + data := json.RawMessage(`{"sport":["rugby","cricket"],"pageType":"article","age":30,"floor":1.25}`) + expectedKeyValArr := []string{"age=30", "floor=1.25", "k1=v1", "k2=v2", "pageType=article", "sport=rugby,cricket"} + + populateFirstPartyDataImpAttributes(data, impExtMap) + + //read dctr value and split on "|" for comparison + actualKeyValArr := strings.Split(impExtMap[dctrKeyName].(string), "|") + sort.Strings(actualKeyValArr) + assert.Equal(t, expectedKeyValArr, actualKeyValArr) +} + +func TestGetStringArray(t *testing.T) { + tests := []struct { + name string + input interface{} + output []string + }{ + { + name: "Valid String Array", + input: append(make([]interface{}, 0), "hello", "world"), + output: []string{"hello", "world"}, + }, + { + name: "Invalid String Array", + input: "hello", + output: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getStringArray(tt.input) + assert.Equal(t, tt.output, got) + }) + } +} + +func TestIsStringArray(t *testing.T) { + tests := []struct { + name string + input []interface{} + output bool + }{ + { + name: "Valid String Array", + input: append(make([]interface{}, 0), "hello", "world"), + output: true, + }, + { + name: "Invalid String Array", + input: append(make([]interface{}, 0), 1, 2), + output: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isStringArray(tt.input) + assert.Equal(t, tt.output, got) + }) + } +} + +func TestGetMapFromJSON(t *testing.T) { + tests := []struct { + name string + input json.RawMessage + output map[string]interface{} + }{ + { + name: "Valid JSON", + input: json.RawMessage("{\"buyid\":\"testBuyId\"}"), + output: map[string]interface{}{ + "buyid": "testBuyId", + }, + }, + { + name: "Invalid JSON", + input: json.RawMessage("{\"buyid\":}"), + output: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getMapFromJSON(tt.input) + assert.Equal(t, tt.output, got) + }) + } +} diff --git a/adapters/pubmatic/pubmatictest/exemplary/banner.json b/adapters/pubmatic/pubmatictest/exemplary/banner.json index 5012192a4ea..494a8442c21 100644 --- a/adapters/pubmatic/pubmatictest/exemplary/banner.json +++ b/adapters/pubmatic/pubmatictest/exemplary/banner.json @@ -36,7 +36,8 @@ "ext": { "prebid": { "bidderparams": { - "acat": ["drg","dlu","ssr"] + "acat": ["drg","dlu","ssr"], + "wiid": "dwzafakjflan-tygannnvlla-mlljvj" } } }, @@ -45,7 +46,7 @@ "publisher": { "id": "1234" } - } + } }, "httpCalls": [ @@ -87,7 +88,8 @@ "ext": { "wrapper": { "profile": 5123, - "version":1 + "version":1, + "wiid" : "dwzafakjflan-tygannnvlla-mlljvj" }, "acat": ["drg","dlu","ssr"] } @@ -151,4 +153,4 @@ ] } ] - } \ No newline at end of file + } diff --git a/adapters/pubmatic/pubmatictest/exemplary/video-rewarded.json b/adapters/pubmatic/pubmatictest/exemplary/video-rewarded.json new file mode 100644 index 00000000000..ae71c315d6c --- /dev/null +++ b/adapters/pubmatic/pubmatictest/exemplary/video-rewarded.json @@ -0,0 +1,174 @@ +{ + "mockBidRequest": { + "id": "test-video-request", + "imp": [{ + "id": "test-video-imp", + "video": { + "w":640, + "h":480, + "mimes": ["video/mp4", "video/x-flv"], + "minduration": 5, + "maxduration": 30, + "startdelay": 5, + "playbackmethod": [1, 3], + "api": [1, 2], + "protocols": [2, 3], + "battr": [13, 14], + "linearity": 1, + "placement": 2, + "minbitrate": 10, + "maxbitrate": 10 + }, + "ext": { + "prebid": { + "is_rewarded_inventory": 1 + }, + "bidder": { + "adSlot": "AdTag_Div1@0x0", + "publisherId": "999", + "keywords": [{ + "key": "pmZoneID", + "value": ["Zone1", "Zone2"] + } + ], + "wrapper": { + "version": 1, + "profile": 5123 + } + } + } + }], + "device":{ + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" + }, + "site": { + "id": "siteID", + "publisher": { + "id": "1234" + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server", + "body": { + "id": "test-video-request", + "imp": [ + { + "id": "test-video-imp", + "tagid":"AdTag_Div1", + "video": { + "w":640, + "h":480, + "mimes": ["video/mp4", "video/x-flv"], + "minduration": 5, + "maxduration": 30, + "startdelay": 5, + "playbackmethod": [1, 3], + "api": [1, 2], + "protocols": [2, 3], + "battr": [13, 14], + "linearity": 1, + "placement": 2, + "minbitrate": 10, + "maxbitrate": 10 + }, + "ext": { + "pmZoneId": "Zone1,Zone2", + "reward": 1 + } + } + ], + "device":{ + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" + }, + "site": { + "id": "siteID", + "publisher": { + "id": "999" + } + }, + "ext": { + "wrapper": { + "profile": 5123, + "version":1 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-video-request", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-video-imp", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["pubmatic.com"], + "crid": "29681110", + "h": 250, + "w": 300, + "dealid":"test deal", + "cat" : ["IAB-1", "IAB-2"], + "ext": { + "dspid": 6, + "deal_channel": 1, + "BidType": 1, + "video" : { + "duration" : 5 + } + } + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-video-imp", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["pubmatic.com"], + "cat": [ + "IAB-1" + ], + "crid": "29681110", + "w": 300, + "h": 250, + "dealid":"test deal", + "ext": { + "dspid": 6, + "deal_channel": 1, + "BidType": 1, + "video" : { + "duration" : 5 + } + } + }, + "type": "video", + "video" :{ + "duration" : 5 + } + } + ] + } + ] + } diff --git a/adapters/pubmatic/pubmatictest/params/race/banner.json b/adapters/pubmatic/pubmatictest/params/race/banner.json new file mode 100644 index 00000000000..86317f86e4c --- /dev/null +++ b/adapters/pubmatic/pubmatictest/params/race/banner.json @@ -0,0 +1,15 @@ +{ + "publisherId": "156209", + "adSlot": "pubmatic_test2@300x250", + "pmzoneid": "drama,sport", + "dctr": "abBucket=4|adType=page|entity=|paidByCategory=|sku=|userLevel=free|platform=android|majorVersion=3.54|version=3.54.0|mobileApplication=true|showId=20166|show=Kisah Untuk Geri|genre=Drama|contentUrl=https://www.iflix.com/title/show/20166|rating=TV-MA|contentLanguage=id", + "keywords": { + "pmzoneid": "Zone1,Zone2", + "dctr": "abBucket=4|adType=page|entity=|paidByCategory=|sku=|userLevel=free|platform=android|majorVersion=3.54|version=3.54.0|mobileApplication=true|showId=20166|show=Kisah Untuk Geri|genre=Drama|contentUrl=https://www.iflix.com/title/show/20166|rating=TV-MA|contentLanguage=id", + "preference": "sports,movies" + }, + "wrapper": { + "version": 2, + "profile": 595 + } +} diff --git a/adapters/pubmatic/pubmatictest/params/race/video.json b/adapters/pubmatic/pubmatictest/params/race/video.json new file mode 100644 index 00000000000..86317f86e4c --- /dev/null +++ b/adapters/pubmatic/pubmatictest/params/race/video.json @@ -0,0 +1,15 @@ +{ + "publisherId": "156209", + "adSlot": "pubmatic_test2@300x250", + "pmzoneid": "drama,sport", + "dctr": "abBucket=4|adType=page|entity=|paidByCategory=|sku=|userLevel=free|platform=android|majorVersion=3.54|version=3.54.0|mobileApplication=true|showId=20166|show=Kisah Untuk Geri|genre=Drama|contentUrl=https://www.iflix.com/title/show/20166|rating=TV-MA|contentLanguage=id", + "keywords": { + "pmzoneid": "Zone1,Zone2", + "dctr": "abBucket=4|adType=page|entity=|paidByCategory=|sku=|userLevel=free|platform=android|majorVersion=3.54|version=3.54.0|mobileApplication=true|showId=20166|show=Kisah Untuk Geri|genre=Drama|contentUrl=https://www.iflix.com/title/show/20166|rating=TV-MA|contentLanguage=id", + "preference": "sports,movies" + }, + "wrapper": { + "version": 2, + "profile": 595 + } +} diff --git a/adapters/pubmatic/pubmatictest/supplemental/app.json b/adapters/pubmatic/pubmatictest/supplemental/app.json index 01ba1f2e239..48678c36f19 100644 --- a/adapters/pubmatic/pubmatictest/supplemental/app.json +++ b/adapters/pubmatic/pubmatictest/supplemental/app.json @@ -27,6 +27,10 @@ "version": 1, "profile": 5123 } + }, + "skadn": { + "skadnetids": ["k674qkevps.skadnetwork"], + "version": "2.0" } } }], @@ -64,7 +68,11 @@ "bidfloor": 0.12, "ext": { "pmZoneId": "Zone1,Zone2", - "preference": "sports,movies" + "preference": "sports,movies", + "skadn": { + "skadnetids": ["k674qkevps.skadnetwork"], + "version": "2.0" + } } } ], @@ -102,7 +110,21 @@ "crid": "29681110", "h": 250, "w": 300, - "dealid":"test deal" + "dealid":"test deal", + "ext": { + "dspid": 6, + "deal_channel": 1, + "skadn": { + "signature": "MDUCGQDreBN5/xBN547tJeUdqcMSBtBA+Lk06b8CGFkjR1V56rh/H9osF8iripkuZApeDsZ+lQ==", + "campaign": "4", + "network": "k674qkevps.skadnetwork", + "nonce": "D0EC0F04-A4BF-445B-ADF1-E010430C29FD", + "timestamp": "1596695461984", + "sourceapp": "525463029", + "itunesitem": "1499436635", + "version": "2.0" + } + } }] } ], @@ -128,17 +150,25 @@ "crid": "29681110", "w": 300, "h": 250, - "dealid":"test deal" + "dealid":"test deal", + "ext": { + "dspid": 6, + "deal_channel": 1, + "skadn": { + "signature": "MDUCGQDreBN5/xBN547tJeUdqcMSBtBA+Lk06b8CGFkjR1V56rh/H9osF8iripkuZApeDsZ+lQ==", + "campaign": "4", + "network": "k674qkevps.skadnetwork", + "nonce": "D0EC0F04-A4BF-445B-ADF1-E010430C29FD", + "timestamp": "1596695461984", + "sourceapp": "525463029", + "itunesitem": "1499436635", + "version": "2.0" + } + } }, "type": "banner" } ] } - ], - "expectedMakeBidsErrors": [ - { - "value": "unexpected end of JSON input", - "comparison": "literal" - } ] - } \ No newline at end of file + } diff --git a/adapters/pubmatic/pubmatictest/supplemental/banner-video.json b/adapters/pubmatic/pubmatictest/supplemental/banner-video.json new file mode 100644 index 00000000000..0447253b0fc --- /dev/null +++ b/adapters/pubmatic/pubmatictest/supplemental/banner-video.json @@ -0,0 +1,128 @@ +{ + "mockBidRequest": { + "id": "multiple-media-request", + "imp": [ + { + "id": "multiple-media-imp", + "video": { + "mimes": ["video/mp4"] + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }, + { + "w": 728, + "h": 90 + }] + }, + "ext": { + "bidder": { + "adSlot": "AdTag_Div1@0x0", + "publisherId": "999" + } + } + } + ], + "site": { + "id": "siteID" + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server", + "body": { + "id": "multiple-media-request", + "imp": [ + { + "id": "multiple-media-imp", + "tagid":"AdTag_Div1", + "video": { + "mimes": ["video/mp4"] + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 728, + "h": 90 + } + ], + "h": 250, + "w": 300 + } + } + ], + "site": { + "id": "siteID", + "publisher": { + "id": "999" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "multiple-media-request", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "multiple-media-imp", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["pubmatic.com"], + "crid": "29681110", + "h": 250, + "w": 300, + "dealid":"test deal", + "ext": { + "dspid": 6, + "deal_channel": 1 + } + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "multiple-media-imp", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["pubmatic.com"], + "crid": "29681110", + "w": 300, + "h": 250, + "dealid":"test deal", + "ext": { + "dspid": 6, + "deal_channel": 1 + } + }, + "type": "banner" + } + ] + } + ] + } \ No newline at end of file diff --git a/adapters/pubmatic/pubmatictest/supplemental/eid.json b/adapters/pubmatic/pubmatictest/supplemental/eid.json new file mode 100644 index 00000000000..453b2bbfb0c --- /dev/null +++ b/adapters/pubmatic/pubmatictest/supplemental/eid.json @@ -0,0 +1,217 @@ +{ + "mockBidRequest": { + "id": "test-request-eid-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": { + "adSlot": "AdTag_Div1@300x250", + "publisherId": "999", + "keywords": [{ + "key": "pmZoneID", + "value": ["Zone1", "Zone2"] + }, + { + "key": "preference", + "value": ["sports", "movies"] + } + ], + "kadfloor": "0.12", + "wrapper": { + "version": 1, + "profile": 5123 + } + } + } + }], + "device":{ + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" + }, + "ext": { + "prebid": { + "bidderparams": { + "acat": ["drg","dlu","ssr"], + "wiid": "dwzafakjflan-tygannnvlla-mlljvj" + } + } + }, + "site": { + "id": "siteID", + "publisher": { + "id": "1234" + } + }, + "user": { + "ext": { + "eids": [ + { + "source": "bvod.connect", + "uids": [ + { + "atype": 501, + "id": "OztamSession-123456" + }, + { + "ext": { + "demgid": "1234", + "seq": 1 + }, + "atype": 2, + "id": "7D92078A-8246-4BA4-AE5B-76104861E7DC" + }, + { + "ext": { + "demgid": "2345", + "seq": 2 + }, + "atype": 2, + "id": "8D92078A-8246-4BA4-AE5B-76104861E7DC" + } + ] + } + ] + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server", + "body": { + "id": "test-request-eid-id", + "imp": [ + { + "id": "test-imp-id", + "tagid":"AdTag_Div1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "h": 250, + "w": 300 + }, + "bidfloor": 0.12, + "ext": { + "pmZoneId": "Zone1,Zone2", + "preference": "sports,movies" + } + } + ], + "device":{ + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" + }, + "site": { + "id": "siteID", + "publisher": { + "id": "999" + } + }, + "user": { + "eids": [ + { + "source": "bvod.connect", + "uids": [ + { + "atype": 501, + "id": "OztamSession-123456" + }, + { + "ext": { + "demgid": "1234", + "seq": 1 + }, + "atype": 2, + "id": "7D92078A-8246-4BA4-AE5B-76104861E7DC" + }, + { + "ext": { + "demgid": "2345", + "seq": 2 + }, + "atype": 2, + "id": "8D92078A-8246-4BA4-AE5B-76104861E7DC" + } + ] + } + ], + "ext":{} + }, + "ext": { + "wrapper": { + "profile": 5123, + "version":1, + "wiid" : "dwzafakjflan-tygannnvlla-mlljvj" + }, + "acat": ["drg","dlu","ssr"] + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["pubmatic.com"], + "crid": "29681110", + "h": 250, + "w": 300, + "dealid":"test deal", + "ext": { + "dspid": 6, + "deal_channel": 1 + } + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["pubmatic.com"], + "crid": "29681110", + "w": 300, + "h": 250, + "dealid":"test deal", + "ext": { + "dspid": 6, + "deal_channel": 1 + } + }, + "type": "banner" + } + ] + } + ] + } diff --git a/adapters/pubmatic/pubmatictest/supplemental/gptSlotNameInImpExt.json b/adapters/pubmatic/pubmatictest/supplemental/gptSlotNameInImpExt.json index 50f677c8c0b..cf016565de0 100644 --- a/adapters/pubmatic/pubmatictest/supplemental/gptSlotNameInImpExt.json +++ b/adapters/pubmatic/pubmatictest/supplemental/gptSlotNameInImpExt.json @@ -55,6 +55,13 @@ "publisher": { "id": "1234" } + }, + "ext": { + "prebid": { + "bidderparams": { + "wiid": "dwzafakjflan-tygannnvlla-mlljvj" + } + } } }, "httpCalls": [ @@ -96,7 +103,8 @@ "ext": { "wrapper": { "profile": 5123, - "version": 1 + "version": 1, + "wiid": "dwzafakjflan-tygannnvlla-mlljvj" } } } diff --git a/adapters/pubmatic/pubmatictest/supplemental/gptSlotNameInImpExtPbAdslot.json b/adapters/pubmatic/pubmatictest/supplemental/gptSlotNameInImpExtPbAdslot.json index cc057909a5b..f3cb4713c9d 100644 --- a/adapters/pubmatic/pubmatictest/supplemental/gptSlotNameInImpExtPbAdslot.json +++ b/adapters/pubmatic/pubmatictest/supplemental/gptSlotNameInImpExtPbAdslot.json @@ -51,6 +51,13 @@ "publisher": { "id": "1234" } + }, + "ext": { + "prebid": { + "bidderparams": { + "wiid": "dwzafakjflan-tygannnvlla-mlljvj" + } + } } }, "httpCalls": [ @@ -92,7 +99,8 @@ "ext": { "wrapper": { "profile": 5123, - "version": 1 + "version": 1, + "wiid": "dwzafakjflan-tygannnvlla-mlljvj" } } } @@ -160,4 +168,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/adapters/pubmatic/pubmatictest/supplemental/impExtData.json b/adapters/pubmatic/pubmatictest/supplemental/impExtData.json new file mode 100644 index 00000000000..45c1176d432 --- /dev/null +++ b/adapters/pubmatic/pubmatictest/supplemental/impExtData.json @@ -0,0 +1,155 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "adSlot": "AdTag_Div1@300x250", + "publisherId": "999", + "wrapper": { + "version": 1, + "profile": 5123 + }, + "dctr": "k1=v1|k2=v2" + }, + "data": { + "adserver": { + "name": "gam", + "adslot": "/1111/home" + }, + "pbadslot": "/2222/home", + "sport": [ + "rugby", + "cricket" + ] + } + } + } + ], + "device": { + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" + }, + "site": { + "id": "siteID", + "publisher": { + "id": "1234" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "AdTag_Div1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "h": 250, + "w": 300 + }, + "ext": { + "dfp_ad_unit_code": "/1111/home", + "key_val": "k1=v1|k2=v2|sport=rugby,cricket" + } + } + ], + "device": { + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" + }, + "site": { + "id": "siteID", + "publisher": { + "id": "999" + } + }, + "ext": { + "wrapper": { + "profile": 5123, + "version": 1 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "pubmatic.com" + ], + "crid": "29681110", + "h": 250, + "w": 300, + "dealid": "test deal", + "ext": { + "dspid": 6, + "deal_channel": 1 + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "pubmatic.com" + ], + "crid": "29681110", + "w": 300, + "h": 250, + "dealid": "test deal", + "ext": { + "dspid": 6, + "deal_channel": 1 + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pubmatic/pubmatictest/supplemental/pmZoneIDInKeywords.json b/adapters/pubmatic/pubmatictest/supplemental/pmZoneIDInKeywords.json index c9c335e8646..7698bde1ba5 100644 --- a/adapters/pubmatic/pubmatictest/supplemental/pmZoneIDInKeywords.json +++ b/adapters/pubmatic/pubmatictest/supplemental/pmZoneIDInKeywords.json @@ -19,7 +19,7 @@ "dctr": "key1=V1,V2,V3|key2=v1|key3=v3,v5", "keywords": [ { - "key": "pmZoneID", + "key": "pmzoneid", "value": [ "Zone1", "Zone2" diff --git a/adapters/pubmatic/pubmatictest/supplemental/urlEncodedDCTR.json b/adapters/pubmatic/pubmatictest/supplemental/urlEncodedDCTR.json new file mode 100644 index 00000000000..501bf2fd165 --- /dev/null +++ b/adapters/pubmatic/pubmatictest/supplemental/urlEncodedDCTR.json @@ -0,0 +1,166 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "adSlot": "AdTag_Div1@300x250", + "publisherId": " 999 ", + "keywords": [ + { + "key": "pmZoneID", + "value": [ + "Zone1", + "Zone2" + ] + }, + { + "key": "preference", + "value": [ + "sports", + "movies" + ] + }, + { + "key":"dctr", + "value":[ + "title%3DThe%20Hunt%7Cgenre%3Danimation%2Cadventure" + ] + } + ], + "wrapper": { + "version": 1, + "profile": 5123 + } + } + } + } + ], + "device": { + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" + }, + "site": { + "id": "siteID", + "publisher": { + "id": "1234" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "AdTag_Div1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "h": 250, + "w": 300 + }, + "ext": { + "key_val": "title=The Hunt|genre=animation,adventure", + "pmZoneId": "Zone1,Zone2", + "preference": "sports,movies" + } + } + ], + "device": { + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" + }, + "site": { + "id": "siteID", + "publisher": { + "id": "999" + } + }, + "ext": { + "wrapper": { + "profile": 5123, + "version": 1 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "pubmatic.com" + ], + "crid": "29681110", + "h": 250, + "w": 300, + "dealid": "test deal", + "ext": { + "dspid": 6, + "deal_channel": 1 + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "pubmatic.com" + ], + "crid": "29681110", + "w": 300, + "h": 250, + "dealid": "test deal", + "ext": { + "dspid": 6, + "deal_channel": 1 + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/spotx/params_test.go b/adapters/spotx/params_test.go new file mode 100644 index 00000000000..de96ebe7953 --- /dev/null +++ b/adapters/spotx/params_test.go @@ -0,0 +1,58 @@ +package spotx + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestSpotxParams(t *testing.T) { + testValidParams(t) + testInvalidParams(t) +} + +func testValidParams(t *testing.T) { + + params := []string{ + `{"channel_id": "12345", "ad_unit": "instream"}`, + `{"channel_id": "12345", "ad_unit": "instream", "secure": true}`, + `{"channel_id": "12345", "ad_unit": "instream", "secure": true, "ad_volume": 0.4}`, + `{"channel_id": "12345", "ad_unit": "instream", "secure": true, "ad_volume": 0.4, "price_floor": 10}`, + `{"channel_id": "12345", "ad_unit": "instream", "secure": true, "ad_volume": 0.4, "price_floor": 10, "hide_skin": false}`, + } + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Error loading json schema for spotx paramaters: %v", err) + } + + for _, param := range params { + if err := validator.Validate(openrtb_ext.BidderSpotX, json.RawMessage(param)); err != nil { + t.Errorf("Params schema mismatch - %s: %v", param, err) + } + } +} + +// TestInvalidParams makes sure that the 33Across schema rejects all the imp.ext fields we don't support. +func testInvalidParams(t *testing.T) { + params := []string{ + `{"channel_id": "1234", "ad_unit": "instream", "secure": true, "ad_volume": 0.4, "price_floor": 10, "hide_skin": false}`, + `{"channel_id": "12345", "ad_unit": "outstream1", "secure": true, "ad_volume": 0.4, "price_floor": 10, "hide_skin": false}`, + `{"ad_unit": "instream", "secure": true, "ad_volume": 0.4, "price_floor": 10, "hide_skin": false}`, + `{"channel_id": "12345", "secure": true, "ad_volume": 0.4, "price_floor": 10, "hide_skin": false}`, + `{"channel_id": "12345", "ad_unit": "instream", "secure": 1, "ad_volume": 0.4, "price_floor": 10, "hide_skin": false}`, + `{"channel_id": "12345", "ad_unit": "instream", "secure": true, "ad_volume": "0.4", "price_floor": 10, "hide_skin": false}`, + `{"channel_id": "12345", "ad_unit": "instream", "secure": true, "ad_volume": 0.4, "price_floor": 10.12, "hide_skin": false}`, + `{"channel_id": "12345", "ad_unit": "instream", "secure": true, "ad_volume": 0.4, "price_floor": 10, "hide_skin": 0}`, + } + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Error loading json schema for spotx paramaters: %v", err) + } + + for _, param := range params { + if err := validator.Validate(openrtb_ext.BidderSpotX, json.RawMessage(param)); err == nil { + t.Errorf("Unexpexted params schema match - %s", param) + } + } +} diff --git a/adapters/spotx/spotx.go b/adapters/spotx/spotx.go new file mode 100644 index 00000000000..91b6ee63797 --- /dev/null +++ b/adapters/spotx/spotx.go @@ -0,0 +1,179 @@ +package spotx + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type Adapter struct { + url string +} + +func (a *Adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + var adapterRequests []*adapters.RequestData + + if len(request.Imp) == 0 { + errs = append(errs, &errortypes.BadInput{Message: "No impression in the bid request"}) + return nil, errs + } + + for i, imp := range request.Imp { + if imp.Video == nil { + errs = append(errs, errors.New(fmt.Sprintf("non video impression at index %d", i))) + continue + } + + adapterReq, err := makeRequest(a, request, imp) + if adapterReq != nil { + adapterRequests = append(adapterRequests, adapterReq) + } + errs = append(errs, err...) + } + + return adapterRequests, errs +} + +func makeRequest(a *Adapter, originalReq *openrtb2.BidRequest, imp openrtb2.Imp) (*adapters.RequestData, []error) { + var errs []error + + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + errs = append(errs, &errortypes.BadInput{ + Message: err.Error(), + }) + return &adapters.RequestData{}, errs + } + + var spotxExt openrtb_ext.ExtImpSpotX + if err := json.Unmarshal(bidderExt.Bidder, &spotxExt); err != nil { + errs = append(errs, &errortypes.BadInput{ + Message: err.Error(), + }) + return &adapters.RequestData{}, errs + } + + reqCopy := *originalReq + reqCopy.ID = spotxExt.ChannelID + + intermediateReq, _ := json.Marshal(reqCopy) + reqMap := make(map[string]interface{}) + _ = json.Unmarshal(intermediateReq, &reqMap) + + intermediateImp, _ := json.Marshal(imp) + impMap := make(map[string]interface{}) + _ = json.Unmarshal(intermediateImp, &impMap) + + if spotxExt.Secure { + impMap["secure"] = 1 + } else { + impMap["secure"] = 0 + } + + impVideoExt := map[string]interface{}{} + if impMap["video"].(map[string]interface{})["ext"] != nil { + _ = json.Unmarshal(impMap["video"].(map[string]interface{})["ext"].([]byte), &impVideoExt) + } + impVideoExt["ad_volume"] = spotxExt.AdVolume + impVideoExt["ad_unit"] = spotxExt.AdUnit + if spotxExt.HideSkin { + impVideoExt["hide_skin"] = 1 + } else { + impVideoExt["hide_skin"] = 0 + } + impMap["video"].(map[string]interface{})["ext"] = impVideoExt + impMap["bidfloor"] = float64(spotxExt.PriceFloor) + + // remove bidder from imp.Ext + if bidderExt.Prebid != nil { + byteExt, _ := json.Marshal(bidderExt) + impMap["ext"] = byteExt + } else { + delete(impMap, "ext") + } + reqMap["imp"] = impMap + + reqJSON, err := json.Marshal(reqMap) + if err != nil { + errs = append(errs, err) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + + return &adapters.RequestData{ + Method: "POST", + Uri: fmt.Sprintf("%s/%s", a.url, spotxExt.ChannelID), + Body: reqJSON, //TODO: This is a custom request struct, other adapters are sending this openrtb2.BidRequest + Headers: headers, + }, errs +} + +func (a *Adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + + if response.StatusCode == http.StatusBadRequest { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), + }} + } + + if response.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), + }} + } + + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) + for _, sb := range bidResp.SeatBid { + for i := range sb.Bid { + if mediaType, err := getMediaTypeForImp(bidResp.ID, internalRequest.Imp); err != nil { + bid := sb.Bid[i] + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: mediaType, + }) + } + } + } + return bidResponse, nil +} + +func getMediaTypeForImp(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { + for _, imp := range imps { + if imp.ID == impID && imp.Video != nil { + return openrtb_ext.BidTypeVideo, nil + } + } + return "", errors.New("only videos supported") +} + +func NewSpotxBidder(url string) *Adapter { + return &Adapter{ + url: url, + } +} + +// Builder builds a new instance of the Sovrn adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { + bidder := &Adapter{ + url: config.Endpoint, + } + return bidder, nil +} diff --git a/adapters/spotx/spotx_test.go b/adapters/spotx/spotx_test.go new file mode 100644 index 00000000000..be57d774ee4 --- /dev/null +++ b/adapters/spotx/spotx_test.go @@ -0,0 +1,66 @@ +package spotx + +import ( + "encoding/json" + "testing" + + "github.com/magiconair/properties/assert" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" +) + +func TestSpotxMakeBid(t *testing.T) { + + var secure int8 = 1 + + parmsJSON := []byte(`{ + "bidder": { + "channel_id": "85394", + "ad_unit": "instream", + "secure": true, + "ad_volume": 0.800000, + "price_floor": 9, + "hide_skin": false + } + }`) + + request := &openrtb2.BidRequest{ + ID: "1559039248176", + Imp: []openrtb2.Imp{ + { + ID: "28635736ddc2bb", + Video: &openrtb2.Video{ + MIMEs: []string{"video/3gpp"}, + }, + Secure: &secure, + Exp: 2, + Ext: parmsJSON, + }, + }, + } + + extReq := adapters.ExtraRequestInfo{} + reqData, err := NewSpotxBidder("https://search.spotxchange.com/openrtb/2.3/dados").MakeRequests(request, &extReq) + if err != nil { + t.Error("Some err occurred while forming request") + t.FailNow() + } + + assert.Equal(t, reqData[0].Method, "POST") + assert.Equal(t, reqData[0].Uri, "https://search.spotxchange.com/openrtb/2.3/dados/85394") + assert.Equal(t, reqData[0].Headers.Get("Content-Type"), "application/json;charset=utf-8") + + var bodyMap map[string]interface{} + _ = json.Unmarshal(reqData[0].Body, &bodyMap) + assert.Equal(t, bodyMap["id"].(string), "85394") + + impMap := bodyMap["imp"].(map[string]interface{}) + assert.Equal(t, impMap["bidfloor"].(float64), float64(9)) + assert.Equal(t, impMap["secure"].(float64), float64(1)) + + extMap := impMap["video"].(map[string]interface{})["ext"].(map[string]interface{}) + assert.Equal(t, extMap["ad_unit"], "instream") + assert.Equal(t, extMap["ad_volume"], 0.8) + assert.Equal(t, extMap["hide_skin"].(float64), float64(0)) + +} diff --git a/adapters/vastbidder/bidder_macro.go b/adapters/vastbidder/bidder_macro.go new file mode 100644 index 00000000000..a07bc7d4652 --- /dev/null +++ b/adapters/vastbidder/bidder_macro.go @@ -0,0 +1,1251 @@ +package vastbidder + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/mxmCherry/openrtb/v16/adcom1" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +//BidderMacro default implementation +type BidderMacro struct { + IBidderMacro + + //Configuration Parameters + Conf *config.Adapter + + //OpenRTB Specific Parameters + Request *openrtb2.BidRequest + IsApp bool + HasGeo bool + Imp *openrtb2.Imp + Publisher *openrtb2.Publisher + Content *openrtb2.Content + + //Extensions + ImpBidderExt openrtb_ext.ExtImpVASTBidder + VASTTag *openrtb_ext.ExtImpVASTBidderTag + UserExt *openrtb_ext.ExtUser + RegsExt *openrtb_ext.ExtRegs + DeviceExt *openrtb_ext.ExtDevice + + //Impression level Request Headers + ImpReqHeaders http.Header +} + +//NewBidderMacro contains definition for all openrtb macro's +func NewBidderMacro() IBidderMacro { + obj := &BidderMacro{} + obj.IBidderMacro = obj + return obj +} + +func (tag *BidderMacro) init() { + if nil != tag.Request.App { + tag.IsApp = true + tag.Publisher = tag.Request.App.Publisher + tag.Content = tag.Request.App.Content + } else { + tag.Publisher = tag.Request.Site.Publisher + tag.Content = tag.Request.Site.Content + } + tag.HasGeo = nil != tag.Request.Device && nil != tag.Request.Device.Geo + + //Read User Extensions + if nil != tag.Request.User && nil != tag.Request.User.Ext { + var ext openrtb_ext.ExtUser + err := json.Unmarshal(tag.Request.User.Ext, &ext) + if nil == err { + tag.UserExt = &ext + } + } + + //Read Regs Extensions + if nil != tag.Request.Regs && nil != tag.Request.Regs.Ext { + var ext openrtb_ext.ExtRegs + err := json.Unmarshal(tag.Request.Regs.Ext, &ext) + if nil == err { + tag.RegsExt = &ext + } + } + + //Read Device Extensions + if nil != tag.Request.Device && nil != tag.Request.Device.Ext { + var ext openrtb_ext.ExtDevice + err := json.Unmarshal(tag.Request.Device.Ext, &ext) + if nil == err { + tag.DeviceExt = &ext + } + } +} + +//InitBidRequest will initialise BidRequest +func (tag *BidderMacro) InitBidRequest(request *openrtb2.BidRequest) { + tag.Request = request + tag.init() +} + +//LoadImpression will set current imp +func (tag *BidderMacro) LoadImpression(imp *openrtb2.Imp) (*openrtb_ext.ExtImpVASTBidder, error) { + tag.Imp = imp + + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, err + } + + tag.ImpBidderExt = openrtb_ext.ExtImpVASTBidder{} + if err := json.Unmarshal(bidderExt.Bidder, &tag.ImpBidderExt); err != nil { + return nil, err + } + return &tag.ImpBidderExt, nil +} + +//LoadVASTTag will set current VAST Tag details in bidder keys +func (tag *BidderMacro) LoadVASTTag(vastTag *openrtb_ext.ExtImpVASTBidderTag) { + tag.VASTTag = vastTag +} + +//GetBidderKeys will set bidder level keys +func (tag *BidderMacro) GetBidderKeys() map[string]string { + //Adding VAST Tag Bidder Parameters + keys := NormalizeJSON(tag.VASTTag.Params) + + //Adding VAST Tag Standard Params + keys["dur"] = strconv.Itoa(tag.VASTTag.Duration) + + //Adding Headers as Custom Macros + + //Adding Cookies as Custom Macros + + //Adding Default Empty for standard keys + for i := range ParamKeys { + if _, ok := keys[ParamKeys[i]]; !ok { + keys[ParamKeys[i]] = "" + } + } + return keys +} + +//SetAdapterConfig will set Adapter config +func (tag *BidderMacro) SetAdapterConfig(conf *config.Adapter) { + tag.Conf = conf +} + +//GetURI get URL +func (tag *BidderMacro) GetURI() string { + + //check for URI at impression level + if nil != tag.VASTTag { + return tag.VASTTag.URL + } + + //check for URI at config level + return tag.Conf.Endpoint +} + +//GetHeaders returns list of custom request headers +//Override this method if your Vast bidder needs custom request headers +func (tag *BidderMacro) GetHeaders() http.Header { + return http.Header{} +} + +/********************* Request *********************/ + +//MacroTest contains definition for Test Parameter +func (tag *BidderMacro) MacroTest(key string) string { + if tag.Request.Test > 0 { + return strconv.Itoa(int(tag.Request.Test)) + } + return "" +} + +//MacroTimeout contains definition for Timeout Parameter +func (tag *BidderMacro) MacroTimeout(key string) string { + if tag.Request.TMax > 0 { + return strconv.FormatInt(tag.Request.TMax, intBase) + } + return "" +} + +//MacroWhitelistSeat contains definition for WhitelistSeat Parameter +func (tag *BidderMacro) MacroWhitelistSeat(key string) string { + return strings.Join(tag.Request.WSeat, comma) +} + +//MacroWhitelistLang contains definition for WhitelistLang Parameter +func (tag *BidderMacro) MacroWhitelistLang(key string) string { + return strings.Join(tag.Request.WLang, comma) +} + +//MacroBlockedSeat contains definition for Blockedseat Parameter +func (tag *BidderMacro) MacroBlockedSeat(key string) string { + return strings.Join(tag.Request.BSeat, comma) +} + +//MacroCurrency contains definition for Currency Parameter +func (tag *BidderMacro) MacroCurrency(key string) string { + return strings.Join(tag.Request.Cur, comma) +} + +//MacroBlockedCategory contains definition for BlockedCategory Parameter +func (tag *BidderMacro) MacroBlockedCategory(key string) string { + return strings.Join(tag.Request.BCat, comma) +} + +//MacroBlockedAdvertiser contains definition for BlockedAdvertiser Parameter +func (tag *BidderMacro) MacroBlockedAdvertiser(key string) string { + return strings.Join(tag.Request.BAdv, comma) +} + +//MacroBlockedApp contains definition for BlockedApp Parameter +func (tag *BidderMacro) MacroBlockedApp(key string) string { + return strings.Join(tag.Request.BApp, comma) +} + +/********************* Source *********************/ + +//MacroFD contains definition for FD Parameter +func (tag *BidderMacro) MacroFD(key string) string { + if nil != tag.Request.Source { + return strconv.Itoa(int(tag.Request.Source.FD)) + } + return "" +} + +//MacroTransactionID contains definition for TransactionID Parameter +func (tag *BidderMacro) MacroTransactionID(key string) string { + if nil != tag.Request.Source { + return tag.Request.Source.TID + } + return "" +} + +//MacroPaymentIDChain contains definition for PaymentIDChain Parameter +func (tag *BidderMacro) MacroPaymentIDChain(key string) string { + if nil != tag.Request.Source { + return tag.Request.Source.PChain + } + return "" +} + +/********************* Regs *********************/ + +//MacroCoppa contains definition for Coppa Parameter +func (tag *BidderMacro) MacroCoppa(key string) string { + if nil != tag.Request.Regs { + return strconv.Itoa(int(tag.Request.Regs.COPPA)) + } + return "" +} + +/********************* Impression *********************/ + +//MacroDisplayManager contains definition for DisplayManager Parameter +func (tag *BidderMacro) MacroDisplayManager(key string) string { + return tag.Imp.DisplayManager +} + +//MacroDisplayManagerVersion contains definition for DisplayManagerVersion Parameter +func (tag *BidderMacro) MacroDisplayManagerVersion(key string) string { + return tag.Imp.DisplayManagerVer +} + +//MacroInterstitial contains definition for Interstitial Parameter +func (tag *BidderMacro) MacroInterstitial(key string) string { + if tag.Imp.Instl > 0 { + return strconv.Itoa(int(tag.Imp.Instl)) + } + return "" +} + +//MacroTagID contains definition for TagID Parameter +func (tag *BidderMacro) MacroTagID(key string) string { + return tag.Imp.TagID +} + +//MacroBidFloor contains definition for BidFloor Parameter +func (tag *BidderMacro) MacroBidFloor(key string) string { + if tag.Imp.BidFloor > 0 { + return fmt.Sprintf("%g", tag.Imp.BidFloor) + } + return "" +} + +//MacroBidFloorCurrency contains definition for BidFloorCurrency Parameter +func (tag *BidderMacro) MacroBidFloorCurrency(key string) string { + return tag.Imp.BidFloorCur +} + +//MacroSecure contains definition for Secure Parameter +func (tag *BidderMacro) MacroSecure(key string) string { + if nil != tag.Imp.Secure { + return strconv.Itoa(int(*tag.Imp.Secure)) + } + return "" +} + +//MacroPMP contains definition for PMP Parameter +func (tag *BidderMacro) MacroPMP(key string) string { + if nil != tag.Imp.PMP { + data, _ := json.Marshal(tag.Imp.PMP) + return string(data) + } + return "" +} + +/********************* Video *********************/ + +//MacroVideoMIMES contains definition for VideoMIMES Parameter +func (tag *BidderMacro) MacroVideoMIMES(key string) string { + if nil != tag.Imp.Video { + return strings.Join(tag.Imp.Video.MIMEs, comma) + } + return "" +} + +//MacroVideoMinimumDuration contains definition for VideoMinimumDuration Parameter +func (tag *BidderMacro) MacroVideoMinimumDuration(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MinDuration > 0 { + return strconv.FormatInt(tag.Imp.Video.MinDuration, intBase) + } + return "" +} + +//MacroVideoMaximumDuration contains definition for VideoMaximumDuration Parameter +func (tag *BidderMacro) MacroVideoMaximumDuration(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MaxDuration > 0 { + return strconv.FormatInt(tag.Imp.Video.MaxDuration, intBase) + } + return "" +} + +//MacroVideoProtocols contains definition for VideoProtocols Parameter +func (tag *BidderMacro) MacroVideoProtocols(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.Protocols + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +//MacroVideoPlayerWidth contains definition for VideoPlayerWidth Parameter +func (tag *BidderMacro) MacroVideoPlayerWidth(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.W > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.W), intBase) + } + return "" +} + +//MacroVideoPlayerHeight contains definition for VideoPlayerHeight Parameter +func (tag *BidderMacro) MacroVideoPlayerHeight(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.H > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.H), intBase) + } + return "" +} + +//MacroVideoStartDelay contains definition for VideoStartDelay Parameter +func (tag *BidderMacro) MacroVideoStartDelay(key string) string { + if nil != tag.Imp.Video && nil != tag.Imp.Video.StartDelay { + return strconv.FormatInt(int64(*tag.Imp.Video.StartDelay), intBase) + } + return "" +} + +//MacroVideoPlacement contains definition for VideoPlacement Parameter +func (tag *BidderMacro) MacroVideoPlacement(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.Placement > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.Placement), intBase) + } + return "" +} + +//MacroVideoLinearity contains definition for VideoLinearity Parameter +func (tag *BidderMacro) MacroVideoLinearity(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.Linearity > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.Linearity), intBase) + } + return "" +} + +//MacroVideoSkip contains definition for VideoSkip Parameter +func (tag *BidderMacro) MacroVideoSkip(key string) string { + if nil != tag.Imp.Video && nil != tag.Imp.Video.Skip { + return strconv.FormatInt(int64(*tag.Imp.Video.Skip), intBase) + } + return "" +} + +//MacroVideoSkipMinimum contains definition for VideoSkipMinimum Parameter +func (tag *BidderMacro) MacroVideoSkipMinimum(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.SkipMin > 0 { + return strconv.FormatInt(tag.Imp.Video.SkipMin, intBase) + } + return "" +} + +//MacroVideoSkipAfter contains definition for VideoSkipAfter Parameter +func (tag *BidderMacro) MacroVideoSkipAfter(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.SkipAfter > 0 { + return strconv.FormatInt(tag.Imp.Video.SkipAfter, intBase) + } + return "" +} + +//MacroVideoSequence contains definition for VideoSequence Parameter +func (tag *BidderMacro) MacroVideoSequence(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.Sequence > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.Sequence), intBase) + } + return "" +} + +//MacroVideoBlockedAttribute contains definition for VideoBlockedAttribute Parameter +func (tag *BidderMacro) MacroVideoBlockedAttribute(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.BAttr + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +//MacroVideoMaximumExtended contains definition for VideoMaximumExtended Parameter +func (tag *BidderMacro) MacroVideoMaximumExtended(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MaxExtended > 0 { + return strconv.FormatInt(tag.Imp.Video.MaxExtended, intBase) + } + return "" +} + +//MacroVideoMinimumBitRate contains definition for VideoMinimumBitRate Parameter +func (tag *BidderMacro) MacroVideoMinimumBitRate(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MinBitRate > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.MinBitRate), intBase) + } + return "" +} + +//MacroVideoMaximumBitRate contains definition for VideoMaximumBitRate Parameter +func (tag *BidderMacro) MacroVideoMaximumBitRate(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.MaxBitRate > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.MaxBitRate), intBase) + } + return "" +} + +//MacroVideoBoxing contains definition for VideoBoxing Parameter +func (tag *BidderMacro) MacroVideoBoxing(key string) string { + if nil != tag.Imp.Video && tag.Imp.Video.BoxingAllowed > 0 { + return strconv.FormatInt(int64(tag.Imp.Video.BoxingAllowed), intBase) + } + return "" +} + +//MacroVideoPlaybackMethod contains definition for VideoPlaybackMethod Parameter +func (tag *BidderMacro) MacroVideoPlaybackMethod(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.PlaybackMethod + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +//MacroVideoDelivery contains definition for VideoDelivery Parameter +func (tag *BidderMacro) MacroVideoDelivery(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.Delivery + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +//MacroVideoPosition contains definition for VideoPosition Parameter +func (tag *BidderMacro) MacroVideoPosition(key string) string { + if nil != tag.Imp.Video && nil != tag.Imp.Video.Pos { + return strconv.FormatInt(int64(*tag.Imp.Video.Pos), intBase) + } + return "" +} + +//MacroVideoAPI contains definition for VideoAPI Parameter +func (tag *BidderMacro) MacroVideoAPI(key string) string { + if nil != tag.Imp.Video { + value := tag.Imp.Video.API + return ObjectArrayToString(len(value), comma, func(i int) string { + return strconv.FormatInt(int64(value[i]), intBase) + }) + } + return "" +} + +/********************* Site *********************/ + +//MacroSiteID contains definition for SiteID Parameter +func (tag *BidderMacro) MacroSiteID(key string) string { + if !tag.IsApp { + return tag.Request.Site.ID + } + return "" +} + +//MacroSiteName contains definition for SiteName Parameter +func (tag *BidderMacro) MacroSiteName(key string) string { + if !tag.IsApp { + return tag.Request.Site.Name + } + return "" +} + +//MacroSitePage contains definition for SitePage Parameter +func (tag *BidderMacro) MacroSitePage(key string) string { + if !tag.IsApp && nil != tag.Request && nil != tag.Request.Site { + return tag.Request.Site.Page + } + return "" +} + +//MacroSiteReferrer contains definition for SiteReferrer Parameter +func (tag *BidderMacro) MacroSiteReferrer(key string) string { + if !tag.IsApp { + return tag.Request.Site.Ref + } + return "" +} + +//MacroSiteSearch contains definition for SiteSearch Parameter +func (tag *BidderMacro) MacroSiteSearch(key string) string { + if !tag.IsApp { + return tag.Request.Site.Search + } + return "" +} + +//MacroSiteMobile contains definition for SiteMobile Parameter +func (tag *BidderMacro) MacroSiteMobile(key string) string { + if !tag.IsApp && tag.Request.Site.Mobile > 0 { + return strconv.FormatInt(int64(tag.Request.Site.Mobile), intBase) + } + return "" +} + +/********************* App *********************/ + +//MacroAppID contains definition for AppID Parameter +func (tag *BidderMacro) MacroAppID(key string) string { + if tag.IsApp { + return tag.Request.App.ID + } + return "" +} + +//MacroAppName contains definition for AppName Parameter +func (tag *BidderMacro) MacroAppName(key string) string { + if tag.IsApp { + return tag.Request.App.Name + } + return "" +} + +//MacroAppBundle contains definition for AppBundle Parameter +func (tag *BidderMacro) MacroAppBundle(key string) string { + if tag.IsApp { + return tag.Request.App.Bundle + } + return "" +} + +//MacroAppStoreURL contains definition for AppStoreURL Parameter +func (tag *BidderMacro) MacroAppStoreURL(key string) string { + if tag.IsApp { + return tag.Request.App.StoreURL + } + return "" +} + +//MacroAppVersion contains definition for AppVersion Parameter +func (tag *BidderMacro) MacroAppVersion(key string) string { + if tag.IsApp { + return tag.Request.App.Ver + } + return "" +} + +//MacroAppPaid contains definition for AppPaid Parameter +func (tag *BidderMacro) MacroAppPaid(key string) string { + if tag.IsApp && tag.Request.App.Paid != 0 { + return strconv.FormatInt(int64(tag.Request.App.Paid), intBase) + } + return "" +} + +/********************* Site/App Common *********************/ + +//MacroCategory contains definition for Category Parameter +func (tag *BidderMacro) MacroCategory(key string) string { + if tag.IsApp { + return strings.Join(tag.Request.App.Cat, comma) + } + return strings.Join(tag.Request.Site.Cat, comma) +} + +//MacroDomain contains definition for Domain Parameter +func (tag *BidderMacro) MacroDomain(key string) string { + if tag.IsApp { + return tag.Request.App.Domain + } + return tag.Request.Site.Domain +} + +//MacroSectionCategory contains definition for SectionCategory Parameter +func (tag *BidderMacro) MacroSectionCategory(key string) string { + if tag.IsApp { + return strings.Join(tag.Request.App.SectionCat, comma) + } + return strings.Join(tag.Request.Site.SectionCat, comma) +} + +//MacroPageCategory contains definition for PageCategory Parameter +func (tag *BidderMacro) MacroPageCategory(key string) string { + if tag.IsApp { + return strings.Join(tag.Request.App.PageCat, comma) + } + return strings.Join(tag.Request.Site.PageCat, comma) +} + +//MacroPrivacyPolicy contains definition for PrivacyPolicy Parameter +func (tag *BidderMacro) MacroPrivacyPolicy(key string) string { + var value int8 = 0 + if tag.IsApp { + value = tag.Request.App.PrivacyPolicy + } else { + value = tag.Request.Site.PrivacyPolicy + } + if value > 0 { + return strconv.FormatInt(int64(value), intBase) + } + return "" +} + +//MacroKeywords contains definition for Keywords Parameter +func (tag *BidderMacro) MacroKeywords(key string) string { + if tag.IsApp { + return tag.Request.App.Keywords + } + return tag.Request.Site.Keywords +} + +/********************* Publisher *********************/ + +//MacroPubID contains definition for PubID Parameter +func (tag *BidderMacro) MacroPubID(key string) string { + if nil != tag.Publisher { + return tag.Publisher.ID + } + return "" +} + +//MacroPubName contains definition for PubName Parameter +func (tag *BidderMacro) MacroPubName(key string) string { + if nil != tag.Publisher { + return tag.Publisher.Name + } + return "" +} + +//MacroPubDomain contains definition for PubDomain Parameter +func (tag *BidderMacro) MacroPubDomain(key string) string { + if nil != tag.Publisher { + return tag.Publisher.Domain + } + return "" +} + +/********************* Content *********************/ + +//MacroContentID contains definition for ContentID Parameter +func (tag *BidderMacro) MacroContentID(key string) string { + if nil != tag.Content { + return tag.Content.ID + } + return "" +} + +//MacroContentEpisode contains definition for ContentEpisode Parameter +func (tag *BidderMacro) MacroContentEpisode(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.Episode), intBase) + } + return "" +} + +//MacroContentTitle contains definition for ContentTitle Parameter +func (tag *BidderMacro) MacroContentTitle(key string) string { + if nil != tag.Content { + return tag.Content.Title + } + return "" +} + +//MacroContentSeries contains definition for ContentSeries Parameter +func (tag *BidderMacro) MacroContentSeries(key string) string { + if nil != tag.Content { + return tag.Content.Series + } + return "" +} + +//MacroContentSeason contains definition for ContentSeason Parameter +func (tag *BidderMacro) MacroContentSeason(key string) string { + if nil != tag.Content { + return tag.Content.Season + } + return "" +} + +//MacroContentArtist contains definition for ContentArtist Parameter +func (tag *BidderMacro) MacroContentArtist(key string) string { + if nil != tag.Content { + return tag.Content.Artist + } + return "" +} + +//MacroContentGenre contains definition for ContentGenre Parameter +func (tag *BidderMacro) MacroContentGenre(key string) string { + if nil != tag.Content { + return tag.Content.Genre + } + return "" +} + +//MacroContentAlbum contains definition for ContentAlbum Parameter +func (tag *BidderMacro) MacroContentAlbum(key string) string { + if nil != tag.Content { + return tag.Content.Album + } + return "" +} + +//MacroContentISrc contains definition for ContentISrc Parameter +func (tag *BidderMacro) MacroContentISrc(key string) string { + if nil != tag.Content { + return tag.Content.ISRC + } + return "" +} + +//MacroContentURL contains definition for ContentURL Parameter +func (tag *BidderMacro) MacroContentURL(key string) string { + if nil != tag.Content { + return tag.Content.URL + } + return "" +} + +//MacroContentCategory contains definition for ContentCategory Parameter +func (tag *BidderMacro) MacroContentCategory(key string) string { + if nil != tag.Content { + return strings.Join(tag.Content.Cat, comma) + } + return "" +} + +//MacroContentProductionQuality contains definition for ContentProductionQuality Parameter +func (tag *BidderMacro) MacroContentProductionQuality(key string) string { + if nil != tag.Content && nil != tag.Content.ProdQ { + return strconv.FormatInt(int64(*tag.Content.ProdQ), intBase) + } + return "" +} + +//MacroContentVideoQuality contains definition for ContentVideoQuality Parameter +func (tag *BidderMacro) MacroContentVideoQuality(key string) string { + if nil != tag.Content && nil != tag.Content.VideoQuality { + return strconv.FormatInt(int64(*tag.Content.VideoQuality), intBase) + } + return "" +} + +//MacroContentContext contains definition for ContentContext Parameter +func (tag *BidderMacro) MacroContentContext(key string) string { + if nil != tag.Content && tag.Content.Context > 0 { + return strconv.FormatInt(int64(tag.Content.Context), intBase) + } + return "" +} + +//MacroContentContentRating contains definition for ContentContentRating Parameter +func (tag *BidderMacro) MacroContentContentRating(key string) string { + if nil != tag.Content { + return tag.Content.ContentRating + } + return "" +} + +//MacroContentUserRating contains definition for ContentUserRating Parameter +func (tag *BidderMacro) MacroContentUserRating(key string) string { + if nil != tag.Content { + return tag.Content.UserRating + } + return "" +} + +//MacroContentQAGMediaRating contains definition for ContentQAGMediaRating Parameter +func (tag *BidderMacro) MacroContentQAGMediaRating(key string) string { + if nil != tag.Content && tag.Content.QAGMediaRating > 0 { + return strconv.FormatInt(int64(tag.Content.QAGMediaRating), intBase) + } + return "" +} + +//MacroContentKeywords contains definition for ContentKeywords Parameter +func (tag *BidderMacro) MacroContentKeywords(key string) string { + if nil != tag.Content { + return tag.Content.Keywords + } + return "" +} + +//MacroContentLiveStream contains definition for ContentLiveStream Parameter +func (tag *BidderMacro) MacroContentLiveStream(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.LiveStream), intBase) + } + return "" +} + +//MacroContentSourceRelationship contains definition for ContentSourceRelationship Parameter +func (tag *BidderMacro) MacroContentSourceRelationship(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.SourceRelationship), intBase) + } + return "" +} + +//MacroContentLength contains definition for ContentLength Parameter +func (tag *BidderMacro) MacroContentLength(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.Len), intBase) + } + return "" +} + +//MacroContentLanguage contains definition for ContentLanguage Parameter +func (tag *BidderMacro) MacroContentLanguage(key string) string { + if nil != tag.Content { + return tag.Content.Language + } + return "" +} + +//MacroContentEmbeddable contains definition for ContentEmbeddable Parameter +func (tag *BidderMacro) MacroContentEmbeddable(key string) string { + if nil != tag.Content { + return strconv.FormatInt(int64(tag.Content.Embeddable), intBase) + } + return "" +} + +/********************* Producer *********************/ + +//MacroProducerID contains definition for ProducerID Parameter +func (tag *BidderMacro) MacroProducerID(key string) string { + if nil != tag.Content && nil != tag.Content.Producer { + return tag.Content.Producer.ID + } + return "" +} + +//MacroProducerName contains definition for ProducerName Parameter +func (tag *BidderMacro) MacroProducerName(key string) string { + if nil != tag.Content && nil != tag.Content.Producer { + return tag.Content.Producer.Name + } + return "" +} + +/********************* Device *********************/ + +//MacroUserAgent contains definition for UserAgent Parameter +func (tag *BidderMacro) MacroUserAgent(key string) string { + if nil != tag.Request && nil != tag.Request.Device { + return tag.Request.Device.UA + } + return "" +} + +//MacroDNT contains definition for DNT Parameter +func (tag *BidderMacro) MacroDNT(key string) string { + if nil != tag.Request.Device && nil != tag.Request.Device.DNT { + return strconv.FormatInt(int64(*tag.Request.Device.DNT), intBase) + } + return "" +} + +//MacroLMT contains definition for LMT Parameter +func (tag *BidderMacro) MacroLMT(key string) string { + if nil != tag.Request.Device && nil != tag.Request.Device.Lmt { + return strconv.FormatInt(int64(*tag.Request.Device.Lmt), intBase) + } + return "" +} + +//MacroIP contains definition for IP Parameter +func (tag *BidderMacro) MacroIP(key string) string { + if nil != tag.Request && nil != tag.Request.Device { + if len(tag.Request.Device.IP) > 0 { + return tag.Request.Device.IP + } else if len(tag.Request.Device.IPv6) > 0 { + return tag.Request.Device.IPv6 + } + } + return "" +} + +//MacroDeviceType contains definition for DeviceType Parameter +func (tag *BidderMacro) MacroDeviceType(key string) string { + if nil != tag.Request.Device && tag.Request.Device.DeviceType > 0 { + return strconv.FormatInt(int64(tag.Request.Device.DeviceType), intBase) + } + return "" +} + +//MacroMake contains definition for Make Parameter +func (tag *BidderMacro) MacroMake(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.Make + } + return "" +} + +//MacroModel contains definition for Model Parameter +func (tag *BidderMacro) MacroModel(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.Model + } + return "" +} + +//MacroDeviceOS contains definition for DeviceOS Parameter +func (tag *BidderMacro) MacroDeviceOS(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.OS + } + return "" +} + +//MacroDeviceOSVersion contains definition for DeviceOSVersion Parameter +func (tag *BidderMacro) MacroDeviceOSVersion(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.OSV + } + return "" +} + +//MacroDeviceWidth contains definition for DeviceWidth Parameter +func (tag *BidderMacro) MacroDeviceWidth(key string) string { + if nil != tag.Request.Device { + return strconv.FormatInt(int64(tag.Request.Device.W), intBase) + } + return "" +} + +//MacroDeviceHeight contains definition for DeviceHeight Parameter +func (tag *BidderMacro) MacroDeviceHeight(key string) string { + if nil != tag.Request.Device { + return strconv.FormatInt(int64(tag.Request.Device.H), intBase) + } + return "" +} + +//MacroDeviceJS contains definition for DeviceJS Parameter +func (tag *BidderMacro) MacroDeviceJS(key string) string { + if nil != tag.Request.Device { + return strconv.FormatInt(int64(tag.Request.Device.JS), intBase) + } + return "" +} + +//MacroDeviceLanguage contains definition for DeviceLanguage Parameter +func (tag *BidderMacro) MacroDeviceLanguage(key string) string { + if nil != tag.Request && nil != tag.Request.Device { + return tag.Request.Device.Language + } + return "" +} + +//MacroDeviceIFA contains definition for DeviceIFA Parameter +func (tag *BidderMacro) MacroDeviceIFA(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.IFA + } + return "" +} + +//MacroDeviceIFAType contains definition for DeviceIFAType +func (tag *BidderMacro) MacroDeviceIFAType(key string) string { + if nil != tag.DeviceExt { + return tag.DeviceExt.IFAType + } + return "" +} + +//MacroDeviceDIDSHA1 contains definition for DeviceDIDSHA1 Parameter +func (tag *BidderMacro) MacroDeviceDIDSHA1(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.DIDSHA1 + } + return "" +} + +//MacroDeviceDIDMD5 contains definition for DeviceDIDMD5 Parameter +func (tag *BidderMacro) MacroDeviceDIDMD5(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.DIDMD5 + } + return "" +} + +//MacroDeviceDPIDSHA1 contains definition for DeviceDPIDSHA1 Parameter +func (tag *BidderMacro) MacroDeviceDPIDSHA1(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.DPIDSHA1 + } + return "" +} + +//MacroDeviceDPIDMD5 contains definition for DeviceDPIDMD5 Parameter +func (tag *BidderMacro) MacroDeviceDPIDMD5(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.DPIDMD5 + } + return "" +} + +//MacroDeviceMACSHA1 contains definition for DeviceMACSHA1 Parameter +func (tag *BidderMacro) MacroDeviceMACSHA1(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.MACSHA1 + } + return "" +} + +//MacroDeviceMACMD5 contains definition for DeviceMACMD5 Parameter +func (tag *BidderMacro) MacroDeviceMACMD5(key string) string { + if nil != tag.Request.Device { + return tag.Request.Device.MACMD5 + } + return "" +} + +/********************* Geo *********************/ + +//MacroLatitude contains definition for Latitude Parameter +func (tag *BidderMacro) MacroLatitude(key string) string { + if tag.HasGeo { + return fmt.Sprintf("%g", tag.Request.Device.Geo.Lat) + } + return "" +} + +//MacroLongitude contains definition for Longitude Parameter +func (tag *BidderMacro) MacroLongitude(key string) string { + if tag.HasGeo { + return fmt.Sprintf("%g", tag.Request.Device.Geo.Lon) + } + return "" +} + +//MacroCountry contains definition for Country Parameter +func (tag *BidderMacro) MacroCountry(key string) string { + if tag.HasGeo { + return tag.Request.Device.Geo.Country + } + return "" +} + +//MacroRegion contains definition for Region Parameter +func (tag *BidderMacro) MacroRegion(key string) string { + if tag.HasGeo { + return tag.Request.Device.Geo.Region + } + return "" +} + +//MacroCity contains definition for City Parameter +func (tag *BidderMacro) MacroCity(key string) string { + if tag.HasGeo { + return tag.Request.Device.Geo.City + } + return "" +} + +//MacroZip contains definition for Zip Parameter +func (tag *BidderMacro) MacroZip(key string) string { + if tag.HasGeo { + return tag.Request.Device.Geo.ZIP + } + return "" +} + +//MacroUTCOffset contains definition for UTCOffset Parameter +func (tag *BidderMacro) MacroUTCOffset(key string) string { + if tag.HasGeo { + return strconv.FormatInt(tag.Request.Device.Geo.UTCOffset, intBase) + } + return "" +} + +/********************* User *********************/ + +//MacroUserID contains definition for UserID Parameter +func (tag *BidderMacro) MacroUserID(key string) string { + if nil != tag.Request.User { + return tag.Request.User.ID + } + return "" +} + +//MacroYearOfBirth contains definition for YearOfBirth Parameter +func (tag *BidderMacro) MacroYearOfBirth(key string) string { + if nil != tag.Request.User && tag.Request.User.Yob > 0 { + return strconv.FormatInt(tag.Request.User.Yob, intBase) + } + return "" +} + +//MacroGender contains definition for Gender Parameter +func (tag *BidderMacro) MacroGender(key string) string { + if nil != tag.Request.User { + return tag.Request.User.Gender + } + return "" +} + +/********************* Extension *********************/ + +//MacroGDPRConsent contains definition for GDPRConsent Parameter +func (tag *BidderMacro) MacroGDPRConsent(key string) string { + if nil != tag.UserExt { + return tag.UserExt.Consent + } + return "" +} + +//MacroGDPR contains definition for GDPR Parameter +func (tag *BidderMacro) MacroGDPR(key string) string { + if nil != tag.RegsExt && nil != tag.RegsExt.GDPR { + return strconv.FormatInt(int64(*tag.RegsExt.GDPR), intBase) + } + return "" +} + +//MacroUSPrivacy contains definition for USPrivacy Parameter +func (tag *BidderMacro) MacroUSPrivacy(key string) string { + if nil != tag.RegsExt { + return tag.RegsExt.USPrivacy + } + return "" +} + +/********************* Additional *********************/ + +//MacroCacheBuster contains definition for CacheBuster Parameter +func (tag *BidderMacro) MacroCacheBuster(key string) string { + //change implementation + return strconv.FormatInt(time.Now().UnixNano(), intBase) +} + +/********************* Request Headers *********************/ + +// setDefaultHeaders sets following default headers based on VAST protocol version +// X-device-IP; end users IP address, per VAST 4.x +// X-Forwarded-For; end users IP address, prior VAST versions +// X-Device-User-Agent; End users user agent, per VAST 4.x +// User-Agent; End users user agent, prior VAST versions +// X-Device-Referer; Referer value from the original request, per VAST 4.x +// X-device-Accept-Language, Accept-language value from the original request, per VAST 4.x +func setDefaultHeaders(tag *BidderMacro) { + // openrtb2. auction.go setDeviceImplicitly + // already populates OpenRTB bid request based on http request headers + // reusing the same information to set these headers via Macro* methods + headers := http.Header{} + ip := tag.IBidderMacro.MacroIP("") + userAgent := tag.IBidderMacro.MacroUserAgent("") + referer := tag.IBidderMacro.MacroSitePage("") + language := tag.IBidderMacro.MacroDeviceLanguage("") + + // 1 - vast 1 - 3 expected, 2 - vast 4 expected + expectedVastTags := 0 + if nil != tag.Imp && nil != tag.Imp.Video && nil != tag.Imp.Video.Protocols && len(tag.Imp.Video.Protocols) > 0 { + for _, protocol := range tag.Imp.Video.Protocols { + if protocol == adcom1.CreativeVAST40 || protocol == adcom1.CreativeVAST40Wrapper { + expectedVastTags |= 1 << 1 + } + if protocol <= adcom1.CreativeVAST30Wrapper { + expectedVastTags |= 1 << 0 + } + } + } else { + // not able to detect protocols. set all headers + expectedVastTags = 3 + } + + if expectedVastTags == 1 || expectedVastTags == 3 { + // vast prior to version 3 headers + setHeaders(headers, "X-Forwarded-For", ip) + setHeaders(headers, "User-Agent", userAgent) + } + + if expectedVastTags == 2 || expectedVastTags == 3 { + // vast 4 specific headers + setHeaders(headers, "X-device-Ip", ip) + setHeaders(headers, "X-Device-User-Agent", userAgent) + setHeaders(headers, "X-Device-Referer", referer) + setHeaders(headers, "X-Device-Accept-Language", language) + } + tag.ImpReqHeaders = headers +} + +func setHeaders(headers http.Header, key, value string) { + if len(value) > 0 { + headers.Set(key, value) + } +} + +//getAllHeaders combines default and custom headers and returns common list +//It internally calls GetHeaders() method for obtaining list of custom headers +func (tag *BidderMacro) getAllHeaders() http.Header { + setDefaultHeaders(tag) + customHeaders := tag.IBidderMacro.GetHeaders() + if nil != customHeaders { + for k, v := range customHeaders { + // custom header may contains default header key with value + // in such case custom value will be prefered + if nil != v && len(v) > 0 { + tag.ImpReqHeaders.Set(k, v[0]) + for i := 1; i < len(v); i++ { + tag.ImpReqHeaders.Add(k, v[i]) + } + } + } + } + return tag.ImpReqHeaders +} diff --git a/adapters/vastbidder/bidder_macro_test.go b/adapters/vastbidder/bidder_macro_test.go new file mode 100644 index 00000000000..2456f57c1fb --- /dev/null +++ b/adapters/vastbidder/bidder_macro_test.go @@ -0,0 +1,1265 @@ +package vastbidder + +import ( + "fmt" + "net/http" + "testing" + + "github.com/mxmCherry/openrtb/v16/adcom1" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/stretchr/testify/assert" +) + +//TestSetDefaultHeaders verifies SetDefaultHeaders +func TestSetDefaultHeaders(t *testing.T) { + type args struct { + req *openrtb2.BidRequest + } + type want struct { + headers http.Header + } + tests := []struct { + name string + args args + want want + }{ + { + name: "check all default headers", + args: args{req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + }}, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, + { + name: "nil bid request", + args: args{req: nil}, + want: want{ + headers: http.Header{}, + }, + }, + { + name: "no headers set", + args: args{req: &openrtb2.BidRequest{}}, + want: want{ + headers: http.Header{}, + }, + }, { + name: "vast 4 protocol", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{ + Protocols: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST40, + adcom1.CreativeDAAST10, + }, + }, + }, + }, + }, + }, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, { + name: "< vast 4", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{ + Protocols: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST20, + adcom1.CreativeDAAST10, + }, + }, + }, + }, + }, + }, + want: want{ + headers: http.Header{ + "X-Forwarded-For": []string{"1.1.1.1"}, + "User-Agent": []string{"user-agent"}, + }, + }, + }, { + name: "vast 4.0 and 4.0 wrapper", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{ + Protocols: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST40, + adcom1.CreativeVAST40Wrapper, + }, + }, + }, + }, + }, + }, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, + { + name: "vast 2.0 and 4.0", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{ + Protocols: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST40, + adcom1.CreativeVAST20Wrapper, + }, + }, + }, + }, + }, + }, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tag := new(BidderMacro) + tag.IBidderMacro = tag + tag.IsApp = false + tag.Request = tt.args.req + if nil != tt.args.req && nil != tt.args.req.Imp && len(tt.args.req.Imp) > 0 { + tag.Imp = &tt.args.req.Imp[0] + } + setDefaultHeaders(tag) + assert.Equal(t, tt.want.headers, tag.ImpReqHeaders) + }) + } +} + +//TestGetAllHeaders verifies default and custom headers are returned +func TestGetAllHeaders(t *testing.T) { + type args struct { + req *openrtb2.BidRequest + myBidder IBidderMacro + } + type want struct { + headers http.Header + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "Default and custom headers check", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + }, + myBidder: newMyVastBidderMacro(map[string]string{ + "my-custom-header": "some-value", + }), + }, + want: want{ + headers: http.Header{ + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + "My-Custom-Header": []string{"some-value"}, + }, + }, + }, + { + name: "override default header value", + args: args{ + req: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Page: "http://test.com/", // default header value + }, + }, + myBidder: newMyVastBidderMacro(map[string]string{ + "X-Device-Referer": "my-custom-value", + }), + }, + want: want{ + headers: http.Header{ + // http://test.com/ is not expected here as value + "X-Device-Referer": []string{"my-custom-value"}, + }, + }, + }, + { + name: "no custom headers", + args: args{ + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + }, + myBidder: newMyVastBidderMacro(nil), // nil - no custom headers + }, + want: want{ + headers: http.Header{ // expect default headers + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tag := tt.args.myBidder + tag.(*myVastBidderMacro).Request = tt.args.req + allHeaders := tag.getAllHeaders() + assert.Equal(t, tt.want.headers, allHeaders) + }) + } +} + +type myVastBidderMacro struct { + *BidderMacro + customHeaders map[string]string +} + +func newMyVastBidderMacro(customHeaders map[string]string) IBidderMacro { + obj := &myVastBidderMacro{ + BidderMacro: &BidderMacro{}, + customHeaders: customHeaders, + } + obj.IBidderMacro = obj + return obj +} + +func (tag *myVastBidderMacro) GetHeaders() http.Header { + if nil == tag.customHeaders { + return nil + } + h := http.Header{} + for k, v := range tag.customHeaders { + h.Set(k, v) + } + return h +} + +type testBidderMacro struct { + *BidderMacro +} + +func (tag *testBidderMacro) MacroCacheBuster(key string) string { + return `cachebuster` +} + +func newTestBidderMacro() IBidderMacro { + obj := &testBidderMacro{ + BidderMacro: &BidderMacro{}, + } + obj.IBidderMacro = obj + return obj +} + +func TestBidderMacro_MacroTest(t *testing.T) { + type args struct { + tag IBidderMacro + conf *config.Adapter + bidRequest *openrtb2.BidRequest + } + tests := []struct { + name string + args args + macros map[string]string + }{ + { + name: `App:EmptyBasicRequest`, + args: args{ + tag: newTestBidderMacro(), + conf: &config.Adapter{}, + bidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{}, + }, + }, + App: &openrtb2.App{ + Publisher: &openrtb2.Publisher{}, + }, + }, + }, + macros: map[string]string{ + MacroTest: ``, + MacroTimeout: ``, + MacroWhitelistSeat: ``, + MacroWhitelistLang: ``, + MacroBlockedSeat: ``, + MacroCurrency: ``, + MacroBlockedCategory: ``, + MacroBlockedAdvertiser: ``, + MacroBlockedApp: ``, + MacroFD: ``, + MacroTransactionID: ``, + MacroPaymentIDChain: ``, + MacroCoppa: ``, + MacroDisplayManager: ``, + MacroDisplayManagerVersion: ``, + MacroInterstitial: ``, + MacroTagID: ``, + MacroBidFloor: ``, + MacroBidFloorCurrency: ``, + MacroSecure: ``, + MacroPMP: ``, + MacroVideoMIMES: ``, + MacroVideoMinimumDuration: ``, + MacroVideoMaximumDuration: ``, + MacroVideoProtocols: ``, + MacroVideoPlayerWidth: ``, + MacroVideoPlayerHeight: ``, + MacroVideoStartDelay: ``, + MacroVideoPlacement: ``, + MacroVideoLinearity: ``, + MacroVideoSkip: ``, + MacroVideoSkipMinimum: ``, + MacroVideoSkipAfter: ``, + MacroVideoSequence: ``, + MacroVideoBlockedAttribute: ``, + MacroVideoMaximumExtended: ``, + MacroVideoMinimumBitRate: ``, + MacroVideoMaximumBitRate: ``, + MacroVideoBoxing: ``, + MacroVideoPlaybackMethod: ``, + MacroVideoDelivery: ``, + MacroVideoPosition: ``, + MacroVideoAPI: ``, + MacroSiteID: ``, + MacroSiteName: ``, + MacroSitePage: ``, + MacroSiteReferrer: ``, + MacroSiteSearch: ``, + MacroSiteMobile: ``, + MacroAppID: ``, + MacroAppName: ``, + MacroAppBundle: ``, + MacroAppStoreURL: ``, + MacroAppVersion: ``, + MacroAppPaid: ``, + MacroCategory: ``, + MacroDomain: ``, + MacroSectionCategory: ``, + MacroPageCategory: ``, + MacroPrivacyPolicy: ``, + MacroKeywords: ``, + MacroPubID: ``, + MacroPubName: ``, + MacroPubDomain: ``, + MacroContentID: ``, + MacroContentEpisode: ``, + MacroContentTitle: ``, + MacroContentSeries: ``, + MacroContentSeason: ``, + MacroContentArtist: ``, + MacroContentGenre: ``, + MacroContentAlbum: ``, + MacroContentISrc: ``, + MacroContentURL: ``, + MacroContentCategory: ``, + MacroContentProductionQuality: ``, + MacroContentVideoQuality: ``, + MacroContentContext: ``, + MacroContentContentRating: ``, + MacroContentUserRating: ``, + MacroContentQAGMediaRating: ``, + MacroContentKeywords: ``, + MacroContentLiveStream: ``, + MacroContentSourceRelationship: ``, + MacroContentLength: ``, + MacroContentLanguage: ``, + MacroContentEmbeddable: ``, + MacroProducerID: ``, + MacroProducerName: ``, + MacroUserAgent: ``, + MacroDNT: ``, + MacroLMT: ``, + MacroIP: ``, + MacroDeviceType: ``, + MacroMake: ``, + MacroModel: ``, + MacroDeviceOS: ``, + MacroDeviceOSVersion: ``, + MacroDeviceWidth: ``, + MacroDeviceHeight: ``, + MacroDeviceJS: ``, + MacroDeviceLanguage: ``, + MacroDeviceIFA: ``, + MacroDeviceIFAType: ``, + MacroDeviceDIDSHA1: ``, + MacroDeviceDIDMD5: ``, + MacroDeviceDPIDSHA1: ``, + MacroDeviceDPIDMD5: ``, + MacroDeviceMACSHA1: ``, + MacroDeviceMACMD5: ``, + MacroLatitude: ``, + MacroLongitude: ``, + MacroCountry: ``, + MacroRegion: ``, + MacroCity: ``, + MacroZip: ``, + MacroUTCOffset: ``, + MacroUserID: ``, + MacroYearOfBirth: ``, + MacroGender: ``, + MacroGDPRConsent: ``, + MacroGDPR: ``, + MacroUSPrivacy: ``, + MacroCacheBuster: `cachebuster`, + }, + }, + { + name: `Site:EmptyBasicRequest`, + args: args{ + tag: newTestBidderMacro(), + conf: &config.Adapter{}, + bidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + Video: &openrtb2.Video{}, + }, + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{}, + }, + }, + }, + macros: map[string]string{ + MacroTest: ``, + MacroTimeout: ``, + MacroWhitelistSeat: ``, + MacroWhitelistLang: ``, + MacroBlockedSeat: ``, + MacroCurrency: ``, + MacroBlockedCategory: ``, + MacroBlockedAdvertiser: ``, + MacroBlockedApp: ``, + MacroFD: ``, + MacroTransactionID: ``, + MacroPaymentIDChain: ``, + MacroCoppa: ``, + MacroDisplayManager: ``, + MacroDisplayManagerVersion: ``, + MacroInterstitial: ``, + MacroTagID: ``, + MacroBidFloor: ``, + MacroBidFloorCurrency: ``, + MacroSecure: ``, + MacroPMP: ``, + MacroVideoMIMES: ``, + MacroVideoMinimumDuration: ``, + MacroVideoMaximumDuration: ``, + MacroVideoProtocols: ``, + MacroVideoPlayerWidth: ``, + MacroVideoPlayerHeight: ``, + MacroVideoStartDelay: ``, + MacroVideoPlacement: ``, + MacroVideoLinearity: ``, + MacroVideoSkip: ``, + MacroVideoSkipMinimum: ``, + MacroVideoSkipAfter: ``, + MacroVideoSequence: ``, + MacroVideoBlockedAttribute: ``, + MacroVideoMaximumExtended: ``, + MacroVideoMinimumBitRate: ``, + MacroVideoMaximumBitRate: ``, + MacroVideoBoxing: ``, + MacroVideoPlaybackMethod: ``, + MacroVideoDelivery: ``, + MacroVideoPosition: ``, + MacroVideoAPI: ``, + MacroSiteID: ``, + MacroSiteName: ``, + MacroSitePage: ``, + MacroSiteReferrer: ``, + MacroSiteSearch: ``, + MacroSiteMobile: ``, + MacroAppID: ``, + MacroAppName: ``, + MacroAppBundle: ``, + MacroAppStoreURL: ``, + MacroAppVersion: ``, + MacroAppPaid: ``, + MacroCategory: ``, + MacroDomain: ``, + MacroSectionCategory: ``, + MacroPageCategory: ``, + MacroPrivacyPolicy: ``, + MacroKeywords: ``, + MacroPubID: ``, + MacroPubName: ``, + MacroPubDomain: ``, + MacroContentID: ``, + MacroContentEpisode: ``, + MacroContentTitle: ``, + MacroContentSeries: ``, + MacroContentSeason: ``, + MacroContentArtist: ``, + MacroContentGenre: ``, + MacroContentAlbum: ``, + MacroContentISrc: ``, + MacroContentURL: ``, + MacroContentCategory: ``, + MacroContentProductionQuality: ``, + MacroContentVideoQuality: ``, + MacroContentContext: ``, + MacroContentContentRating: ``, + MacroContentUserRating: ``, + MacroContentQAGMediaRating: ``, + MacroContentKeywords: ``, + MacroContentLiveStream: ``, + MacroContentSourceRelationship: ``, + MacroContentLength: ``, + MacroContentLanguage: ``, + MacroContentEmbeddable: ``, + MacroProducerID: ``, + MacroProducerName: ``, + MacroUserAgent: ``, + MacroDNT: ``, + MacroLMT: ``, + MacroIP: ``, + MacroDeviceType: ``, + MacroMake: ``, + MacroModel: ``, + MacroDeviceOS: ``, + MacroDeviceOSVersion: ``, + MacroDeviceWidth: ``, + MacroDeviceHeight: ``, + MacroDeviceJS: ``, + MacroDeviceLanguage: ``, + MacroDeviceIFA: ``, + MacroDeviceIFAType: ``, + MacroDeviceDIDSHA1: ``, + MacroDeviceDIDMD5: ``, + MacroDeviceDPIDSHA1: ``, + MacroDeviceDPIDMD5: ``, + MacroDeviceMACSHA1: ``, + MacroDeviceMACMD5: ``, + MacroLatitude: ``, + MacroLongitude: ``, + MacroCountry: ``, + MacroRegion: ``, + MacroCity: ``, + MacroZip: ``, + MacroUTCOffset: ``, + MacroUserID: ``, + MacroYearOfBirth: ``, + MacroGender: ``, + MacroGDPRConsent: ``, + MacroGDPR: ``, + MacroUSPrivacy: ``, + MacroCacheBuster: `cachebuster`, + }, + }, + { + name: `Site:RequestLevelMacros`, + args: args{ + tag: newTestBidderMacro(), + conf: &config.Adapter{}, + bidRequest: &openrtb2.BidRequest{ + Test: 1, + TMax: 1000, + WSeat: []string{`wseat-1`, `wseat-2`}, + WLang: []string{`wlang-1`, `wlang-2`}, + BSeat: []string{`bseat-1`, `bseat-2`}, + Cur: []string{`usd`, `inr`}, + BCat: []string{`bcat-1`, `bcat-2`}, + BAdv: []string{`badv-1`, `badv-2`}, + BApp: []string{`bapp-1`, `bapp-2`}, + Source: &openrtb2.Source{ + FD: 1, + TID: `source-tid`, + PChain: `source-pchain`, + }, + Regs: &openrtb2.Regs{ + COPPA: 1, + Ext: []byte(`{"gdpr":1,"us_privacy":"user-privacy"}`), + }, + Imp: []openrtb2.Imp{ + { + DisplayManager: `disp-mgr`, + DisplayManagerVer: `1.2`, + Instl: 1, + TagID: `tag-id`, + BidFloor: 3.0, + BidFloorCur: `usd`, + Secure: new(int8), + PMP: &openrtb2.PMP{ + PrivateAuction: 1, + Deals: []openrtb2.Deal{ + { + ID: `deal-1`, + BidFloor: 4.0, + BidFloorCur: `usd`, + AT: 1, + WSeat: []string{`wseat-11`, `wseat-12`}, + WADomain: []string{`wdomain-11`, `wdomain-12`}, + }, + { + ID: `deal-2`, + BidFloor: 5.0, + BidFloorCur: `inr`, + AT: 1, + WSeat: []string{`wseat-21`, `wseat-22`}, + WADomain: []string{`wdomain-21`, `wdomain-22`}, + }, + }, + }, + Video: &openrtb2.Video{ + MIMEs: []string{`mp4`, `flv`}, + MinDuration: 30, + MaxDuration: 60, + Protocols: []adcom1.MediaCreativeSubtype{adcom1.CreativeVAST30, adcom1.CreativeVAST40Wrapper}, + Protocol: adcom1.CreativeVAST40Wrapper, + W: 640, + H: 480, + StartDelay: new(adcom1.StartDelay), + Placement: adcom1.VideoInStream, + Linearity: adcom1.LinearityLinear, + Skip: new(int8), + SkipMin: 10, + SkipAfter: 5, + Sequence: 1, + BAttr: []adcom1.CreativeAttribute{adcom1.AttrAudioAuto, adcom1.AttrAudioUser}, + MaxExtended: 10, + MinBitRate: 360, + MaxBitRate: 1080, + BoxingAllowed: 1, + PlaybackMethod: []adcom1.PlaybackMethod{adcom1.PlaybackPageLoadSoundOn, adcom1.PlaybackClickSoundOn}, + PlaybackEnd: adcom1.PlaybackCompletion, + Delivery: []adcom1.DeliveryMethod{adcom1.DeliveryStreaming, adcom1.DeliveryDownload}, + Pos: new(adcom1.PlacementPosition), + API: []adcom1.APIFramework{adcom1.APIVPAID10, adcom1.APIVPAID20}, + }, + }, + }, + Site: &openrtb2.Site{ + ID: `site-id`, + Name: `site-name`, + Domain: `site-domain`, + Cat: []string{`site-cat1`, `site-cat2`}, + SectionCat: []string{`site-sec-cat1`, `site-sec-cat2`}, + PageCat: []string{`site-page-cat1`, `site-page-cat2`}, + Page: `site-page-url`, + Ref: `site-referer-url`, + Search: `site-search-keywords`, + Mobile: 1, + PrivacyPolicy: 2, + Keywords: `site-keywords`, + Publisher: &openrtb2.Publisher{ + ID: `site-pub-id`, + Name: `site-pub-name`, + Domain: `site-pub-domain`, + }, + Content: &openrtb2.Content{ + ID: `site-cnt-id`, + Episode: 2, + Title: `site-cnt-title`, + Series: `site-cnt-series`, + Season: `site-cnt-season`, + Artist: `site-cnt-artist`, + Genre: `site-cnt-genre`, + Album: `site-cnt-album`, + ISRC: `site-cnt-isrc`, + URL: `site-cnt-url`, + Cat: []string{`site-cnt-cat1`, `site-cnt-cat2`}, + ProdQ: new(adcom1.ProductionQuality), + VideoQuality: new(adcom1.ProductionQuality), + Context: adcom1.ContentVideo, + ContentRating: `1.2`, + UserRating: `2.2`, + QAGMediaRating: adcom1.MediaRatingAll, + Keywords: `site-cnt-keywords`, + LiveStream: 1, + SourceRelationship: 1, + Len: 100, + Language: `english`, + Embeddable: 1, + Producer: &openrtb2.Producer{ + ID: `site-cnt-prod-id`, + Name: `site-cnt-prod-name`, + }, + }, + }, + Device: &openrtb2.Device{ + UA: `user-agent`, + DNT: new(int8), + Lmt: new(int8), + IP: `ipv4`, + IPv6: `ipv6`, + DeviceType: adcom1.DeviceTV, + Make: `device-make`, + Model: `device-model`, + OS: `os`, + OSV: `os-version`, + H: 1024, + W: 2048, + JS: 1, + Language: `device-lang`, + ConnectionType: new(adcom1.ConnectionType), + IFA: `ifa`, + DIDSHA1: `didsha1`, + DIDMD5: `didmd5`, + DPIDSHA1: `dpidsha1`, + DPIDMD5: `dpidmd5`, + MACSHA1: `macsha1`, + MACMD5: `macmd5`, + Geo: &openrtb2.Geo{ + Lat: 1.1, + Lon: 2.2, + Country: `country`, + Region: `region`, + City: `city`, + ZIP: `zip`, + UTCOffset: 1000, + }, + Ext: []byte(`{"ifa_type":"idfa"}`), + }, + User: &openrtb2.User{ + ID: `user-id`, + Yob: 1990, + Gender: `M`, + Ext: []byte(`{"consent":"user-gdpr-consent"}`), + }, + }, + }, + macros: map[string]string{ + MacroTest: `1`, + MacroTimeout: `1000`, + MacroWhitelistSeat: `wseat-1,wseat-2`, + MacroWhitelistLang: `wlang-1,wlang-2`, + MacroBlockedSeat: `bseat-1,bseat-2`, + MacroCurrency: `usd,inr`, + MacroBlockedCategory: `bcat-1,bcat-2`, + MacroBlockedAdvertiser: `badv-1,badv-2`, + MacroBlockedApp: `bapp-1,bapp-2`, + MacroFD: `1`, + MacroTransactionID: `source-tid`, + MacroPaymentIDChain: `source-pchain`, + MacroCoppa: `1`, + MacroDisplayManager: `disp-mgr`, + MacroDisplayManagerVersion: `1.2`, + MacroInterstitial: `1`, + MacroTagID: `tag-id`, + MacroBidFloor: `3`, + MacroBidFloorCurrency: `usd`, + MacroSecure: `0`, + MacroPMP: `{"private_auction":1,"deals":[{"id":"deal-1","bidfloor":4,"bidfloorcur":"usd","at":1,"wseat":["wseat-11","wseat-12"],"wadomain":["wdomain-11","wdomain-12"]},{"id":"deal-2","bidfloor":5,"bidfloorcur":"inr","at":1,"wseat":["wseat-21","wseat-22"],"wadomain":["wdomain-21","wdomain-22"]}]}`, + MacroVideoMIMES: `mp4,flv`, + MacroVideoMinimumDuration: `30`, + MacroVideoMaximumDuration: `60`, + MacroVideoProtocols: `3,8`, + MacroVideoPlayerWidth: `640`, + MacroVideoPlayerHeight: `480`, + MacroVideoStartDelay: `0`, + MacroVideoPlacement: `1`, + MacroVideoLinearity: `1`, + MacroVideoSkip: `0`, + MacroVideoSkipMinimum: `10`, + MacroVideoSkipAfter: `5`, + MacroVideoSequence: `1`, + MacroVideoBlockedAttribute: `1,2`, + MacroVideoMaximumExtended: `10`, + MacroVideoMinimumBitRate: `360`, + MacroVideoMaximumBitRate: `1080`, + MacroVideoBoxing: `1`, + MacroVideoPlaybackMethod: `1,3`, + MacroVideoDelivery: `1,3`, + MacroVideoPosition: `0`, + MacroVideoAPI: `1,2`, + MacroSiteID: `site-id`, + MacroSiteName: `site-name`, + MacroSitePage: `site-page-url`, + MacroSiteReferrer: `site-referer-url`, + MacroSiteSearch: `site-search-keywords`, + MacroSiteMobile: `1`, + MacroAppID: ``, + MacroAppName: ``, + MacroAppBundle: ``, + MacroAppStoreURL: ``, + MacroAppVersion: ``, + MacroAppPaid: ``, + MacroCategory: `site-cat1,site-cat2`, + MacroDomain: `site-domain`, + MacroSectionCategory: `site-sec-cat1,site-sec-cat2`, + MacroPageCategory: `site-page-cat1,site-page-cat2`, + MacroPrivacyPolicy: `2`, + MacroKeywords: `site-keywords`, + MacroPubID: `site-pub-id`, + MacroPubName: `site-pub-name`, + MacroPubDomain: `site-pub-domain`, + MacroContentID: `site-cnt-id`, + MacroContentEpisode: `2`, + MacroContentTitle: `site-cnt-title`, + MacroContentSeries: `site-cnt-series`, + MacroContentSeason: `site-cnt-season`, + MacroContentArtist: `site-cnt-artist`, + MacroContentGenre: `site-cnt-genre`, + MacroContentAlbum: `site-cnt-album`, + MacroContentISrc: `site-cnt-isrc`, + MacroContentURL: `site-cnt-url`, + MacroContentCategory: `site-cnt-cat1,site-cnt-cat2`, + MacroContentProductionQuality: `0`, + MacroContentVideoQuality: `0`, + MacroContentContext: `1`, + MacroContentContentRating: `1.2`, + MacroContentUserRating: `2.2`, + MacroContentQAGMediaRating: `1`, + MacroContentKeywords: `site-cnt-keywords`, + MacroContentLiveStream: `1`, + MacroContentSourceRelationship: `1`, + MacroContentLength: `100`, + MacroContentLanguage: `english`, + MacroContentEmbeddable: `1`, + MacroProducerID: `site-cnt-prod-id`, + MacroProducerName: `site-cnt-prod-name`, + MacroUserAgent: `user-agent`, + MacroDNT: `0`, + MacroLMT: `0`, + MacroIP: `ipv4`, + MacroDeviceType: `3`, + MacroMake: `device-make`, + MacroModel: `device-model`, + MacroDeviceOS: `os`, + MacroDeviceOSVersion: `os-version`, + MacroDeviceWidth: `2048`, + MacroDeviceHeight: `1024`, + MacroDeviceJS: `1`, + MacroDeviceLanguage: `device-lang`, + MacroDeviceIFA: `ifa`, + MacroDeviceIFAType: `idfa`, + MacroDeviceDIDSHA1: `didsha1`, + MacroDeviceDIDMD5: `didmd5`, + MacroDeviceDPIDSHA1: `dpidsha1`, + MacroDeviceDPIDMD5: `dpidmd5`, + MacroDeviceMACSHA1: `macsha1`, + MacroDeviceMACMD5: `macmd5`, + MacroLatitude: `1.1`, + MacroLongitude: `2.2`, + MacroCountry: `country`, + MacroRegion: `region`, + MacroCity: `city`, + MacroZip: `zip`, + MacroUTCOffset: `1000`, + MacroUserID: `user-id`, + MacroYearOfBirth: `1990`, + MacroGender: `M`, + MacroGDPRConsent: `user-gdpr-consent`, + MacroGDPR: `1`, + MacroUSPrivacy: `user-privacy`, + MacroCacheBuster: `cachebuster`, + }, + }, + { + name: `App:RequestLevelMacros`, + args: args{ + tag: newTestBidderMacro(), + conf: &config.Adapter{}, + bidRequest: &openrtb2.BidRequest{ + Test: 1, + TMax: 1000, + WSeat: []string{`wseat-1`, `wseat-2`}, + WLang: []string{`wlang-1`, `wlang-2`}, + BSeat: []string{`bseat-1`, `bseat-2`}, + Cur: []string{`usd`, `inr`}, + BCat: []string{`bcat-1`, `bcat-2`}, + BAdv: []string{`badv-1`, `badv-2`}, + BApp: []string{`bapp-1`, `bapp-2`}, + Source: &openrtb2.Source{ + FD: 1, + TID: `source-tid`, + PChain: `source-pchain`, + }, + Regs: &openrtb2.Regs{ + COPPA: 1, + Ext: []byte(`{"gdpr":1,"us_privacy":"user-privacy"}`), + }, + Imp: []openrtb2.Imp{ + { + DisplayManager: `disp-mgr`, + DisplayManagerVer: `1.2`, + Instl: 1, + TagID: `tag-id`, + BidFloor: 3.0, + BidFloorCur: `usd`, + Secure: new(int8), + PMP: &openrtb2.PMP{ + PrivateAuction: 1, + Deals: []openrtb2.Deal{ + { + ID: `deal-1`, + BidFloor: 4.0, + BidFloorCur: `usd`, + AT: 1, + WSeat: []string{`wseat-11`, `wseat-12`}, + WADomain: []string{`wdomain-11`, `wdomain-12`}, + }, + { + ID: `deal-2`, + BidFloor: 5.0, + BidFloorCur: `inr`, + AT: 1, + WSeat: []string{`wseat-21`, `wseat-22`}, + WADomain: []string{`wdomain-21`, `wdomain-22`}, + }, + }, + }, + Video: &openrtb2.Video{ + MIMEs: []string{`mp4`, `flv`}, + MinDuration: 30, + MaxDuration: 60, + Protocols: []adcom1.MediaCreativeSubtype{adcom1.CreativeVAST30, adcom1.CreativeVAST40Wrapper}, + Protocol: adcom1.CreativeVAST40Wrapper, + W: 640, + H: 480, + StartDelay: new(adcom1.StartDelay), + Placement: adcom1.VideoInStream, + Linearity: adcom1.LinearityLinear, + Skip: new(int8), + SkipMin: 10, + SkipAfter: 5, + Sequence: 1, + BAttr: []adcom1.CreativeAttribute{adcom1.AttrAudioAuto, adcom1.AttrAudioUser}, + MaxExtended: 10, + MinBitRate: 360, + MaxBitRate: 1080, + BoxingAllowed: 1, + PlaybackMethod: []adcom1.PlaybackMethod{adcom1.PlaybackPageLoadSoundOn, adcom1.PlaybackClickSoundOn}, + PlaybackEnd: adcom1.PlaybackCompletion, + Delivery: []adcom1.DeliveryMethod{adcom1.DeliveryStreaming, adcom1.DeliveryDownload}, + Pos: new(adcom1.PlacementPosition), + API: []adcom1.APIFramework{adcom1.APIVPAID10, adcom1.APIVPAID20}, + }, + }, + }, + App: &openrtb2.App{ + ID: `app-id`, + Bundle: `app-bundle`, + StoreURL: `app-store-url`, + Ver: `app-version`, + Paid: 1, + Name: `app-name`, + Domain: `app-domain`, + Cat: []string{`app-cat1`, `app-cat2`}, + SectionCat: []string{`app-sec-cat1`, `app-sec-cat2`}, + PageCat: []string{`app-page-cat1`, `app-page-cat2`}, + PrivacyPolicy: 2, + Keywords: `app-keywords`, + Publisher: &openrtb2.Publisher{ + ID: `app-pub-id`, + Name: `app-pub-name`, + Domain: `app-pub-domain`, + }, + Content: &openrtb2.Content{ + ID: `app-cnt-id`, + Episode: 2, + Title: `app-cnt-title`, + Series: `app-cnt-series`, + Season: `app-cnt-season`, + Artist: `app-cnt-artist`, + Genre: `app-cnt-genre`, + Album: `app-cnt-album`, + ISRC: `app-cnt-isrc`, + URL: `app-cnt-url`, + Cat: []string{`app-cnt-cat1`, `app-cnt-cat2`}, + ProdQ: new(adcom1.ProductionQuality), + VideoQuality: new(adcom1.ProductionQuality), + Context: adcom1.ContentVideo, + ContentRating: `1.2`, + UserRating: `2.2`, + QAGMediaRating: adcom1.MediaRatingAll, + Keywords: `app-cnt-keywords`, + LiveStream: 1, + SourceRelationship: 1, + Len: 100, + Language: `english`, + Embeddable: 1, + Producer: &openrtb2.Producer{ + ID: `app-cnt-prod-id`, + Name: `app-cnt-prod-name`, + }, + }, + }, + Device: &openrtb2.Device{ + UA: `user-agent`, + DNT: new(int8), + Lmt: new(int8), + IPv6: `ipv6`, + DeviceType: adcom1.DeviceTV, + Make: `device-make`, + Model: `device-model`, + OS: `os`, + OSV: `os-version`, + H: 1024, + W: 2048, + JS: 1, + Language: `device-lang`, + ConnectionType: new(adcom1.ConnectionType), + IFA: `ifa`, + DIDSHA1: `didsha1`, + DIDMD5: `didmd5`, + DPIDSHA1: `dpidsha1`, + DPIDMD5: `dpidmd5`, + MACSHA1: `macsha1`, + MACMD5: `macmd5`, + Geo: &openrtb2.Geo{ + Lat: 1.1, + Lon: 2.2, + Country: `country`, + Region: `region`, + City: `city`, + ZIP: `zip`, + UTCOffset: 1000, + }, + Ext: []byte(`{"ifa_type":"idfa"}`), + }, + User: &openrtb2.User{ + ID: `user-id`, + Yob: 1990, + Gender: `M`, + Ext: []byte(`{"consent":"user-gdpr-consent"}`), + }, + }, + }, + macros: map[string]string{ + MacroTest: `1`, + MacroTimeout: `1000`, + MacroWhitelistSeat: `wseat-1,wseat-2`, + MacroWhitelistLang: `wlang-1,wlang-2`, + MacroBlockedSeat: `bseat-1,bseat-2`, + MacroCurrency: `usd,inr`, + MacroBlockedCategory: `bcat-1,bcat-2`, + MacroBlockedAdvertiser: `badv-1,badv-2`, + MacroBlockedApp: `bapp-1,bapp-2`, + MacroFD: `1`, + MacroTransactionID: `source-tid`, + MacroPaymentIDChain: `source-pchain`, + MacroCoppa: `1`, + MacroDisplayManager: `disp-mgr`, + MacroDisplayManagerVersion: `1.2`, + MacroInterstitial: `1`, + MacroTagID: `tag-id`, + MacroBidFloor: `3`, + MacroBidFloorCurrency: `usd`, + MacroSecure: `0`, + MacroPMP: `{"private_auction":1,"deals":[{"id":"deal-1","bidfloor":4,"bidfloorcur":"usd","at":1,"wseat":["wseat-11","wseat-12"],"wadomain":["wdomain-11","wdomain-12"]},{"id":"deal-2","bidfloor":5,"bidfloorcur":"inr","at":1,"wseat":["wseat-21","wseat-22"],"wadomain":["wdomain-21","wdomain-22"]}]}`, + MacroVideoMIMES: `mp4,flv`, + MacroVideoMinimumDuration: `30`, + MacroVideoMaximumDuration: `60`, + MacroVideoProtocols: `3,8`, + MacroVideoPlayerWidth: `640`, + MacroVideoPlayerHeight: `480`, + MacroVideoStartDelay: `0`, + MacroVideoPlacement: `1`, + MacroVideoLinearity: `1`, + MacroVideoSkip: `0`, + MacroVideoSkipMinimum: `10`, + MacroVideoSkipAfter: `5`, + MacroVideoSequence: `1`, + MacroVideoBlockedAttribute: `1,2`, + MacroVideoMaximumExtended: `10`, + MacroVideoMinimumBitRate: `360`, + MacroVideoMaximumBitRate: `1080`, + MacroVideoBoxing: `1`, + MacroVideoPlaybackMethod: `1,3`, + MacroVideoDelivery: `1,3`, + MacroVideoPosition: `0`, + MacroVideoAPI: `1,2`, + MacroSiteID: ``, + MacroSiteName: ``, + MacroSitePage: ``, + MacroSiteReferrer: ``, + MacroSiteSearch: ``, + MacroSiteMobile: ``, + MacroAppID: `app-id`, + MacroAppName: `app-name`, + MacroAppBundle: `app-bundle`, + MacroAppStoreURL: `app-store-url`, + MacroAppVersion: `app-version`, + MacroAppPaid: `1`, + MacroCategory: `app-cat1,app-cat2`, + MacroDomain: `app-domain`, + MacroSectionCategory: `app-sec-cat1,app-sec-cat2`, + MacroPageCategory: `app-page-cat1,app-page-cat2`, + MacroPrivacyPolicy: `2`, + MacroKeywords: `app-keywords`, + MacroPubID: `app-pub-id`, + MacroPubName: `app-pub-name`, + MacroPubDomain: `app-pub-domain`, + MacroContentID: `app-cnt-id`, + MacroContentEpisode: `2`, + MacroContentTitle: `app-cnt-title`, + MacroContentSeries: `app-cnt-series`, + MacroContentSeason: `app-cnt-season`, + MacroContentArtist: `app-cnt-artist`, + MacroContentGenre: `app-cnt-genre`, + MacroContentAlbum: `app-cnt-album`, + MacroContentISrc: `app-cnt-isrc`, + MacroContentURL: `app-cnt-url`, + MacroContentCategory: `app-cnt-cat1,app-cnt-cat2`, + MacroContentProductionQuality: `0`, + MacroContentVideoQuality: `0`, + MacroContentContext: `1`, + MacroContentContentRating: `1.2`, + MacroContentUserRating: `2.2`, + MacroContentQAGMediaRating: `1`, + MacroContentKeywords: `app-cnt-keywords`, + MacroContentLiveStream: `1`, + MacroContentSourceRelationship: `1`, + MacroContentLength: `100`, + MacroContentLanguage: `english`, + MacroContentEmbeddable: `1`, + MacroProducerID: `app-cnt-prod-id`, + MacroProducerName: `app-cnt-prod-name`, + MacroUserAgent: `user-agent`, + MacroDNT: `0`, + MacroLMT: `0`, + MacroIP: `ipv6`, + MacroDeviceType: `3`, + MacroMake: `device-make`, + MacroModel: `device-model`, + MacroDeviceOS: `os`, + MacroDeviceOSVersion: `os-version`, + MacroDeviceWidth: `2048`, + MacroDeviceHeight: `1024`, + MacroDeviceJS: `1`, + MacroDeviceLanguage: `device-lang`, + MacroDeviceIFA: `ifa`, + MacroDeviceIFAType: `idfa`, + MacroDeviceDIDSHA1: `didsha1`, + MacroDeviceDIDMD5: `didmd5`, + MacroDeviceDPIDSHA1: `dpidsha1`, + MacroDeviceDPIDMD5: `dpidmd5`, + MacroDeviceMACSHA1: `macsha1`, + MacroDeviceMACMD5: `macmd5`, + MacroLatitude: `1.1`, + MacroLongitude: `2.2`, + MacroCountry: `country`, + MacroRegion: `region`, + MacroCity: `city`, + MacroZip: `zip`, + MacroUTCOffset: `1000`, + MacroUserID: `user-id`, + MacroYearOfBirth: `1990`, + MacroGender: `M`, + MacroGDPRConsent: `user-gdpr-consent`, + MacroGDPR: `1`, + MacroUSPrivacy: `user-privacy`, + MacroCacheBuster: `cachebuster`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + macroMappings := GetDefaultMapper() + + tag := tt.args.tag + tag.InitBidRequest(tt.args.bidRequest) + tag.SetAdapterConfig(tt.args.conf) + tag.LoadImpression(&tt.args.bidRequest.Imp[0]) + + for key, result := range tt.macros { + cb, ok := macroMappings[key] + if !ok { + assert.NotEmpty(t, result) + } else { + actual := cb.callback(tag, key) + assert.Equal(t, result, actual, fmt.Sprintf("MacroFunction: %v", key)) + } + } + }) + } +} diff --git a/adapters/vastbidder/constant.go b/adapters/vastbidder/constant.go new file mode 100644 index 00000000000..fdead1f7d28 --- /dev/null +++ b/adapters/vastbidder/constant.go @@ -0,0 +1,167 @@ +package vastbidder + +const ( + intBase = 10 + comma = `,` +) + +//List of Tag Bidder Macros +const ( + //Request + MacroTest = `test` + MacroTimeout = `timeout` + MacroWhitelistSeat = `wseat` + MacroWhitelistLang = `wlang` + MacroBlockedSeat = `bseat` + MacroCurrency = `cur` + MacroBlockedCategory = `bcat` + MacroBlockedAdvertiser = `badv` + MacroBlockedApp = `bapp` + + //Source + MacroFD = `fd` + MacroTransactionID = `tid` + MacroPaymentIDChain = `pchain` + + //Regs + MacroCoppa = `coppa` + + //Impression + MacroDisplayManager = `displaymanager` + MacroDisplayManagerVersion = `displaymanagerver` + MacroInterstitial = `instl` + MacroTagID = `tagid` + MacroBidFloor = `bidfloor` + MacroBidFloorCurrency = `bidfloorcur` + MacroSecure = `secure` + MacroPMP = `pmp` + + //Video + MacroVideoMIMES = `mimes` + MacroVideoMinimumDuration = `minduration` + MacroVideoMaximumDuration = `maxduration` + MacroVideoProtocols = `protocols` + MacroVideoPlayerWidth = `playerwidth` + MacroVideoPlayerHeight = `playerheight` + MacroVideoStartDelay = `startdelay` + MacroVideoPlacement = `placement` + MacroVideoLinearity = `linearity` + MacroVideoSkip = `skip` + MacroVideoSkipMinimum = `skipmin` + MacroVideoSkipAfter = `skipafter` + MacroVideoSequence = `sequence` + MacroVideoBlockedAttribute = `battr` + MacroVideoMaximumExtended = `maxextended` + MacroVideoMinimumBitRate = `minbitrate` + MacroVideoMaximumBitRate = `maxbitrate` + MacroVideoBoxing = `boxingallowed` + MacroVideoPlaybackMethod = `playbackmethod` + MacroVideoDelivery = `delivery` + MacroVideoPosition = `position` + MacroVideoAPI = `api` + + //Site + MacroSiteID = `siteid` + MacroSiteName = `sitename` + MacroSitePage = `page` + MacroSiteReferrer = `ref` + MacroSiteSearch = `search` + MacroSiteMobile = `mobile` + + //App + MacroAppID = `appid` + MacroAppName = `appname` + MacroAppBundle = `bundle` + MacroAppStoreURL = `storeurl` + MacroAppVersion = `appver` + MacroAppPaid = `paid` + + //SiteAppCommon + MacroCategory = `cat` + MacroDomain = `domain` + MacroSectionCategory = `sectioncat` + MacroPageCategory = `pagecat` + MacroPrivacyPolicy = `privacypolicy` + MacroKeywords = `keywords` + + //Publisher + MacroPubID = `pubid` + MacroPubName = `pubname` + MacroPubDomain = `pubdomain` + + //Content + MacroContentID = `contentid` + MacroContentEpisode = `episode` + MacroContentTitle = `title` + MacroContentSeries = `series` + MacroContentSeason = `season` + MacroContentArtist = `artist` + MacroContentGenre = `genre` + MacroContentAlbum = `album` + MacroContentISrc = `isrc` + MacroContentURL = `contenturl` + MacroContentCategory = `contentcat` + MacroContentProductionQuality = `contentprodq` + MacroContentVideoQuality = `contentvideoquality` + MacroContentContext = `context` + MacroContentContentRating = `contentrating` + MacroContentUserRating = `userrating` + MacroContentQAGMediaRating = `qagmediarating` + MacroContentKeywords = `contentkeywords` + MacroContentLiveStream = `livestream` + MacroContentSourceRelationship = `sourcerelationship` + MacroContentLength = `contentlen` + MacroContentLanguage = `contentlanguage` + MacroContentEmbeddable = `contentembeddable` + + //Producer + MacroProducerID = `prodid` + MacroProducerName = `prodname` + + //Device + MacroUserAgent = `useragent` + MacroDNT = `dnt` + MacroLMT = `lmt` + MacroIP = `ip` + MacroDeviceType = `devicetype` + MacroMake = `make` + MacroModel = `model` + MacroDeviceOS = `os` + MacroDeviceOSVersion = `osv` + MacroDeviceWidth = `devicewidth` + MacroDeviceHeight = `deviceheight` + MacroDeviceJS = `js` + MacroDeviceLanguage = `lang` + MacroDeviceIFA = `ifa` + MacroDeviceIFAType = `ifa_type` + MacroDeviceDIDSHA1 = `didsha1` + MacroDeviceDIDMD5 = `didmd5` + MacroDeviceDPIDSHA1 = `dpidsha1` + MacroDeviceDPIDMD5 = `dpidmd5` + MacroDeviceMACSHA1 = `macsha1` + MacroDeviceMACMD5 = `macmd5` + + //Geo + MacroLatitude = `lat` + MacroLongitude = `lon` + MacroCountry = `country` + MacroRegion = `region` + MacroCity = `city` + MacroZip = `zip` + MacroUTCOffset = `utcoffset` + + //User + MacroUserID = `uid` + MacroYearOfBirth = `yob` + MacroGender = `gender` + + //Extension + MacroGDPRConsent = `consent` + MacroGDPR = `gdpr` + MacroUSPrivacy = `usprivacy` + + //Additional + MacroCacheBuster = `cachebuster` +) + +var ParamKeys = []string{"param1", "param2", "param3", "param4", "param5"} diff --git a/adapters/vastbidder/ibidder_macro.go b/adapters/vastbidder/ibidder_macro.go new file mode 100644 index 00000000000..72cc57f8c44 --- /dev/null +++ b/adapters/vastbidder/ibidder_macro.go @@ -0,0 +1,195 @@ +package vastbidder + +import ( + "net/http" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +//IBidderMacro interface will capture all macro definition +type IBidderMacro interface { + //Helper Function + InitBidRequest(request *openrtb2.BidRequest) + LoadImpression(imp *openrtb2.Imp) (*openrtb_ext.ExtImpVASTBidder, error) + LoadVASTTag(tag *openrtb_ext.ExtImpVASTBidderTag) + GetBidderKeys() map[string]string + SetAdapterConfig(*config.Adapter) + GetURI() string + GetHeaders() http.Header + //getAllHeaders returns default and custom heades + getAllHeaders() http.Header + + //Request + MacroTest(string) string + MacroTimeout(string) string + MacroWhitelistSeat(string) string + MacroWhitelistLang(string) string + MacroBlockedSeat(string) string + MacroCurrency(string) string + MacroBlockedCategory(string) string + MacroBlockedAdvertiser(string) string + MacroBlockedApp(string) string + + //Source + MacroFD(string) string + MacroTransactionID(string) string + MacroPaymentIDChain(string) string + + //Regs + MacroCoppa(string) string + + //Impression + MacroDisplayManager(string) string + MacroDisplayManagerVersion(string) string + MacroInterstitial(string) string + MacroTagID(string) string + MacroBidFloor(string) string + MacroBidFloorCurrency(string) string + MacroSecure(string) string + MacroPMP(string) string + + //Video + MacroVideoMIMES(string) string + MacroVideoMinimumDuration(string) string + MacroVideoMaximumDuration(string) string + MacroVideoProtocols(string) string + MacroVideoPlayerWidth(string) string + MacroVideoPlayerHeight(string) string + MacroVideoStartDelay(string) string + MacroVideoPlacement(string) string + MacroVideoLinearity(string) string + MacroVideoSkip(string) string + MacroVideoSkipMinimum(string) string + MacroVideoSkipAfter(string) string + MacroVideoSequence(string) string + MacroVideoBlockedAttribute(string) string + MacroVideoMaximumExtended(string) string + MacroVideoMinimumBitRate(string) string + MacroVideoMaximumBitRate(string) string + MacroVideoBoxing(string) string + MacroVideoPlaybackMethod(string) string + MacroVideoDelivery(string) string + MacroVideoPosition(string) string + MacroVideoAPI(string) string + + //Site + MacroSiteID(string) string + MacroSiteName(string) string + MacroSitePage(string) string + MacroSiteReferrer(string) string + MacroSiteSearch(string) string + MacroSiteMobile(string) string + + //App + MacroAppID(string) string + MacroAppName(string) string + MacroAppBundle(string) string + MacroAppStoreURL(string) string + MacroAppVersion(string) string + MacroAppPaid(string) string + + //SiteAppCommon + MacroCategory(string) string + MacroDomain(string) string + MacroSectionCategory(string) string + MacroPageCategory(string) string + MacroPrivacyPolicy(string) string + MacroKeywords(string) string + + //Publisher + MacroPubID(string) string + MacroPubName(string) string + MacroPubDomain(string) string + + //Content + MacroContentID(string) string + MacroContentEpisode(string) string + MacroContentTitle(string) string + MacroContentSeries(string) string + MacroContentSeason(string) string + MacroContentArtist(string) string + MacroContentGenre(string) string + MacroContentAlbum(string) string + MacroContentISrc(string) string + MacroContentURL(string) string + MacroContentCategory(string) string + MacroContentProductionQuality(string) string + MacroContentVideoQuality(string) string + MacroContentContext(string) string + MacroContentContentRating(string) string + MacroContentUserRating(string) string + MacroContentQAGMediaRating(string) string + MacroContentKeywords(string) string + MacroContentLiveStream(string) string + MacroContentSourceRelationship(string) string + MacroContentLength(string) string + MacroContentLanguage(string) string + MacroContentEmbeddable(string) string + + //Producer + MacroProducerID(string) string + MacroProducerName(string) string + + //Device + MacroUserAgent(string) string + MacroDNT(string) string + MacroLMT(string) string + MacroIP(string) string + MacroDeviceType(string) string + MacroMake(string) string + MacroModel(string) string + MacroDeviceOS(string) string + MacroDeviceOSVersion(string) string + MacroDeviceWidth(string) string + MacroDeviceHeight(string) string + MacroDeviceJS(string) string + MacroDeviceLanguage(string) string + MacroDeviceIFA(string) string + MacroDeviceIFAType(string) string + MacroDeviceDIDSHA1(string) string + MacroDeviceDIDMD5(string) string + MacroDeviceDPIDSHA1(string) string + MacroDeviceDPIDMD5(string) string + MacroDeviceMACSHA1(string) string + MacroDeviceMACMD5(string) string + + //Geo + MacroLatitude(string) string + MacroLongitude(string) string + MacroCountry(string) string + MacroRegion(string) string + MacroCity(string) string + MacroZip(string) string + MacroUTCOffset(string) string + + //User + MacroUserID(string) string + MacroYearOfBirth(string) string + MacroGender(string) string + + //Extension + MacroGDPRConsent(string) string + MacroGDPR(string) string + MacroUSPrivacy(string) string + + //Additional + MacroCacheBuster(string) string +} + +var bidderMacroMap = map[openrtb_ext.BidderName]func() IBidderMacro{} + +//RegisterNewBidderMacro will be used by each bidder to set its respective macro IBidderMacro +func RegisterNewBidderMacro(bidder openrtb_ext.BidderName, macro func() IBidderMacro) { + bidderMacroMap[bidder] = macro +} + +//GetNewBidderMacro will return IBidderMacro of specific bidder +func GetNewBidderMacro(bidder openrtb_ext.BidderName) IBidderMacro { + callback, ok := bidderMacroMap[bidder] + if ok { + return callback() + } + return NewBidderMacro() +} diff --git a/adapters/vastbidder/itag_response_handler.go b/adapters/vastbidder/itag_response_handler.go new file mode 100644 index 00000000000..eba17be1fc5 --- /dev/null +++ b/adapters/vastbidder/itag_response_handler.go @@ -0,0 +1,43 @@ +package vastbidder + +import ( + "errors" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" +) + +//ITagRequestHandler parse bidder request +type ITagRequestHandler interface { + MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) +} + +//ITagResponseHandler parse bidder response +type ITagResponseHandler interface { + Validate(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) []error + MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) +} + +//HandlerType list of tag based response handlers +type HandlerType string + +const ( + VASTTagHandlerType HandlerType = `vasttag` +) + +//GetResponseHandler returns response handler +func GetResponseHandler(responseType HandlerType) (ITagResponseHandler, error) { + switch responseType { + case VASTTagHandlerType: + return NewVASTTagResponseHandler(), nil + } + return nil, errors.New(`Unkown Response Handler`) +} + +func GetRequestHandler(responseType HandlerType) (ITagRequestHandler, error) { + switch responseType { + case VASTTagHandlerType: + return nil, nil + } + return nil, errors.New(`Unkown Response Handler`) +} diff --git a/adapters/vastbidder/macro_processor.go b/adapters/vastbidder/macro_processor.go new file mode 100644 index 00000000000..a441893135e --- /dev/null +++ b/adapters/vastbidder/macro_processor.go @@ -0,0 +1,176 @@ +package vastbidder + +import ( + "bytes" + "net/url" + "strings" + + "github.com/golang/glog" +) + +const ( + macroPrefix string = `{` //macro prefix can not be empty + macroSuffix string = `}` //macro suffix can not be empty + macroEscapeSuffix string = `_ESC` + macroPrefixLen int = len(macroPrefix) + macroSuffixLen int = len(macroSuffix) + macroEscapeSuffixLen int = len(macroEscapeSuffix) +) + +//Flags to customize macro processing wrappers + +//MacroProcessor struct to hold openrtb request and cache values +type MacroProcessor struct { + bidderMacro IBidderMacro + mapper Mapper + macroCache map[string]string + bidderKeys map[string]string +} + +//NewMacroProcessor will process macro's of openrtb bid request +func NewMacroProcessor(bidderMacro IBidderMacro, mapper Mapper) *MacroProcessor { + return &MacroProcessor{ + bidderMacro: bidderMacro, + mapper: mapper, + macroCache: make(map[string]string), + } +} + +//SetMacro Adding Custom Macro Manually +func (mp *MacroProcessor) SetMacro(key, value string) { + mp.macroCache[key] = value +} + +//SetBidderKeys will flush and set bidder specific keys +func (mp *MacroProcessor) SetBidderKeys(keys map[string]string) { + mp.bidderKeys = keys +} + +//processKey : returns value of key macro and status found or not +func (mp *MacroProcessor) processKey(key string) (string, bool) { + var valueCallback *macroCallBack + var value string + nEscaping := 0 + tmpKey := key + found := false + + for { + //Search in macro cache + if value, found = mp.macroCache[tmpKey]; found { + break + } + + //Search for bidder keys + if nil != mp.bidderKeys { + if value, found = mp.bidderKeys[tmpKey]; found { + //default escaping of bidder keys + if len(value) > 0 && nEscaping == 0 { + //escape parameter only if _ESC is not present + value = url.QueryEscape(value) + } + break + } + } + + valueCallback, found = mp.mapper[tmpKey] + if found { + //found callback function + value = valueCallback.callback(mp.bidderMacro, tmpKey) + + //checking if default escaping needed or not + if len(value) > 0 && valueCallback.escape && nEscaping == 0 { + //escape parameter only if defaultescaping is true and _ESC is not present + value = url.QueryEscape(value) + } + + break + } else if strings.HasSuffix(tmpKey, macroEscapeSuffix) { + //escaping macro found + tmpKey = tmpKey[0 : len(tmpKey)-macroEscapeSuffixLen] + nEscaping++ + continue + } + break + } + + if found { + if len(value) > 0 { + if nEscaping > 0 { + //escaping string nEscaping times + value = escape(value, nEscaping) + } + if nil != valueCallback && valueCallback.cached { + //cached value if its cached flag is true + mp.macroCache[key] = value + } + } + } + + return value, found +} + +//Process : Substitute macros in input string +func (mp *MacroProcessor) Process(in string) (response string) { + var out bytes.Buffer + pos, start, end, size := 0, 0, 0, len(in) + + for pos < size { + //find macro prefix index + if start = strings.Index(in[pos:], macroPrefix); -1 == start { + //[prefix_not_found] append remaining string to response + out.WriteString(in[pos:]) + + //macro prefix not found + break + } + + //prefix index w.r.t original string + start = start + pos + + //append non macro prefix content + out.WriteString(in[pos:start]) + + if (end - macroSuffixLen) <= (start + macroPrefixLen) { + //string contains {{TEXT_{{MACRO}} -> it should replace it with{{TEXT_MACROVALUE + //find macro suffix index + if end = strings.Index(in[start+macroPrefixLen:], macroSuffix); -1 == end { + //[suffix_not_found] append remaining string to response + out.WriteString(in[start:]) + + // We Found First %% and Not Found Second %% But We are in between of string + break + } + + end = start + macroPrefixLen + end + macroSuffixLen + } + + //get actual macro key by removing macroPrefix and macroSuffix from key itself + key := in[start+macroPrefixLen : end-macroSuffixLen] + + //process macro + value, found := mp.processKey(key) + if found { + out.WriteString(value) + pos = end + } else { + out.WriteByte(macroPrefix[0]) + pos = start + 1 + } + //glog.Infof("\nSearch[%d] : [%d,%d,%s]", count, start, end, key) + } + response = out.String() + glog.V(3).Infof("[MACRO]:in:[%s] replaced:[%s]", in, response) + return +} + +//GetMacroKey will return macro formatted key +func GetMacroKey(key string) string { + return macroPrefix + key + macroSuffix +} + +func escape(str string, n int) string { + for ; n > 0; n-- { + str = url.QueryEscape(str) + } + return str[:] +} diff --git a/adapters/vastbidder/macro_processor_test.go b/adapters/vastbidder/macro_processor_test.go new file mode 100644 index 00000000000..2175f508753 --- /dev/null +++ b/adapters/vastbidder/macro_processor_test.go @@ -0,0 +1,304 @@ +package vastbidder + +import ( + "testing" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/stretchr/testify/assert" +) + +func TestMacroProcessor_Process(t *testing.T) { + bidRequestValues := map[string]string{ + MacroPubID: `pubID`, + MacroTagID: `tagid value`, + } + + testMacroValues := map[string]string{ + MacroPubID: `pubID`, + MacroTagID: `tagid+value`, //default escaping + MacroTagID + macroEscapeSuffix: `tagid+value`, //single escaping explicitly + MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%2Bvalue`, + } + + sampleBidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {TagID: bidRequestValues[MacroTagID]}, + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: bidRequestValues[MacroPubID], + }, + }, + } + + tests := []struct { + name string + in string + expected string + }{ + { + name: "EmptyInput", + in: "", + expected: "", + }, + { + name: "NoMacroReplacement", + in: "Hello Test No Macro", + expected: "Hello Test No Macro", + }, + { + name: "StartMacro", + in: GetMacroKey(MacroTagID) + "HELLO", + expected: testMacroValues[MacroTagID] + "HELLO", + }, + { + name: "EndMacro", + in: "HELLO" + GetMacroKey(MacroTagID), + expected: "HELLO" + testMacroValues[MacroTagID], + }, + { + name: "StartEndMacro", + in: GetMacroKey(MacroTagID) + "HELLO" + GetMacroKey(MacroTagID), + expected: testMacroValues[MacroTagID] + "HELLO" + testMacroValues[MacroTagID], + }, + { + name: "HalfStartMacro", + in: macroPrefix + GetMacroKey(MacroTagID) + "HELLO", + expected: macroPrefix + testMacroValues[MacroTagID] + "HELLO", + }, + { + name: "HalfEndMacro", + in: "HELLO" + GetMacroKey(MacroTagID) + macroSuffix, + expected: "HELLO" + testMacroValues[MacroTagID] + macroSuffix, + }, + { + name: "ConcatenatedMacro", + in: GetMacroKey(MacroTagID) + GetMacroKey(MacroTagID) + "HELLO", + expected: testMacroValues[MacroTagID] + testMacroValues[MacroTagID] + "HELLO", + }, + { + name: "IncompleteConcatenationMacro", + in: GetMacroKey(MacroTagID) + macroSuffix + "LINKHELLO", + expected: testMacroValues[MacroTagID] + macroSuffix + "LINKHELLO", + }, + { + name: "ConcatenationWithSuffixMacro", + in: GetMacroKey(MacroTagID) + macroPrefix + GetMacroKey(MacroTagID) + "HELLO", + expected: testMacroValues[MacroTagID] + macroPrefix + testMacroValues[MacroTagID] + "HELLO", + }, + { + name: "UnknownMacro", + in: GetMacroKey(`UNKNOWN`) + `ABC`, + expected: GetMacroKey(`UNKNOWN`) + `ABC`, + }, + { + name: "IncompleteMacroSuffix", + in: "START" + macroSuffix, + expected: "START" + macroSuffix, + }, + { + name: "IncompleteStartAndEnd", + in: string(macroPrefix[0]) + GetMacroKey(MacroTagID) + " Value " + GetMacroKey(MacroTagID) + string(macroSuffix[0]), + expected: string(macroPrefix[0]) + testMacroValues[MacroTagID] + " Value " + testMacroValues[MacroTagID] + string(macroSuffix[0]), + }, + { + name: "SpecialCharacter", + in: macroPrefix + MacroTagID + `\n` + macroSuffix + "Sample \"" + GetMacroKey(MacroTagID) + "\" Data", + expected: macroPrefix + MacroTagID + `\n` + macroSuffix + "Sample \"" + testMacroValues[MacroTagID] + "\" Data", + }, + { + name: "EmptyValue", + in: GetMacroKey(MacroTimeout) + "Hello", + expected: "Hello", + }, + { + name: "EscapingMacro", + in: GetMacroKey(MacroTagID), + expected: testMacroValues[MacroTagID], + }, + { + name: "SingleEscapingMacro", + in: GetMacroKey(MacroTagID + macroEscapeSuffix), + expected: testMacroValues[MacroTagID+macroEscapeSuffix], + }, + { + name: "DoubleEscapingMacro", + in: GetMacroKey(MacroTagID + macroEscapeSuffix + macroEscapeSuffix), + expected: testMacroValues[MacroTagID+macroEscapeSuffix+macroEscapeSuffix], + }, + + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderMacro := NewBidderMacro() + mapper := GetDefaultMapper() + mp := NewMacroProcessor(bidderMacro, mapper) + + //Init Bidder Macro + bidderMacro.InitBidRequest(sampleBidRequest) + bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) + + gotResponse := mp.Process(tt.in) + assert.Equal(t, tt.expected, gotResponse) + }) + } +} + +func TestMacroProcessor_processKey(t *testing.T) { + bidRequestValues := map[string]string{ + MacroPubID: `1234`, + MacroTagID: `tagid value`, + } + + testMacroValues := map[string]string{ + MacroPubID: `1234`, + MacroPubID + macroEscapeSuffix: `1234`, + MacroTagID: `tagid+value`, + MacroTagID + macroEscapeSuffix: `tagid+value`, + MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%2Bvalue`, + } + + sampleBidRequest := &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {TagID: bidRequestValues[MacroTagID]}, + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: bidRequestValues[MacroPubID], + }, + }, + } + type args struct { + cache map[string]string + key string + } + type want struct { + expected string + ok bool + cache map[string]string + } + tests := []struct { + name string + args args + want want + }{ + { + name: `emptyKey`, + args: args{}, + want: want{ + expected: "", + ok: false, + cache: map[string]string{}, + }, + }, + { + name: `cachedKeyFound`, + args: args{ + cache: map[string]string{ + MacroPubID: testMacroValues[MacroPubID], + }, + key: MacroPubID, + }, + want: want{ + expected: testMacroValues[MacroPubID], + ok: true, + cache: map[string]string{ + MacroPubID: testMacroValues[MacroPubID], + }, + }, + }, + { + name: `valueFound`, + args: args{ + key: MacroTagID, + }, + want: want{ + expected: testMacroValues[MacroTagID], + ok: true, + cache: map[string]string{}, + }, + }, + { + name: `2TimesEscaping`, + args: args{ + key: MacroTagID + macroEscapeSuffix + macroEscapeSuffix, + }, + want: want{ + expected: testMacroValues[MacroTagID+macroEscapeSuffix+macroEscapeSuffix], + ok: true, + cache: map[string]string{}, + }, + }, + { + name: `macroNotPresent`, + args: args{ + key: `Unknown`, + }, + want: want{ + expected: "", + ok: false, + cache: map[string]string{}, + }, + }, + { + name: `macroNotPresentInEscaping`, + args: args{ + key: `Unknown` + macroEscapeSuffix, + }, + want: want{ + expected: "", + ok: false, + cache: map[string]string{}, + }, + }, + { + name: `cachedKey`, + args: args{ + key: MacroPubID, + }, + want: want{ + expected: testMacroValues[MacroPubID], + ok: true, + cache: map[string]string{ + MacroPubID: testMacroValues[MacroPubID], + }, + }, + }, + { + name: `cachedEscapingKey`, + args: args{ + key: MacroPubID + macroEscapeSuffix, + }, + want: want{ + expected: testMacroValues[MacroPubID+macroEscapeSuffix], + ok: true, + cache: map[string]string{ + MacroPubID + macroEscapeSuffix: testMacroValues[MacroPubID+macroEscapeSuffix], + }, + }, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderMacro := NewBidderMacro() + mapper := GetDefaultMapper() + mp := NewMacroProcessor(bidderMacro, mapper) + + //init bidder macro + bidderMacro.InitBidRequest(sampleBidRequest) + bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) + + //init cache of macro processor + if nil != tt.args.cache { + mp.macroCache = tt.args.cache + } + + actual, ok := mp.processKey(tt.args.key) + assert.Equal(t, tt.want.expected, actual) + assert.Equal(t, tt.want.ok, ok) + assert.Equal(t, tt.want.cache, mp.macroCache) + }) + } +} diff --git a/adapters/vastbidder/mapper.go b/adapters/vastbidder/mapper.go new file mode 100644 index 00000000000..6c5b09a3771 --- /dev/null +++ b/adapters/vastbidder/mapper.go @@ -0,0 +1,182 @@ +package vastbidder + +type macroCallBack struct { + cached bool + escape bool + callback func(IBidderMacro, string) string +} + +//Mapper will map macro with its respective call back function +type Mapper map[string]*macroCallBack + +func (obj Mapper) clone() Mapper { + cloned := make(Mapper, len(obj)) + for k, v := range obj { + newCallback := *v + cloned[k] = &newCallback + } + return cloned +} + +var _defaultMapper = Mapper{ + //Request + MacroTest: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTest}, + MacroTimeout: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTimeout}, + MacroWhitelistSeat: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroWhitelistSeat}, + MacroWhitelistLang: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroWhitelistLang}, + MacroBlockedSeat: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroBlockedSeat}, + MacroCurrency: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCurrency}, + MacroBlockedCategory: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroBlockedCategory}, + MacroBlockedAdvertiser: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroBlockedAdvertiser}, + MacroBlockedApp: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroBlockedApp}, + + //Source + MacroFD: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroFD}, + MacroTransactionID: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroTransactionID}, + MacroPaymentIDChain: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroPaymentIDChain}, + + //Regs + MacroCoppa: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCoppa}, + + //Impression + MacroDisplayManager: ¯oCallBack{cached: false, escape: true, callback: IBidderMacro.MacroDisplayManager}, + MacroDisplayManagerVersion: ¯oCallBack{cached: false, escape: true, callback: IBidderMacro.MacroDisplayManagerVersion}, + MacroInterstitial: ¯oCallBack{cached: false, callback: IBidderMacro.MacroInterstitial}, + MacroTagID: ¯oCallBack{cached: false, escape: true, callback: IBidderMacro.MacroTagID}, + MacroBidFloor: ¯oCallBack{cached: false, callback: IBidderMacro.MacroBidFloor}, + MacroBidFloorCurrency: ¯oCallBack{cached: false, callback: IBidderMacro.MacroBidFloorCurrency}, + MacroSecure: ¯oCallBack{cached: false, callback: IBidderMacro.MacroSecure}, + MacroPMP: ¯oCallBack{cached: false, escape: true, callback: IBidderMacro.MacroPMP}, + + //Video + MacroVideoMIMES: ¯oCallBack{cached: false, escape: true, callback: IBidderMacro.MacroVideoMIMES}, + MacroVideoMinimumDuration: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMinimumDuration}, + MacroVideoMaximumDuration: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumDuration}, + MacroVideoProtocols: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoProtocols}, + MacroVideoPlayerWidth: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlayerWidth}, + MacroVideoPlayerHeight: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlayerHeight}, + MacroVideoStartDelay: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoStartDelay}, + MacroVideoPlacement: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlacement}, + MacroVideoLinearity: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoLinearity}, + MacroVideoSkip: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkip}, + MacroVideoSkipMinimum: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkipMinimum}, + MacroVideoSkipAfter: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkipAfter}, + MacroVideoSequence: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSequence}, + MacroVideoBlockedAttribute: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoBlockedAttribute}, + MacroVideoMaximumExtended: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumExtended}, + MacroVideoMinimumBitRate: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMinimumBitRate}, + MacroVideoMaximumBitRate: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumBitRate}, + MacroVideoBoxing: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoBoxing}, + MacroVideoPlaybackMethod: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlaybackMethod}, + MacroVideoDelivery: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoDelivery}, + MacroVideoPosition: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPosition}, + MacroVideoAPI: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoAPI}, + + //Site + MacroSiteID: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroSiteID}, + MacroSiteName: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroSiteName}, + MacroSitePage: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroSitePage}, + MacroSiteReferrer: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroSiteReferrer}, + MacroSiteSearch: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroSiteSearch}, + MacroSiteMobile: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteMobile}, + + //App + MacroAppID: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroAppID}, + MacroAppName: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroAppName}, + MacroAppBundle: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroAppBundle}, + MacroAppStoreURL: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroAppStoreURL}, + MacroAppVersion: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroAppVersion}, + MacroAppPaid: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppPaid}, + + //SiteAppCommon + MacroCategory: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroCategory}, + MacroDomain: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroDomain}, + MacroSectionCategory: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroSectionCategory}, + MacroPageCategory: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroPageCategory}, + MacroPrivacyPolicy: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPrivacyPolicy}, + MacroKeywords: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroKeywords}, + + //Publisher + MacroPubID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPubID}, + MacroPubName: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroPubName}, + MacroPubDomain: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroPubDomain}, + + //Content + MacroContentID: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentID}, + MacroContentEpisode: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentEpisode}, + MacroContentTitle: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentTitle}, + MacroContentSeries: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentSeries}, + MacroContentSeason: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentSeason}, + MacroContentArtist: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentArtist}, + MacroContentGenre: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentGenre}, + MacroContentAlbum: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentAlbum}, + MacroContentISrc: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentISrc}, + MacroContentURL: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentURL}, + MacroContentCategory: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentCategory}, + MacroContentProductionQuality: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentProductionQuality}, + MacroContentVideoQuality: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentVideoQuality}, + MacroContentContext: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentContext}, + MacroContentContentRating: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentContentRating}, + MacroContentUserRating: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentUserRating}, + MacroContentQAGMediaRating: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentQAGMediaRating}, + MacroContentKeywords: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentKeywords}, + MacroContentLiveStream: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentLiveStream}, + MacroContentSourceRelationship: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentSourceRelationship}, + MacroContentLength: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentLength}, + MacroContentLanguage: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroContentLanguage}, + MacroContentEmbeddable: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentEmbeddable}, + + //Producer + MacroProducerID: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroProducerID}, + MacroProducerName: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroProducerName}, + + //Device + MacroUserAgent: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroUserAgent}, + MacroDNT: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDNT}, + MacroLMT: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLMT}, + MacroIP: ¯oCallBack{cached: true, callback: IBidderMacro.MacroIP}, + MacroDeviceType: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceType}, + MacroMake: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroMake}, + MacroModel: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroModel}, + MacroDeviceOS: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroDeviceOS}, + MacroDeviceOSVersion: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroDeviceOSVersion}, + MacroDeviceWidth: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceWidth}, + MacroDeviceHeight: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceHeight}, + MacroDeviceJS: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceJS}, + MacroDeviceLanguage: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroDeviceLanguage}, + MacroDeviceIFA: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroDeviceIFA}, + MacroDeviceIFAType: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroDeviceIFAType}, + MacroDeviceDIDSHA1: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroDeviceDIDSHA1}, + MacroDeviceDIDMD5: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroDeviceDIDMD5}, + MacroDeviceDPIDSHA1: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroDeviceDPIDSHA1}, + MacroDeviceDPIDMD5: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroDeviceDPIDMD5}, + MacroDeviceMACSHA1: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroDeviceMACSHA1}, + MacroDeviceMACMD5: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroDeviceMACMD5}, + + //Geo + MacroLatitude: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLatitude}, + MacroLongitude: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLongitude}, + MacroCountry: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroCountry}, + MacroRegion: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroRegion}, + MacroCity: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroCity}, + MacroZip: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroZip}, + MacroUTCOffset: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUTCOffset}, + + //User + MacroUserID: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroUserID}, + MacroYearOfBirth: ¯oCallBack{cached: true, callback: IBidderMacro.MacroYearOfBirth}, + MacroGender: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroGender}, + + //Extension + MacroGDPRConsent: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroGDPRConsent}, + MacroGDPR: ¯oCallBack{cached: true, callback: IBidderMacro.MacroGDPR}, + MacroUSPrivacy: ¯oCallBack{cached: true, escape: true, callback: IBidderMacro.MacroUSPrivacy}, + + //Additional + MacroCacheBuster: ¯oCallBack{cached: false, callback: IBidderMacro.MacroCacheBuster}, +} + +//GetDefaultMapper will return clone of default Mapper function +func GetDefaultMapper() Mapper { + return _defaultMapper.clone() +} diff --git a/adapters/vastbidder/sample_spotx_macro.go.bak b/adapters/vastbidder/sample_spotx_macro.go.bak new file mode 100644 index 00000000000..8f3aafbdcc7 --- /dev/null +++ b/adapters/vastbidder/sample_spotx_macro.go.bak @@ -0,0 +1,28 @@ +package vastbidder + +import ( + "github.com/prebid/prebid-server/openrtb_ext" +) + +//SpotxMacro default implementation +type SpotxMacro struct { + *BidderMacro +} + +//NewSpotxMacro contains definition for all openrtb macro's +func NewSpotxMacro() IBidderMacro { + obj := &SpotxMacro{ + BidderMacro: &BidderMacro{}, + } + obj.IBidderMacro = obj + return obj +} + +//GetBidderKeys will set bidder level keys +func (tag *SpotxMacro) GetBidderKeys() map[string]string { + return NormalizeJSON(tag.ImpBidderExt) +} + +func init() { + RegisterNewBidderMacro(openrtb_ext.BidderSpotX, NewSpotxMacro) +} diff --git a/adapters/vastbidder/tagbidder.go b/adapters/vastbidder/tagbidder.go new file mode 100644 index 00000000000..691a68a46c0 --- /dev/null +++ b/adapters/vastbidder/tagbidder.go @@ -0,0 +1,87 @@ +package vastbidder + +import ( + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +//TagBidder is default implementation of ITagBidder +type TagBidder struct { + adapters.Bidder + bidderName openrtb_ext.BidderName + adapterConfig *config.Adapter +} + +//MakeRequests will contains default definition for processing queries +func (a *TagBidder) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + bidderMacro := GetNewBidderMacro(a.bidderName) + bidderMapper := GetDefaultMapper() + macroProcessor := NewMacroProcessor(bidderMacro, bidderMapper) + + //Setting config parameters + //bidderMacro.SetBidderConfig(a.bidderConfig) + bidderMacro.SetAdapterConfig(a.adapterConfig) + bidderMacro.InitBidRequest(request) + + requestData := []*adapters.RequestData{} + for impIndex := range request.Imp { + bidderExt, err := bidderMacro.LoadImpression(&request.Imp[impIndex]) + if nil != err { + continue + } + + //iterate each vast tags, and load vast tag + for vastTagIndex, tag := range bidderExt.Tags { + //load vasttag + bidderMacro.LoadVASTTag(tag) + + //Setting Bidder Level Keys + bidderKeys := bidderMacro.GetBidderKeys() + macroProcessor.SetBidderKeys(bidderKeys) + + uri := macroProcessor.Process(bidderMacro.GetURI()) + + // append custom headers if any + headers := bidderMacro.getAllHeaders() + + requestData = append(requestData, &adapters.RequestData{ + Params: &adapters.BidRequestParams{ + ImpIndex: impIndex, + VASTTagIndex: vastTagIndex, + }, + Method: `GET`, + Uri: uri, + Headers: headers, + }) + } + } + + return requestData, nil +} + +//MakeBids makes bids +func (a *TagBidder) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + //response validation can be done here independently + //handler, err := GetResponseHandler(a.bidderConfig.ResponseType) + handler, err := GetResponseHandler(VASTTagHandlerType) + if nil != err { + return nil, []error{err} + } + return handler.MakeBids(internalRequest, externalRequest, response) +} + +//NewTagBidder is an constructor for TagBidder +func NewTagBidder(bidderName openrtb_ext.BidderName, config config.Adapter) *TagBidder { + obj := &TagBidder{ + bidderName: bidderName, + adapterConfig: &config, + } + return obj +} + +// Builder builds a new instance of the 33Across adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { + return NewTagBidder(bidderName, config), nil +} diff --git a/adapters/vastbidder/tagbidder_test.go b/adapters/vastbidder/tagbidder_test.go new file mode 100644 index 00000000000..8af990c4143 --- /dev/null +++ b/adapters/vastbidder/tagbidder_test.go @@ -0,0 +1,150 @@ +package vastbidder + +import ( + "net/http" + "testing" + + "github.com/mxmCherry/openrtb/v16/adcom1" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +//TestMakeRequests verifies +// 1. default and custom headers are set +func TestMakeRequests(t *testing.T) { + + type args struct { + customHeaders map[string]string + req *openrtb2.BidRequest + } + type want struct { + impIDReqHeaderMap map[string]http.Header + } + tests := []struct { + name string + args args + want want + }{ + { + name: "multi_impression_req", + args: args{ + customHeaders: map[string]string{ + "my-custom-header": "custom-value", + }, + req: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + IP: "1.1.1.1", + UA: "user-agent", + Language: "en", + }, + Site: &openrtb2.Site{ + Page: "http://test.com/", + }, + Imp: []openrtb2.Imp{ + { // vast 2.0 + ID: "vast_2_0_imp_req", + Video: &openrtb2.Video{ + Protocols: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST20, + }, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + { + ID: "vast_4_0_imp_req", + Video: &openrtb2.Video{ // vast 4.0 + Protocols: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST40, + }, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + { + ID: "vast_2_0_4_0_wrapper_imp_req", + Video: &openrtb2.Video{ // vast 2 and 4.0 wrapper + Protocols: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST40Wrapper, + adcom1.CreativeVAST20, + }, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + { + ID: "other_non_vast_protocol", + Video: &openrtb2.Video{ // DAAST 1.0 + Protocols: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeDAAST10, + }, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + { + + ID: "no_protocol_field_set", + Video: &openrtb2.Video{ // vast 2 and 4.0 wrapper + Protocols: []adcom1.MediaCreativeSubtype{}, + }, + Ext: []byte(`{"bidder" :{}}`), + }, + }, + }, + }, + want: want{ + impIDReqHeaderMap: map[string]http.Header{ + "vast_2_0_imp_req": { + "X-Forwarded-For": []string{"1.1.1.1"}, + "User-Agent": []string{"user-agent"}, + "My-Custom-Header": []string{"custom-value"}, + }, + "vast_4_0_imp_req": { + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + "My-Custom-Header": []string{"custom-value"}, + }, + "vast_2_0_4_0_wrapper_imp_req": { + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + "My-Custom-Header": []string{"custom-value"}, + }, + "other_non_vast_protocol": { + "My-Custom-Header": []string{"custom-value"}, + }, // no default headers expected + "no_protocol_field_set": { // set all default headers + "X-Device-Ip": []string{"1.1.1.1"}, + "X-Forwarded-For": []string{"1.1.1.1"}, + "X-Device-User-Agent": []string{"user-agent"}, + "User-Agent": []string{"user-agent"}, + "X-Device-Referer": []string{"http://test.com/"}, + "X-Device-Accept-Language": []string{"en"}, + "My-Custom-Header": []string{"custom-value"}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bidderName := openrtb_ext.BidderName("myVastBidderMacro") + RegisterNewBidderMacro(bidderName, func() IBidderMacro { + return newMyVastBidderMacro(tt.args.customHeaders) + }) + bidder := NewTagBidder(bidderName, config.Adapter{}) + reqData, err := bidder.MakeRequests(tt.args.req, nil) + assert.Nil(t, err) + for _, req := range reqData { + impID := tt.args.req.Imp[req.Params.ImpIndex].ID + expectedHeaders := tt.want.impIDReqHeaderMap[impID] + assert.Equal(t, expectedHeaders, req.Headers, "test for - "+impID) + } + }) + } +} diff --git a/adapters/vastbidder/util.go b/adapters/vastbidder/util.go new file mode 100644 index 00000000000..8ad02535ec6 --- /dev/null +++ b/adapters/vastbidder/util.go @@ -0,0 +1,70 @@ +package vastbidder + +import ( + "bytes" + "encoding/json" + "fmt" + "math/rand" + "strconv" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func ObjectArrayToString(len int, separator string, cb func(i int) string) string { + if 0 == len { + return "" + } + + var out bytes.Buffer + for i := 0; i < len; i++ { + if out.Len() > 0 { + out.WriteString(separator) + } + out.WriteString(cb(i)) + } + return out.String() +} + +func readImpExt(impExt json.RawMessage) (*openrtb_ext.ExtImpVASTBidder, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(impExt, &bidderExt); err != nil { + return nil, err + } + + vastBidderExt := openrtb_ext.ExtImpVASTBidder{} + if err := json.Unmarshal(bidderExt.Bidder, &vastBidderExt); err != nil { + return nil, err + } + return &vastBidderExt, nil +} + +func normalizeObject(prefix string, out map[string]string, obj map[string]interface{}) { + for k, value := range obj { + key := k + if len(prefix) > 0 { + key = prefix + "." + k + } + + switch val := value.(type) { + case string: + out[key] = val + case []interface{}: //array + continue + case map[string]interface{}: //object + normalizeObject(key, out, val) + default: //all int, float + out[key] = fmt.Sprint(value) + } + } +} + +func NormalizeJSON(obj map[string]interface{}) map[string]string { + out := map[string]string{} + normalizeObject("", out, obj) + return out +} + +var GetRandomID = func() string { + return strconv.FormatInt(rand.Int63(), intBase) +} diff --git a/adapters/vastbidder/vast_tag_response_handler.go b/adapters/vastbidder/vast_tag_response_handler.go new file mode 100644 index 00000000000..c1250e7ba54 --- /dev/null +++ b/adapters/vastbidder/vast_tag_response_handler.go @@ -0,0 +1,334 @@ +package vastbidder + +import ( + "encoding/json" + "errors" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/beevik/etree" + "github.com/golang/glog" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +var durationRegExp = regexp.MustCompile(`^([01]?\d|2[0-3]):([0-5]?\d):([0-5]?\d)(\.(\d{1,3}))?$`) + +//IVASTTagResponseHandler to parse VAST Tag +type IVASTTagResponseHandler interface { + ITagResponseHandler + ParseExtension(version string, tag *etree.Element, bid *adapters.TypedBid) []error + GetStaticPrice(ext json.RawMessage) float64 +} + +//VASTTagResponseHandler to parse VAST Tag +type VASTTagResponseHandler struct { + IVASTTagResponseHandler + ImpBidderExt *openrtb_ext.ExtImpVASTBidder + VASTTag *openrtb_ext.ExtImpVASTBidderTag +} + +//NewVASTTagResponseHandler returns new object +func NewVASTTagResponseHandler() *VASTTagResponseHandler { + obj := &VASTTagResponseHandler{} + obj.IVASTTagResponseHandler = obj + return obj +} + +//Validate will return bids +func (handler *VASTTagResponseHandler) Validate(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) []error { + if response.StatusCode != http.StatusOK { + return []error{errors.New(`validation failed`)} + } + + if len(internalRequest.Imp) < externalRequest.Params.ImpIndex { + return []error{errors.New(`validation failed invalid impression index`)} + } + + impExt, err := readImpExt(internalRequest.Imp[externalRequest.Params.ImpIndex].Ext) + if nil != err { + return []error{err} + } + + if len(impExt.Tags) < externalRequest.Params.VASTTagIndex { + return []error{errors.New(`validation failed invalid vast tag index`)} + } + + //Initialise Extensions + handler.ImpBidderExt = impExt + handler.VASTTag = impExt.Tags[externalRequest.Params.VASTTagIndex] + return nil +} + +//MakeBids will return bids +func (handler *VASTTagResponseHandler) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if err := handler.IVASTTagResponseHandler.Validate(internalRequest, externalRequest, response); len(err) > 0 { + return nil, err[:] + } + + bidResponses, err := handler.vastTagToBidderResponse(internalRequest, externalRequest, response) + return bidResponses, err +} + +//ParseExtension will parse VAST XML extension object +func (handler *VASTTagResponseHandler) ParseExtension(version string, ad *etree.Element, bid *adapters.TypedBid) []error { + return nil +} + +func (handler *VASTTagResponseHandler) vastTagToBidderResponse(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errs []error + + doc := etree.NewDocument() + + //Read Document + if err := doc.ReadFromBytes(response.Body); err != nil { + errs = append(errs, err) + return nil, errs[:] + } + + //Check VAST Tag + vast := doc.Element.FindElement(`./VAST`) + if vast == nil { + errs = append(errs, errors.New("VAST Tag Not Found")) + return nil, errs[:] + } + + //Check VAST/Ad Tag + adElement := getAdElement(vast) + if nil == adElement { + errs = append(errs, errors.New("VAST/Ad Tag Not Found")) + return nil, errs[:] + } + + typedBid := &adapters.TypedBid{ + Bid: &openrtb2.Bid{}, + BidType: openrtb_ext.BidTypeVideo, + BidVideo: &openrtb_ext.ExtBidPrebidVideo{ + VASTTagID: handler.VASTTag.TagID, + }, + } + + creatives := adElement.FindElements("Creatives/Creative") + if nil != creatives { + for _, creative := range creatives { + // get creative id + typedBid.Bid.CrID = getCreativeID(creative) + + // get duration from vast creative + dur, err := getDuration(creative) + if nil != err { + // get duration from input bidder vast tag + dur = getStaticDuration(handler.VASTTag) + } + if dur > 0 { + typedBid.BidVideo.Duration = int(dur) // prebid expects int value + } + } + } + + bidResponse := &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{typedBid}, + Currency: `USD`, //TODO: Need to check how to get currency value + } + + //GetVersion + version := vast.SelectAttrValue(`version`, `2.0`) + + if err := handler.IVASTTagResponseHandler.ParseExtension(version, adElement, typedBid); len(err) > 0 { + errs = append(errs, err...) + return nil, errs[:] + } + + //if bid.price is not set in ParseExtension + if typedBid.Bid.Price <= 0 { + price, currency := getPricingDetails(version, adElement) + if price <= 0 { + price, currency = getStaticPricingDetails(handler.VASTTag) + if price <= 0 { + errs = append(errs, &errortypes.NoBidPrice{Message: "Bid Price Not Present"}) + return nil, errs[:] + } + } + typedBid.Bid.Price = price + if len(currency) > 0 { + bidResponse.Currency = currency + } + } + + typedBid.Bid.ADomain = getAdvertisers(version, adElement) + + //if bid.id is not set in ParseExtension + if len(typedBid.Bid.ID) == 0 { + typedBid.Bid.ID = GetRandomID() + } + + //if bid.impid is not set in ParseExtension + if len(typedBid.Bid.ImpID) == 0 { + typedBid.Bid.ImpID = internalRequest.Imp[externalRequest.Params.ImpIndex].ID + } + + //if bid.adm is not set in ParseExtension + if len(typedBid.Bid.AdM) == 0 { + typedBid.Bid.AdM = string(response.Body) + } + + //if bid.CrID is not set in ParseExtension + if len(typedBid.Bid.CrID) == 0 { + typedBid.Bid.CrID = "cr_" + GetRandomID() + } + + return bidResponse, nil +} + +func getAdElement(vast *etree.Element) *etree.Element { + if ad := vast.FindElement(`./Ad/Wrapper`); nil != ad { + return ad + } + if ad := vast.FindElement(`./Ad/InLine`); nil != ad { + return ad + } + return nil +} + +func getAdvertisers(vastVer string, ad *etree.Element) []string { + version, err := strconv.ParseFloat(vastVer, 64) + if err != nil { + version = 2.0 + } + + advertisers := make([]string, 0) + + switch int(version) { + case 2, 3: + for _, ext := range ad.FindElements(`./Extensions/Extension/`) { + for _, attr := range ext.Attr { + if attr.Key == "type" && attr.Value == "advertiser" { + for _, ele := range ext.ChildElements() { + if ele.Tag == "Advertiser" { + if strings.TrimSpace(ele.Text()) != "" { + advertisers = append(advertisers, ele.Text()) + } + } + } + } + } + } + case 4: + if ad.FindElement("./Advertiser") != nil { + adv := strings.TrimSpace(ad.FindElement("./Advertiser").Text()) + if adv != "" { + advertisers = append(advertisers, adv) + } + } + default: + glog.V(3).Infof("Handle getAdvertisers for VAST version %d", int(version)) + } + + if len(advertisers) == 0 { + return nil + } + return advertisers +} + +func getStaticPricingDetails(vastTag *openrtb_ext.ExtImpVASTBidderTag) (float64, string) { + if nil == vastTag { + return 0.0, "" + } + return vastTag.Price, "USD" +} + +func getPricingDetails(version string, ad *etree.Element) (float64, string) { + var currency string + var node *etree.Element + + if version == `2.0` { + node = ad.FindElement(`./Extensions/Extension/Price`) + } else { + node = ad.FindElement(`./Pricing`) + } + + if node == nil { + return 0.0, currency + } + + priceValue, err := strconv.ParseFloat(node.Text(), 64) + if nil != err { + return 0.0, currency + } + + currencyNode := node.SelectAttr(`currency`) + if nil != currencyNode { + currency = currencyNode.Value + } + + return priceValue, currency +} + +// getDuration extracts the duration of the bid from input creative of Linear type. +// The lookup may vary from vast version provided in the input +// returns duration in seconds or error if failed to obtained the duration. +// If multple Linear tags are present, onlyfirst one will be used +// +// It will lookup for duration only in case of creative type is Linear. +// If creative type other than Linear then this function will return error +// For Linear Creative it will lookup for Duration attribute.Duration value will be in hh:mm:ss.mmm format as per VAST specifications +// If Duration attribute not present this will return error +// +// After extracing the duration it will convert it into seconds +// +// The ad server uses the element to denote +// the intended playback duration for the video or audio component of the ad. +// Time value may be in the format HH:MM:SS.mmm where .mmm indicates milliseconds. +// Providing milliseconds is optional. +// +// Reference +// 1.https://iabtechlab.com/wp-content/uploads/2019/06/VAST_4.2_final_june26.pdf +// 2.https://iabtechlab.com/wp-content/uploads/2018/11/VAST4.1-final-Nov-8-2018.pdf +// 3.https://iabtechlab.com/wp-content/uploads/2016/05/VAST4.0_Updated_April_2016.pdf +// 4.https://iabtechlab.com/wp-content/uploads/2016/04/VASTv3_0.pdf +func getDuration(creative *etree.Element) (int, error) { + if nil == creative { + return 0, errors.New("Invalid Creative") + } + node := creative.FindElement("./Linear/Duration") + if nil == node { + return 0, errors.New("Invalid Duration") + } + duration := node.Text() + // check if milliseconds is provided + match := durationRegExp.FindStringSubmatch(duration) + if nil == match { + return 0, errors.New("Invalid Duration") + } + repl := "${1}h${2}m${3}s" + ms := match[5] + if "" != ms { + repl += "${5}ms" + } + duration = durationRegExp.ReplaceAllString(duration, repl) + dur, err := time.ParseDuration(duration) + if err != nil { + return 0, err + } + return int(dur.Seconds()), nil +} + +func getStaticDuration(vastTag *openrtb_ext.ExtImpVASTBidderTag) int { + if nil == vastTag { + return 0 + } + return vastTag.Duration +} + +//getCreativeID looks for ID inside input creative tag +func getCreativeID(creative *etree.Element) string { + if nil == creative { + return "" + } + return creative.SelectAttrValue("id", "") +} diff --git a/adapters/vastbidder/vast_tag_response_handler_test.go b/adapters/vastbidder/vast_tag_response_handler_test.go new file mode 100644 index 00000000000..dc1665bb74b --- /dev/null +++ b/adapters/vastbidder/vast_tag_response_handler_test.go @@ -0,0 +1,385 @@ +package vastbidder + +import ( + "errors" + "fmt" + "sort" + "testing" + + "github.com/beevik/etree" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestVASTTagResponseHandler_vastTagToBidderResponse(t *testing.T) { + type args struct { + internalRequest *openrtb2.BidRequest + externalRequest *adapters.RequestData + response *adapters.ResponseData + vastTag *openrtb_ext.ExtImpVASTBidderTag + } + type want struct { + bidderResponse *adapters.BidderResponse + err []error + } + tests := []struct { + name string + args args + want want + }{ + { + name: `InlinePricingNode`, + args: args{ + internalRequest: &openrtb2.BidRequest{ + ID: `request_id_1`, + Imp: []openrtb2.Imp{ + { + ID: `imp_id_1`, + }, + }, + }, + externalRequest: &adapters.RequestData{ + Params: &adapters.BidRequestParams{ + ImpIndex: 0, + }, + }, + response: &adapters.ResponseData{ + Body: []byte(` `), + }, + vastTag: &openrtb_ext.ExtImpVASTBidderTag{ + TagID: "101", + Duration: 15, + }, + }, + want: want{ + bidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: `1234`, + ImpID: `imp_id_1`, + Price: 0.05, + AdM: ` `, + CrID: "cr_1234", + }, + BidType: openrtb_ext.BidTypeVideo, + BidVideo: &openrtb_ext.ExtBidPrebidVideo{ + VASTTagID: "101", + Duration: 15, + }, + }, + }, + Currency: `USD`, + }, + }, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := NewVASTTagResponseHandler() + GetRandomID = func() string { + return `1234` + } + handler.VASTTag = tt.args.vastTag + + bidderResponse, err := handler.vastTagToBidderResponse(tt.args.internalRequest, tt.args.externalRequest, tt.args.response) + assert.Equal(t, tt.want.bidderResponse, bidderResponse) + assert.Equal(t, tt.want.err, err) + }) + } +} + +//TestGetDurationInSeconds ... +// hh:mm:ss.mmm => 3:40:43.5 => 3 hours, 40 minutes, 43 seconds and 5 milliseconds +// => 3*60*60 + 40*60 + 43 + 5*0.001 => 10800 + 2400 + 43 + 0.005 => 13243.005 +func TestGetDurationInSeconds(t *testing.T) { + type args struct { + creativeTag string // ad element + } + type want struct { + duration int // seconds (will converted from string with format as HH:MM:SS.mmm) + err error + } + tests := []struct { + name string + args args + want want + }{ + // duration validation tests + {name: "duration 00:00:25 (= 25 seconds)", want: want{duration: 25}, args: args{creativeTag: ` 00:00:25 `}}, + {name: "duration 00:00:-25 (= -25 seconds)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 00:00:-25 `}}, + {name: "duration 00:00:30.999 (= 30.990 seconds (int -> 30 seconds))", want: want{duration: 30}, args: args{creativeTag: ` 00:00:30.999 `}}, + {name: "duration 00:01:08 (1 min 8 seconds = 68 seconds)", want: want{duration: 68}, args: args{creativeTag: ` 00:01:08 `}}, + {name: "duration 02:13:12 (2 hrs 13 min 12 seconds) = 7992 seconds)", want: want{duration: 7992}, args: args{creativeTag: ` 02:13:12 `}}, + {name: "duration 3:40:43.5 (3 hrs 40 min 43 seconds 5 ms) = 6043.005 seconds (int -> 6043 seconds))", want: want{duration: 13243}, args: args{creativeTag: ` 3:40:43.5 `}}, + {name: "duration 00:00:25.0005458 (0 hrs 0 min 25 seconds 0005458 ms) - invalid max ms is 999", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 00:00:25.0005458 `}}, + {name: "invalid duration 3:13:900 (3 hrs 13 min 900 seconds) = Invalid seconds )", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 3:13:900 `}}, + {name: "invalid duration 3:13:34:44 (3 hrs 13 min 34 seconds :44=invalid) = ?? )", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 3:13:34:44 `}}, + {name: "duration = 0:0:45.038 , with milliseconds duration (0 hrs 0 min 45 seconds and 038 millseconds) = 45.038 seconds (int -> 45 seconds) )", want: want{duration: 45}, args: args{creativeTag: ` 0:0:45.038 `}}, + {name: "duration = 0:0:48.50 = 48.050 seconds (int -> 48 seconds))", want: want{duration: 48}, args: args{creativeTag: ` 0:0:48.50 `}}, + {name: "duration = 0:0:28.59 = 28.059 seconds (int -> 28 seconds))", want: want{duration: 28}, args: args{creativeTag: ` 0:0:28.59 `}}, + {name: "duration = 56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 56 `}}, + {name: "duration = :56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` :56 `}}, + {name: "duration = :56: (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` :56: `}}, + {name: "duration = ::56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` ::56 `}}, + {name: "duration = 56.445 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 56.445 `}}, + {name: "duration = a:b:c.d (no numbers)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` a:b:c.d `}}, + + // tag validations tests + {name: "Linear Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ``}}, + {name: "Companion Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ``}}, + {name: "Non-Linear Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ``}}, + {name: "Invalid Creative tag", want: want{err: errors.New("Invalid Creative")}, args: args{creativeTag: ``}}, + {name: "Nil Creative tag", want: want{err: errors.New("Invalid Creative")}, args: args{creativeTag: ""}}, + + // multiple linear tags in creative + {name: "Multiple Linear Ads within Creative", want: want{duration: 25}, args: args{creativeTag: `0:0:250:0:30`}}, + // Case sensitivity check - passing DURATION (vast is case-sensitive as per https://vastvalidator.iabtechlab.com/dash) + {name: " all caps", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `0:0:10`}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := etree.NewDocument() + doc.ReadFromString(tt.args.creativeTag) + dur, err := getDuration(doc.FindElement("./Creative")) + assert.Equal(t, tt.want.duration, dur) + assert.Equal(t, tt.want.err, err) + // if error expects 0 value for duration + if nil != err { + assert.Equal(t, 0, dur) + } + }) + } +} + +func BenchmarkGetDuration(b *testing.B) { + doc := etree.NewDocument() + doc.ReadFromString(` 0:0:56.3 `) + creative := doc.FindElement("/Creative") + for n := 0; n < b.N; n++ { + getDuration(creative) + } +} + +func TestGetCreativeId(t *testing.T) { + type args struct { + creativeTag string // ad element + } + type want struct { + id string + } + tests := []struct { + name string + args args + want want + }{ + {name: "creative tag with id", want: want{id: "233ff44"}, args: args{creativeTag: ``}}, + {name: "creative tag without id", want: want{id: ""}, args: args{creativeTag: ``}}, + {name: "no creative tag", want: want{id: ""}, args: args{creativeTag: ""}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := etree.NewDocument() + doc.ReadFromString(tt.args.creativeTag) + id := getCreativeID(doc.FindElement("./Creative")) + assert.Equal(t, tt.want.id, id) + }) + } +} + +func BenchmarkGetCreativeID(b *testing.B) { + doc := etree.NewDocument() + doc.ReadFromString(` `) + creative := doc.FindElement("/Creative") + for n := 0; n < b.N; n++ { + getCreativeID(creative) + } +} + +func TestGetAdvertisers(t *testing.T) { + tt := []struct { + name string + vastStr string + expected []string + }{ + { + name: "vast_4_with_advertiser", + vastStr: ` + + + www.iabtechlab.com + + + `, + expected: []string{"www.iabtechlab.com"}, + }, + { + name: "vast_4_without_advertiser", + vastStr: ` + + + + + `, + expected: []string{}, + }, + { + name: "vast_4_with_empty_advertiser", + vastStr: ` + + + + + + `, + expected: []string{}, + }, + { + name: "vast_2_with_single_advertiser", + vastStr: ` + + + + + google.com + + + + + `, + expected: []string{"google.com"}, + }, + { + name: "vast_2_with_two_advertiser", + vastStr: ` + + + + + google.com + + + facebook.com + + + + + `, + expected: []string{"google.com", "facebook.com"}, + }, + { + name: "vast_2_with_no_advertiser", + vastStr: ` + + + + + `, + expected: []string{}, + }, + { + name: "vast_2_with_epmty_advertiser", + vastStr: ` + + + + + + + + + + `, + expected: []string{}, + }, + { + name: "vast_3_with_single_advertiser", + vastStr: ` + + + + + google.com + + + + + `, + expected: []string{"google.com"}, + }, + { + name: "vast_3_with_two_advertiser", + vastStr: ` + + + + + google.com + + + facebook.com + + + + + `, + expected: []string{"google.com", "facebook.com"}, + }, + { + name: "vast_3_with_no_advertiser", + vastStr: ` + + + + + `, + expected: []string{}, + }, + { + name: "vast_3_with_epmty_advertiser", + vastStr: ` + + + + + + + + + + `, + expected: []string{}, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + + doc := etree.NewDocument() + if err := doc.ReadFromString(tc.vastStr); err != nil { + t.Errorf("Failed to create etree doc from string %+v", err) + } + + vastDoc := doc.FindElement("./VAST") + vastVer := vastDoc.SelectAttrValue(`version`, `2.0`) + + ad := getAdElement(vastDoc) + + result := getAdvertisers(vastVer, ad) + + sort.Strings(result) + sort.Strings(tc.expected) + + if !assert.Equal(t, len(tc.expected), len(result), fmt.Sprintf("Expected slice length - %+v \nResult slice length - %+v", len(tc.expected), len(result))) { + return + } + + for i, expected := range tc.expected { + assert.Equal(t, expected, result[i], fmt.Sprintf("Element mismatch at position %d.\nExpected - %s\nActual - %s", i, expected, result[i])) + } + }) + } +} diff --git a/config/accounts.go b/config/accounts.go index 8705b167b37..58981716036 100644 --- a/config/accounts.go +++ b/config/accounts.go @@ -32,6 +32,7 @@ type Account struct { Events Events `mapstructure:"events" json:"events"` // Don't enable this feature. It is still under developmment - https://github.com/prebid/prebid-server/issues/1725 TruncateTargetAttribute *int `mapstructure:"truncate_target_attr" json:"truncate_target_attr"` AlternateBidderCodes AlternateBidderCodes `mapstructure:"alternatebiddercodes" json:"alternatebiddercodes"` + PriceFloors AccountPriceFloors `mapstructure:"price_floors" json:"price_floors"` } // CookieSync represents the account-level defaults for the cookie sync endpoint. @@ -41,6 +42,25 @@ type CookieSync struct { DefaultCoopSync *bool `mapstructure:"default_coop_sync" json:"default_coop_sync"` } +type AccountPriceFloors struct { + Enabled bool `mapstructure:"enabled" json:"enabled"` + EnforceFloorRate int `mapstructure:"enforce_floors_rate" json:"enforce_floors_rate"` + BidAdjustment bool `mapstructure:"adjust_for_bid_adjustment" json:"adjust_for_bid_adjustment"` + EnforceDealFloors bool `mapstructure:"enforce_deal_floors" json:"enforce_deal_floors"` + UseDynamicData bool `mapstructure:"use_dynamic_data" json:"use_dynamic_data"` + Fetch AccountFloorFetch `mapstructure:"fetch" json:"fetch"` +} + +type AccountFloorFetch struct { + Enabled bool `mapstructure:"enabled" json:"enabled"` + URL string `mapstructure:"url" json:"url"` + Timeout int `mapstructure:"timeout_ms" json:"timeout_ms"` + MaxFileSize int `mapstructure:"max_file_size_kb" json:"max_file_size_kb"` + MaxRules int `mapstructure:"max_rules" json:"max_rules"` + MaxAge int `mapstructure:"max_age_sec" json:"max_age_sec"` + Period int `mapstructure:"period_sec" json:"period_sec"` +} + // AccountCCPA represents account-specific CCPA configuration type AccountCCPA struct { Enabled *bool `mapstructure:"enabled" json:"enabled,omitempty"` diff --git a/config/adapter.go b/config/adapter.go index eae47981a70..da40ff26574 100644 --- a/config/adapter.go +++ b/config/adapter.go @@ -23,6 +23,9 @@ type Adapter struct { // needed for Facebook PlatformID string `mapstructure:"platform_id"` AppSecret string `mapstructure:"app_secret"` + + // needed for commerce partners + ComParams AdapterCommerce `mapstructure:"commerceparams"` } type AdapterXAPI struct { diff --git a/config/adapter_cm.go b/config/adapter_cm.go new file mode 100644 index 00000000000..93481c06e80 --- /dev/null +++ b/config/adapter_cm.go @@ -0,0 +1,8 @@ +package config + +//Adapter level Commerce Specific parameters +type AdapterCommerce struct { + ImpTracker string `mapstructure:"impurl"` + ClickTracker string `mapstructure:"clickurl"` + ConversionTracker string `mapstructure:"conversionurl"` +} diff --git a/config/config.go b/config/config.go index 1c500adcafc..34a02adffe7 100644 --- a/config/config.go +++ b/config/config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "net/url" "reflect" "strings" @@ -97,6 +98,23 @@ type Configuration struct { HostSChainNode *openrtb2.SupplyChainNode `mapstructure:"host_schain_node"` // Experiment configures non-production ready features. Experiment Experiment `mapstructure:"experiment"` + + TrackerURL string `mapstructure:"tracker_url"` + VendorListScheduler VendorListScheduler `mapstructure:"vendor_list_scheduler"` + PriceFloors PriceFloors `mapstructure:"price_floors"` +} + +type PriceFloors struct { + Enabled bool `mapstructure:"enabled"` + UseDynamicData bool `mapstructure:"use_dynamic_data"` + EnforceFloorsRate int `mapstructure:"enforce_floors_rate"` + EnforceDealFloors bool `mapstructure:"enforce_deal_floors"` +} + +type VendorListScheduler struct { + Enabled bool `mapstructure:"enabled"` + Interval string `mapstructure:"interval"` + Timeout string `mapstructure:"timeout"` } const MIN_COOKIE_SIZE_BYTES = 500 @@ -106,6 +124,12 @@ type HTTPClient struct { MaxIdleConns int `mapstructure:"max_idle_connections"` MaxIdleConnsPerHost int `mapstructure:"max_idle_connections_per_host"` IdleConnTimeout int `mapstructure:"idle_connection_timeout_seconds"` + + TLSHandshakeTimeout int `mapstructure:"tls_handshake_timeout"` + ResponseHeaderTimeout int `mapstructure:"response_header_timeout"` + DialTimeout int `mapstructure:"dial_timeout"` + DialKeepAlive int `mapstructure:"dial_keepalive"` + InsecureSkipVerify bool `mapstructure:"insecure_skipverify"` } func (cfg *Configuration) validate(v *viper.Viper) []error { @@ -125,6 +149,7 @@ func (cfg *Configuration) validate(v *viper.Viper) []error { errs = validateAdapters(cfg.Adapters, errs) errs = cfg.Debug.validate(errs) errs = cfg.ExtCacheURL.validate(errs) + errs = cfg.AccountDefaults.PriceFloors.validate(errs) if cfg.AccountDefaults.Disabled { glog.Warning(`With account_defaults.disabled=true, host-defined accounts must exist and have "disabled":false. All other requests will be rejected.`) } @@ -132,6 +157,9 @@ func (cfg *Configuration) validate(v *viper.Viper) []error { glog.Warning(`account_defaults.events will currently not do anything as the feature is still under development. Please follow https://github.com/prebid/prebid-server/issues/1725 for more updates`) } errs = cfg.Experiment.validate(errs) + if cfg.PriceFloors.Enabled { + glog.Warning(`PriceFloors.Enabled will enforce floor feature which is still under development.`) + } return errs } @@ -142,6 +170,38 @@ type AuctionTimeouts struct { Max uint64 `mapstructure:"max"` } +func (pf *AccountPriceFloors) validate(errs []error) []error { + + if !(pf.EnforceFloorRate >= 0 && pf.EnforceFloorRate <= 100) { + errs = append(errs, fmt.Errorf(`account_defaults.price_floors.enforce_floors_rate should be between 0 and 100`)) + } + + if pf.Fetch.Period > pf.Fetch.MaxAge { + errs = append(errs, fmt.Errorf(`account_defaults.price_floors.fetch.period_sec should be less than account_defaults.price_floors.fetch.max_age_sec`)) + } + + if pf.Fetch.Period < 300 { + errs = append(errs, fmt.Errorf(`account_defaults.price_floors.fetch.period_sec should not be less than 300 seconds`)) + } + + if !(pf.Fetch.MaxAge > 600 && pf.Fetch.MaxAge < math.MaxInt32) { + errs = append(errs, fmt.Errorf(`account_defaults.price_floors.fetch.max_age_sec should not be less than 600 seconds and greater than maximum integer value`)) + } + + if !(pf.Fetch.Timeout > 10 && pf.Fetch.Timeout < 10000) { + errs = append(errs, fmt.Errorf(`account_defaults.price_floors.fetch.timeout_ms should be between 10 to 10,000 mili seconds`)) + } + + if !(pf.Fetch.MaxRules >= 0 && pf.Fetch.MaxRules < math.MaxInt32) { + errs = append(errs, fmt.Errorf(`account_defaults.price_floors.fetch.max_rules should not be less than 0 seconds and greater than maximum integer value`)) + } + + if !(pf.Fetch.MaxFileSize >= 0 && pf.Fetch.MaxFileSize < math.MaxInt32) { + errs = append(errs, fmt.Errorf(`account_defaults.price_floors.fetch.max_file_size_kb should not be less than 0 seconds and greater than maximum integer value`)) + } + return errs +} + func (cfg *AuctionTimeouts) validate(errs []error) []error { if cfg.Max < cfg.Default { errs = append(errs, fmt.Errorf("auction_timeouts_ms.max cannot be less than auction_timeouts_ms.default. max=%d, default=%d", cfg.Max, cfg.Default)) @@ -742,6 +802,8 @@ func (cfg *Configuration) GetCachedAssetURL(uuid string) string { // Set the default config values for the viper object we are using. func SetupViper(v *viper.Viper, filename string) { + defer setupViperOW(v) + if filename != "" { v.SetConfigName(filename) v.AddConfigPath(".") @@ -1059,6 +1121,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.sonobi.endpoint", "https://apex.go.sonobi.com/prebid?partnerid=71d9d3d8af") v.SetDefault("adapters.sovrn.endpoint", "http://pbs.lijit.com/rtb/bid?src=prebid_server") v.SetDefault("adapters.sspbc.endpoint", "https://ssp.wp.pl/bidder/") + v.SetDefault("adapters.spotx.endpoint", "https://search.spotxchange.com/openrtb/2.3/dados") v.SetDefault("adapters.streamkey.endpoint", "http://ghb.hb.streamkey.net/pbs/ortb") v.SetDefault("adapters.stroeercore.disabled", true) v.SetDefault("adapters.stroeercore.endpoint", "http://mhb.adscale.de/s2sdsh") @@ -1087,7 +1150,12 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.yieldone.endpoint", "https://y.one.impact-ad.jp/hbs_imp") v.SetDefault("adapters.yssp.disabled", true) v.SetDefault("adapters.zeroclickfraud.endpoint", "http://{{.Host}}/openrtb2?sid={{.SourceId}}") - + v.SetDefault("adapters.koddi.endpoint","http://{{.Host}}:8001/TestCommerce") + v.SetDefault("adapters.koddi.commerceparams.impurl", "https://{{.Host}}.koddi.io/event-collection/beacon/?action=impression") + v.SetDefault("adapters.koddi.commerceparams.clickurl", "https://{{.Host}}.koddi.io/event-collection/beacon/?action=click") + v.SetDefault("adapters.koddi.commerceparams.conversionurl", "https://{{.Host}}.koddi.io/event-collection/beacon/conversion") + v.SetDefault("adapters.adbuttler.endpoint", "https://servedbyadbutler.com/adserve/;ID={{.AccountID}};setID={{.ZoneID}};type=pdb_query") + v.SetDefault("adapters.criteoretail.endpoint", "https://d.us.criteo.com/delivery/adserving") v.SetDefault("max_request_size", 1024*256) v.SetDefault("analytics.file.filename", "") v.SetDefault("analytics.pubstack.endpoint", "https://s2s.pbstck.com/v1") @@ -1141,8 +1209,21 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("blacklisted_apps", []string{""}) v.SetDefault("blacklisted_accts", []string{""}) v.SetDefault("account_required", false) + v.SetDefault("account_defaults.disabled", false) v.SetDefault("account_defaults.debug_allow", true) + v.SetDefault("account_defaults.price_floors.enabled", true) + v.SetDefault("account_defaults.price_floors.enforce_floors_rate", 100) + v.SetDefault("account_defaults.price_floors.adjust_for_bid_adjustment", true) + v.SetDefault("account_defaults.price_floors.enforce_deal_floors", false) + v.SetDefault("account_defaults.price_floors.use_dynamic_data", true) + v.SetDefault("account_defaults.price_floors.fetch.enabled", false) + v.SetDefault("account_defaults.price_floors.fetch.timeout_ms", 3000) + v.SetDefault("account_defaults.price_floors.fetch.max_file_size_kb", 100) + v.SetDefault("account_defaults.price_floors.fetch.max_rules", 1000) + v.SetDefault("account_defaults.price_floors.fetch.max_age_sec", 86400) + v.SetDefault("account_defaults.price_floors.fetch.period_sec", 3600) + v.SetDefault("certificates_file", "") v.SetDefault("auto_gen_source_tid", true) v.SetDefault("generate_bid_id", false) @@ -1168,8 +1249,8 @@ func SetupViper(v *viper.Viper, filename string) { /* Link Local: fe80::/10 /* Multicast: ff00::/8 */ - v.SetDefault("request_validation.ipv4_private_networks", []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "127.0.0.0/8"}) - v.SetDefault("request_validation.ipv6_private_networks", []string{"::1/128", "fc00::/7", "fe80::/10", "ff00::/8", "2001:db8::/32"}) + //v.SetDefault("request_validation.ipv4_private_networks", []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "127.0.0.0/8"}) + //v.SetDefault("request_validation.ipv6_private_networks", []string{"::1/128", "fc00::/7", "fe80::/10", "ff00::/8", "2001:db8::/32"}) // Set environment variable support: v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) @@ -1211,6 +1292,10 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("gdpr.tcf2.purpose_one_treatment.access_allowed", true) v.SetDefault("gdpr.tcf2.special_feature1.enforce", true) v.SetDefault("gdpr.tcf2.special_feature1.vendor_exceptions", []openrtb_ext.BidderName{}) + v.SetDefault("price_floors.enabled", false) + v.SetDefault("price_floors.use_dynamic_data", false) + v.SetDefault("price_floors.enforce_floors_rate", 100) + v.SetDefault("price_floors.enforce_deal_floors", false) // Defaults for account_defaults.events.default_url v.SetDefault("account_defaults.events.default_url", "https://PBS_HOST/event?t=##PBS-EVENTTYPE##&vtype=##PBS-VASTEVENT##&b=##PBS-BIDID##&f=i&a=##PBS-ACCOUNTID##&ts=##PBS-TIMESTAMP##&bidder=##PBS-BIDDER##&int=##PBS-INTEGRATION##&mt=##PBS-MEDIATYPE##&ch=##PBS-CHANNEL##&aid=##PBS-AUCTIONID##&l=##PBS-LINEID##") @@ -1326,3 +1411,4 @@ func isValidCookieSize(maxCookieSize int) error { } return nil } + diff --git a/config/config_ow.go b/config/config_ow.go new file mode 100644 index 00000000000..1b5dd2d8967 --- /dev/null +++ b/config/config_ow.go @@ -0,0 +1,63 @@ +package config + +import "github.com/spf13/viper" + +/* + +Better Move SetupViper() -> SetDefault() patches to pbs.yaml (app-resources.yaml) instead of setupViper() + +metrics: + disabled_metrics: + account_stored_responses: false + + +http_client: + tls_handshake_timeout: 0 + response_header_timeout: 0 + dial_timeout: 0 + dial_keepalive: 0 + +category_mapping: + filesystem: + directorypath: "/home/http/GO_SERVER/dmhbserver/static/category-mapping" + +adapters: + ix: + disabled: false + endpoint: "http://exchange.indexww.com/pbs?p=192919" + pangle: + disabled: false + endpoint: "https://api16-access-sg.pangle.io/api/ad/union/openrtb/get_ads/" + rubicon: + disabled: false + spotx: + endpoint: "https://search.spotxchange.com/openrtb/2.3/dados" + vastbidder: + endpoint: "https://test.com" + vrtcal: + endpoint: "http://rtb.vrtcal.com/bidder_prebid.vap?ssp=1812" + +gdpr: + default_value: 0 + usersync_if_ambiguous: true + +*/ + +func setupViperOW(v *viper.Viper) { + v.SetDefault("http_client.tls_handshake_timeout", 0) //no timeout + v.SetDefault("http_client.response_header_timeout", 0) //unlimited + v.SetDefault("http_client.dial_timeout", 0) //no timeout + v.SetDefault("http_client.dial_keepalive", 0) //no restriction + v.SetDefault("category_mapping.filesystem.directorypath", "/home/http/GO_SERVER/dmhbserver/static/category-mapping") + v.SetDefault("adapters.ix.disabled", false) + v.SetDefault("adapters.ix.endpoint", "http://exchange.indexww.com/pbs?p=192919") + v.SetDefault("adapters.pangle.disabled", false) + v.SetDefault("adapters.pangle.endpoint", "https://api16-access-sg.pangle.io/api/ad/union/openrtb/get_ads/") + v.SetDefault("adapters.rubicon.disabled", false) + v.SetDefault("adapters.spotx.endpoint", "https://search.spotxchange.com/openrtb/2.3/dados") + v.SetDefault("adapters.vastbidder.endpoint", "https://test.com") + v.SetDefault("adapters.vrtcal.endpoint", "http://rtb.vrtcal.com/bidder_prebid.vap?ssp=1812") + v.SetDefault("adapters.yahoossp.disabled", true) + v.SetDefault("gdpr.default_value", "0") + v.SetDefault("gdpr.usersync_if_ambiguous", true) +} diff --git a/config/config_test.go b/config/config_test.go index ca02550e019..44dd540e8ee 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,15 +2,15 @@ package config import ( "bytes" + "encoding/json" "errors" "net" "os" + "reflect" "strings" "testing" "time" - "encoding/json" - "github.com/prebid/go-gdpr/consentconstants" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" @@ -155,6 +155,24 @@ func TestDefaults(t *testing.T) { cmpInts(t, "experiment.adscert.remote.signing_timeout_ms", cfg.Experiment.AdCerts.Remote.SigningTimeoutMs, 5) cmpNils(t, "host_schain_node", cfg.HostSChainNode) + //Assert the price floor default values + cmpBools(t, "price_floors.enabled", cfg.PriceFloors.Enabled, false) + cmpBools(t, "price_floors.use_dynamic_data", cfg.PriceFloors.UseDynamicData, false) + cmpInts(t, "price_floors.enforce_floors_rate", cfg.PriceFloors.EnforceFloorsRate, 100) + cmpBools(t, "price_floors.enforce_deal_floors", cfg.PriceFloors.EnforceDealFloors, false) + + cmpBools(t, "account_defaults.price_floors.enabled", cfg.AccountDefaults.PriceFloors.Enabled, true) + cmpInts(t, "account_defaults.price_floors.enforce_floors_rate", cfg.AccountDefaults.PriceFloors.EnforceFloorRate, 100) + cmpBools(t, "account_defaults.price_floors.adjust_for_bid_adjustment", cfg.AccountDefaults.PriceFloors.BidAdjustment, true) + cmpBools(t, "account_defaults.price_floors.enforce_deal_floors", cfg.AccountDefaults.PriceFloors.EnforceDealFloors, false) + cmpBools(t, "account_defaults.price_floors.use_dynamic_data", cfg.AccountDefaults.PriceFloors.UseDynamicData, true) + cmpBools(t, "account_defaults.price_floors.fetch.enabled", cfg.AccountDefaults.PriceFloors.Fetch.Enabled, false) + cmpInts(t, "account_defaults.price_floors.fetch.timeout_ms", cfg.AccountDefaults.PriceFloors.Fetch.Timeout, 3000) + cmpInts(t, "account_defaults.price_floors.fetch.max_file_size_kb", cfg.AccountDefaults.PriceFloors.Fetch.MaxFileSize, 100) + cmpInts(t, "account_defaults.price_floors.fetch.max_rules", cfg.AccountDefaults.PriceFloors.Fetch.MaxRules, 1000) + cmpInts(t, "account_defaults.price_floors.fetch.max_age_sec", cfg.AccountDefaults.PriceFloors.Fetch.MaxAge, 86400) + cmpInts(t, "account_defaults.price_floors.fetch.period_sec", cfg.AccountDefaults.PriceFloors.Fetch.Period, 3600) + //Assert purpose VendorExceptionMap hash tables were built correctly expectedTCF2 := TCF2{ Enabled: true, @@ -402,6 +420,11 @@ experiment: remote: url: "" signing_timeout_ms: 10 +price_floors: + enabled: true + use_dynamic_data: false + enforce_floors_rate: 100 + enforce_deal_floors: true `) var adapterExtraInfoConfig = []byte(` @@ -501,6 +524,12 @@ func TestFullConfig(t *testing.T) { cmpStrings(t, "host_schain_node.rid", cfg.HostSChainNode.RID, "BidRequest") cmpInt8s(t, "host_schain_node.hp", cfg.HostSChainNode.HP, &int8One) + //Assert the price floor values + cmpBools(t, "price_floors.enabled", cfg.PriceFloors.Enabled, true) + cmpBools(t, "price_floors.use_dynamic_data", cfg.PriceFloors.UseDynamicData, false) + cmpInts(t, "price_floors.enforce_floors_rate", cfg.PriceFloors.EnforceFloorsRate, 100) + cmpBools(t, "price_floors.enforce_deal_floors", cfg.PriceFloors.EnforceDealFloors, true) + //Assert the NonStandardPublishers was correctly unmarshalled assert.Equal(t, []string{"pub1", "pub2"}, cfg.GDPR.NonStandardPublishers, "gdpr.non_standard_publishers") assert.Equal(t, map[string]struct{}{"pub1": {}, "pub2": {}}, cfg.GDPR.NonStandardPublisherMap, "gdpr.non_standard_publishers Hash Map") @@ -735,6 +764,15 @@ func TestValidateConfig(t *testing.T) { Files: FileFetcherConfig{Enabled: true}, InMemoryCache: InMemoryCache{Type: "none"}, }, + AccountDefaults: Account{ + PriceFloors: AccountPriceFloors{ + Fetch: AccountFloorFetch{ + Period: 400, + Timeout: 20, + MaxAge: 700, + }, + }, + }, } v := viper.New() @@ -1757,3 +1795,193 @@ func TestTCF2FeatureOneVendorException(t *testing.T) { assert.Equal(t, tt.wantIsVendorException, value, tt.description) } } + +func TestAccountPriceFloorsValidate(t *testing.T) { + type fields struct { + Enabled bool + EnforceFloorRate int + BidAdjustment bool + EnforceDealFloors bool + UseDynamicData bool + Fetch AccountFloorFetch + } + type args struct { + errs []error + } + tests := []struct { + name string + fields fields + args args + want []error + }{ + { + name: "Enforce Floor rate is invalid", + fields: fields{ + Enabled: true, + EnforceFloorRate: 200, + BidAdjustment: true, + EnforceDealFloors: true, + UseDynamicData: true, + Fetch: AccountFloorFetch{ + Enabled: true, + Timeout: 500, + MaxFileSize: 1, + MaxRules: 1, + MaxAge: 1000, + Period: 400, + }, + }, + args: args{ + errs: []error{}, + }, + want: []error{errors.New("account_defaults.price_floors.enforce_floors_rate should be between 0 and 100")}, + }, + { + name: "Max Age is less than Period", + fields: fields{ + Enabled: true, + EnforceFloorRate: 100, + BidAdjustment: true, + EnforceDealFloors: true, + UseDynamicData: true, + Fetch: AccountFloorFetch{ + Enabled: true, + Timeout: 500, + MaxFileSize: 1, + MaxRules: 1, + MaxAge: 700, + Period: 800, + }, + }, + args: args{ + errs: []error{}, + }, + want: []error{errors.New("account_defaults.price_floors.fetch.period_sec should be less than account_defaults.price_floors.fetch.max_age_sec")}, + }, + { + name: "Period is less than 300", + fields: fields{ + Enabled: true, + EnforceFloorRate: 100, + BidAdjustment: true, + EnforceDealFloors: true, + UseDynamicData: true, + Fetch: AccountFloorFetch{ + Enabled: true, + Timeout: 500, + MaxFileSize: 1, + MaxRules: 1, + MaxAge: 700, + Period: 200, + }, + }, + args: args{ + errs: []error{}, + }, + want: []error{errors.New("account_defaults.price_floors.fetch.period_sec should not be less than 300 seconds")}, + }, + { + name: "Invalid Max age", + fields: fields{ + Enabled: true, + EnforceFloorRate: 100, + BidAdjustment: true, + EnforceDealFloors: true, + UseDynamicData: true, + Fetch: AccountFloorFetch{ + Enabled: true, + Timeout: 500, + MaxFileSize: 1, + MaxRules: 1, + MaxAge: 500, + Period: 400, + }, + }, + args: args{ + errs: []error{}, + }, + want: []error{errors.New("account_defaults.price_floors.fetch.max_age_sec should not be less than 600 seconds and greater than maximum integer value")}, + }, + { + name: "Invalid Timeout", + fields: fields{ + Enabled: true, + EnforceFloorRate: 100, + BidAdjustment: true, + EnforceDealFloors: true, + UseDynamicData: true, + Fetch: AccountFloorFetch{ + Enabled: true, + Timeout: 1, + MaxFileSize: 1, + MaxRules: 1, + MaxAge: 700, + Period: 400, + }, + }, + args: args{ + errs: []error{}, + }, + want: []error{errors.New("account_defaults.price_floors.fetch.timeout_ms should be between 10 to 10,000 mili seconds")}, + }, + { + name: "Invalid Max rules", + fields: fields{ + Enabled: true, + EnforceFloorRate: 100, + BidAdjustment: true, + EnforceDealFloors: true, + UseDynamicData: true, + Fetch: AccountFloorFetch{ + Enabled: true, + Timeout: 11, + MaxFileSize: 1, + MaxRules: -1, + MaxAge: 700, + Period: 400, + }, + }, + args: args{ + errs: []error{}, + }, + want: []error{errors.New("account_defaults.price_floors.fetch.max_rules should not be less than 0 seconds and greater than maximum integer value")}, + }, + { + name: "Invalid Max file size", + fields: fields{ + Enabled: true, + EnforceFloorRate: 100, + BidAdjustment: true, + EnforceDealFloors: true, + UseDynamicData: true, + Fetch: AccountFloorFetch{ + Enabled: true, + Timeout: 11, + MaxFileSize: -1, + MaxRules: 1, + MaxAge: 700, + Period: 400, + }, + }, + args: args{ + errs: []error{}, + }, + want: []error{errors.New("account_defaults.price_floors.fetch.max_file_size_kb should not be less than 0 seconds and greater than maximum integer value")}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pf := &AccountPriceFloors{ + Enabled: tt.fields.Enabled, + EnforceFloorRate: tt.fields.EnforceFloorRate, + BidAdjustment: tt.fields.BidAdjustment, + EnforceDealFloors: tt.fields.EnforceDealFloors, + UseDynamicData: tt.fields.UseDynamicData, + Fetch: tt.fields.Fetch, + } + if got := pf.validate(tt.args.errs); !reflect.DeepEqual(got, tt.want) { + t.Errorf("AccountPriceFloors.validate() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/docs/developers/automated-tests.md b/docs/developers/automated-tests.md index 6814fba385c..9e435aaf57e 100644 --- a/docs/developers/automated-tests.md +++ b/docs/developers/automated-tests.md @@ -15,7 +15,7 @@ For more info on how to write tests in Go, see [the Go docs](https://golang.org/ ## Adapter Tests If your adapter makes HTTP calls using standard JSON, you should use the -[RunJSONBidderTest](https://github.com/prebid/prebid-server/blob/master/adapters/adapterstest/test_json.go#L50) function. +[RunJSONBidderTest](https://github.com/PubMatic-OpenWrap/prebid-server/blob/master/adapters/adapterstest/test_json.go#L50) function. This will be much more thorough, convenient, maintainable, and reusable than writing standard Go tests for your adapter. diff --git a/docs/developers/code-reviews.md b/docs/developers/code-reviews.md index d8ee820cd80..59455d3b8ad 100644 --- a/docs/developers/code-reviews.md +++ b/docs/developers/code-reviews.md @@ -1,9 +1,9 @@ # Code Reviews ## Standards -Anyone is free to review and comment on any [open pull requests](https://github.com/prebid/prebid-server/pulls). +Anyone is free to review and comment on any [open pull requests](https://github.com/PubMatic-OpenWrap/prebid-server/pulls). -All pull requests must be reviewed and approved by at least one [core member](https://github.com/orgs/prebid/teams/core/members) before merge. +All pull requests must be reviewed and approved by at least one [core member](https://github.com/orgs/PubMatic-OpenWrap/teams/core/members) before merge. Very small pull requests may be merged with just one review if they: @@ -38,7 +38,7 @@ Some examples include: - Can we improve the user's experience in any way? - Have the relevant [docs](..) been added or updated? If not, add the `needs docs` label. - Do you believe that the code works by looking at the unit tests? If not, suggest more tests until you do! -- Is the motivation behind these changes clear? If not, there must be [an issue](https://github.com/prebid/prebid-server/issues) explaining it. Are there better ways to achieve those goals? +- Is the motivation behind these changes clear? If not, there must be [an issue](https://github.com/PubMatic-OpenWrap/prebid-server/issues) explaining it. Are there better ways to achieve those goals? - Does the code use any global, mutable state? [Inject dependencies](https://en.wikipedia.org/wiki/Dependency_injection) instead! - Can the code be organized into smaller, more modular pieces? - Is there dead code which can be deleted? Or TODO comments which should be resolved? diff --git a/docs/developers/contributing.md b/docs/developers/contributing.md index 2a6a574ed14..cc2daaecd11 100644 --- a/docs/developers/contributing.md +++ b/docs/developers/contributing.md @@ -2,7 +2,7 @@ ## Create an issue -[Create an issue](https://github.com/prebid/prebid-server/issues/new) describing the motivation for your changes. +[Create an issue](https://github.com/PubMatic-OpenWrap/prebid-server/issues/new) describing the motivation for your changes. Are you fixing a bug? Improving documentation? Optimizing some slow code? Pull Requests without associated Issues may still be accepted, if the motivation is obvious. @@ -38,7 +38,7 @@ those updates must be submitted in the same Pull Request as the code changes. ## Open a Pull Request When you're ready, [submit a Pull Request](https://help.github.com/articles/creating-a-pull-request/) -against the `master` branch of [our GitHub repository](https://github.com/prebid/prebid-server/compare). +against the `master` branch of [our GitHub repository](https://github.com/PubMatic-OpenWrap/prebid-server/compare). Pull Requests will be vetted through GitHub Actions. To reproduce these same tests locally, do: @@ -49,5 +49,5 @@ To reproduce these same tests locally, do: If the tests pass locally, but fail on your PR, [update your fork](https://help.github.com/articles/syncing-a-fork/) with the latest code from `master`. -**Note**: We also have some [known intermittent failures](https://github.com/prebid/prebid-server/issues/103). +**Note**: We also have some [known intermittent failures](https://github.com/PubMatic-OpenWrap/prebid-server/issues/103). If the tests still fail after pulling `master`, don't worry about it. We'll re-run them when we review your PR. diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go index c6f651ca11b..2964e99406f 100644 --- a/endpoints/cookie_sync.go +++ b/endpoints/cookie_sync.go @@ -31,6 +31,7 @@ var ( errCookieSyncBody = errors.New("Failed to read request body") errCookieSyncGDPRConsentMissing = errors.New("gdpr_consent is required if gdpr=1") errCookieSyncGDPRConsentMissingSignalAmbiguous = errors.New("gdpr_consent is required. gdpr is not specified and is assumed to be 1 by the server. set gdpr=0 to exempt this request") + errCookieSyncGDPRMandatoryByHost = errors.New("gdpr_consent is required. gdpr exemption disabled by host") errCookieSyncInvalidBiddersType = errors.New("invalid bidders type. must either be a string '*' or a string array of bidders") errCookieSyncAccountBlocked = errors.New("account is disabled, please reach out to the prebid server host") errCookieSyncAccountInvalid = errors.New("account must be valid if provided, please reach out to the prebid server host") @@ -132,6 +133,11 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr return usersync.Request{}, privacy.Policies{}, err } + //OpenWrap: do not allow publishers to bypass GDPR + if c.privacyConfig.gdprConfig.DefaultValue == "1" && gdprSignal == gdpr.SignalNo { + return usersync.Request{}, privacy.Policies{}, errCookieSyncGDPRMandatoryByHost + } + if request.GDPRConsent == "" { if gdprSignal == gdpr.SignalYes { return usersync.Request{}, privacy.Policies{}, errCookieSyncGDPRConsentMissing @@ -342,10 +348,16 @@ func (c *cookieSyncEndpoint) handleResponse(w http.ResponseWriter, tf usersync.S } for _, syncerChoice := range s { - syncTypes := tf.ForBidder(syncerChoice.Bidder) + //added hack to support to old wrapper versions having indexExchange as partner + //TODO: Remove when a stable version is released + bidderName := syncerChoice.Bidder + if bidderName == "indexExchange" { + bidderName = "ix" + } + syncTypes := tf.ForBidder(bidderName) sync, err := syncerChoice.Syncer.GetSync(syncTypes, p) if err != nil { - glog.Errorf("Failed to get usersync info for %s: %v", syncerChoice.Bidder, err) + glog.Errorf("Failed to get usersync info for %s: %v", bidderName, err) continue } @@ -387,6 +399,9 @@ func mapBidderStatusToAnalytics(from []cookieSyncResponseBidder) []*analytics.Co return to } +type CookieSyncReq cookieSyncRequest +type CookieSyncResp cookieSyncResponse + type cookieSyncRequest struct { Bidders []string `json:"bidders"` GDPR *int `json:"gdpr"` diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index 171b1b24151..cdcbd9b87b1 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -682,6 +682,13 @@ func TestCookieSyncParseRequest(t *testing.T) { givenCCPAEnabled: true, expectedError: "gdpr_consent is required. gdpr is not specified and is assumed to be 1 by the server. set gdpr=0 to exempt this request", }, + { + description: "Explicit GDPR Signal 0 - Default Value 1", + givenBody: strings.NewReader(`{"gdpr": 0}`), + givenGDPRConfig: config.GDPR{Enabled: true, DefaultValue: "1"}, + givenCCPAEnabled: true, + expectedError: "gdpr_consent is required. gdpr exemption disabled by host", + }, { description: "HTTP Read Error", givenBody: ErrReader(errors.New("anyError")), diff --git a/endpoints/events/vtrack_ow.go b/endpoints/events/vtrack_ow.go new file mode 100644 index 00000000000..ef0cbd8103b --- /dev/null +++ b/endpoints/events/vtrack_ow.go @@ -0,0 +1,282 @@ +package events + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "strings" + + "github.com/beevik/etree" + "github.com/golang/glog" + "github.com/mxmCherry/openrtb/v16/adcom1" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// standard VAST macros +// https://interactiveadvertisingbureau.github.io/vast/vast4macros/vast4-macros-latest.html#macro-spec-adcount +const ( + VASTAdTypeMacro = "[ADTYPE]" + VASTAppBundleMacro = "[APPBUNDLE]" + VASTDomainMacro = "[DOMAIN]" + VASTPageURLMacro = "[PAGEURL]" + + // PBS specific macros + PBSEventIDMacro = "[EVENT_ID]" // macro for injecting PBS defined video event tracker id + //[PBS-ACCOUNT] represents publisher id / account id + PBSAccountMacro = "[PBS-ACCOUNT]" + // [PBS-BIDDER] represents bidder name + PBSBidderMacro = "[PBS-BIDDER]" + // [PBS-ORIG_BIDID] represents original bid id. + PBSOrigBidIDMacro = "[PBS-ORIG_BIDID]" + // [PBS-BIDID] represents bid id. If auction.generate-bid-id config is on, then resolve with response.seatbid.bid.ext.prebid.bidid. Else replace with response.seatbid.bid.id + PBSBidIDMacro = "[PBS-BIDID]" + // [ADERVERTISER_NAME] represents advertiser name + PBSAdvertiserNameMacro = "[ADVERTISER_NAME]" + // Pass imp.tagId using this macro + PBSAdUnitIDMacro = "[AD_UNIT]" + //PBSBidderCodeMacro represents an alias id or core bidder id. + PBSBidderCodeMacro = "[BIDDER_CODE]" +) + +var trackingEvents = []string{"start", "firstQuartile", "midpoint", "thirdQuartile", "complete"} + +// PubMatic specific event IDs +// This will go in event-config once PreBid modular design is in place +var eventIDMap = map[string]string{ + "start": "2", + "firstQuartile": "4", + "midpoint": "3", + "thirdQuartile": "5", + "complete": "6", +} + +//InjectVideoEventTrackers injects the video tracking events +//Returns VAST xml contains as first argument. Second argument indicates whether the trackers are injected and last argument indicates if there is any error in injecting the trackers +func InjectVideoEventTrackers(trackerURL, vastXML string, bid *openrtb2.Bid, prebidGenBidId, requestingBidder, bidderCoreName, accountID string, timestamp int64, bidRequest *openrtb2.BidRequest) ([]byte, bool, error) { + // parse VAST + doc := etree.NewDocument() + err := doc.ReadFromString(vastXML) + if nil != err { + err = fmt.Errorf("Error parsing VAST XML. '%v'", err.Error()) + glog.Errorf(err.Error()) + return []byte(vastXML), false, err // false indicates events trackers are not injected + } + + //Maintaining BidRequest Impression Map (Copied from exchange.go#applyCategoryMapping) + //TODO: It should be optimized by forming once and reusing + impMap := make(map[string]*openrtb2.Imp) + for i := range bidRequest.Imp { + impMap[bidRequest.Imp[i].ID] = &bidRequest.Imp[i] + } + + eventURLMap := GetVideoEventTracking(trackerURL, bid, prebidGenBidId, requestingBidder, bidderCoreName, accountID, timestamp, bidRequest, doc, impMap) + trackersInjected := false + // return if if no tracking URL + if len(eventURLMap) == 0 { + return []byte(vastXML), false, errors.New("Event URLs are not found") + } + + creatives := FindCreatives(doc) + + if adm := strings.TrimSpace(bid.AdM); adm == "" || strings.HasPrefix(adm, "http") { + // determine which creative type to be created based on linearity + if imp, ok := impMap[bid.ImpID]; ok && nil != imp.Video { + // create creative object + creatives = doc.FindElements("VAST/Ad/Wrapper/Creatives") + // var creative *etree.Element + // if len(creatives) > 0 { + // creative = creatives[0] // consider only first creative + // } else { + creative := doc.CreateElement("Creative") + creatives[0].AddChild(creative) + + // } + + switch imp.Video.Linearity { + case adcom1.LinearityLinear: + creative.AddChild(doc.CreateElement("Linear")) + case adcom1.LinearityNonLinear: + creative.AddChild(doc.CreateElement("NonLinearAds")) + default: // create both type of creatives + creative.AddChild(doc.CreateElement("Linear")) + creative.AddChild(doc.CreateElement("NonLinearAds")) + } + creatives = creative.ChildElements() // point to actual cratives + } + } + for _, creative := range creatives { + trackingEvents := creative.SelectElement("TrackingEvents") + if nil == trackingEvents { + trackingEvents = creative.CreateElement("TrackingEvents") + creative.AddChild(trackingEvents) + } + // Inject + for event, url := range eventURLMap { + trackingEle := trackingEvents.CreateElement("Tracking") + trackingEle.CreateAttr("event", event) + trackingEle.SetText(fmt.Sprintf("%s", url)) + trackersInjected = true + } + } + + out := []byte(vastXML) + var wErr error + if trackersInjected { + out, wErr = doc.WriteToBytes() + trackersInjected = trackersInjected && nil == wErr + if nil != wErr { + glog.Errorf("%v", wErr.Error()) + } + } + return out, trackersInjected, wErr +} + +// GetVideoEventTracking returns map containing key as event name value as associaed video event tracking URL +// By default PBS will expect [EVENT_ID] macro in trackerURL to inject event information +// [EVENT_ID] will be injected with one of the following values +// firstQuartile, midpoint, thirdQuartile, complete +// If your company can not use [EVENT_ID] and has its own macro. provide config.TrackerMacros implementation +// and ensure that your macro is part of trackerURL configuration +func GetVideoEventTracking(trackerURL string, bid *openrtb2.Bid, prebidGenBidId, requestingBidder string, bidderCoreName string, accountId string, timestamp int64, req *openrtb2.BidRequest, doc *etree.Document, impMap map[string]*openrtb2.Imp) map[string]string { + eventURLMap := make(map[string]string) + if "" == strings.TrimSpace(trackerURL) { + return eventURLMap + } + + // lookup custom macros + var customMacroMap map[string]string + if nil != req.Ext { + reqExt := new(openrtb_ext.ExtRequest) + err := json.Unmarshal(req.Ext, &reqExt) + if err == nil { + customMacroMap = reqExt.Prebid.Macros + } else { + glog.Warningf("Error in unmarshling req.Ext.Prebid.Vast: [%s]", err.Error()) + } + } + + for _, event := range trackingEvents { + eventURL := trackerURL + // lookup in custom macros + if nil != customMacroMap { + for customMacro, value := range customMacroMap { + eventURL = replaceMacro(eventURL, customMacro, value) + } + } + // replace standard macros + eventURL = replaceMacro(eventURL, VASTAdTypeMacro, string(openrtb_ext.BidTypeVideo)) + if nil != req && nil != req.App { + // eventURL = replaceMacro(eventURL, VASTAppBundleMacro, req.App.Bundle) + eventURL = replaceMacro(eventURL, VASTDomainMacro, req.App.Bundle) + if nil != req.App.Publisher { + eventURL = replaceMacro(eventURL, PBSAccountMacro, req.App.Publisher.ID) + } + } + if nil != req && nil != req.Site { + eventURL = replaceMacro(eventURL, VASTDomainMacro, getDomain(req.Site)) + eventURL = replaceMacro(eventURL, VASTPageURLMacro, req.Site.Page) + if nil != req.Site.Publisher { + eventURL = replaceMacro(eventURL, PBSAccountMacro, req.Site.Publisher.ID) + } + } + + domain := "" + if len(bid.ADomain) > 0 { + var err error + //eventURL = replaceMacro(eventURL, PBSAdvertiserNameMacro, strings.Join(bid.ADomain, ",")) + domain, err = extractDomain(bid.ADomain[0]) + if err != nil { + glog.Warningf("Unable to extract domain from '%s'. [%s]", bid.ADomain[0], err.Error()) + } + } + + eventURL = replaceMacro(eventURL, PBSAdvertiserNameMacro, domain) + + eventURL = replaceMacro(eventURL, PBSBidderMacro, bidderCoreName) + eventURL = replaceMacro(eventURL, PBSBidderCodeMacro, requestingBidder) + + /* Use generated bidId if present, else use bid.ID */ + if len(prebidGenBidId) > 0 && prebidGenBidId != bid.ID { + eventURL = replaceMacro(eventURL, PBSBidIDMacro, prebidGenBidId) + } else { + eventURL = replaceMacro(eventURL, PBSBidIDMacro, bid.ID) + } + eventURL = replaceMacro(eventURL, PBSOrigBidIDMacro, bid.ID) + + // replace [EVENT_ID] macro with PBS defined event ID + eventURL = replaceMacro(eventURL, PBSEventIDMacro, eventIDMap[event]) + + if imp, ok := impMap[bid.ImpID]; ok { + eventURL = replaceMacro(eventURL, PBSAdUnitIDMacro, imp.TagID) + } else { + glog.Warningf("Setting empty value for %s macro, as failed to determine imp.TagID for bid.ImpID: %s", PBSAdUnitIDMacro, bid.ImpID) + eventURL = replaceMacro(eventURL, PBSAdUnitIDMacro, "") + } + + eventURLMap[event] = eventURL + } + return eventURLMap +} + +func replaceMacro(trackerURL, macro, value string) string { + macro = strings.TrimSpace(macro) + trimmedValue := strings.TrimSpace(value) + + if strings.HasPrefix(macro, "[") && strings.HasSuffix(macro, "]") && len(trimmedValue) > 0 { + trackerURL = strings.ReplaceAll(trackerURL, macro, url.QueryEscape(value)) + } else if strings.HasPrefix(macro, "[") && strings.HasSuffix(macro, "]") && len(trimmedValue) == 0 { + trackerURL = strings.ReplaceAll(trackerURL, macro, url.QueryEscape("")) + } else { + glog.Warningf("Invalid macro '%v'. Either empty or missing prefix '[' or suffix ']", macro) + } + return trackerURL +} + +//FindCreatives finds Linear, NonLinearAds fro InLine and Wrapper Type of creatives +//from input doc - VAST Document +//NOTE: This function is temporarily seperated to reuse in ctv_auction.go. Because, in case of ctv +//we generate bid.id +func FindCreatives(doc *etree.Document) []*etree.Element { + // Find Creatives of Linear and NonLinear Type + // Injecting Tracking Events for Companion is not supported here + creatives := doc.FindElements("VAST/Ad/InLine/Creatives/Creative/Linear") + creatives = append(creatives, doc.FindElements("VAST/Ad/Wrapper/Creatives/Creative/Linear")...) + creatives = append(creatives, doc.FindElements("VAST/Ad/InLine/Creatives/Creative/NonLinearAds")...) + creatives = append(creatives, doc.FindElements("VAST/Ad/Wrapper/Creatives/Creative/NonLinearAds")...) + return creatives +} + +func extractDomain(rawURL string) (string, error) { + if !strings.HasPrefix(rawURL, "http") { + rawURL = "http://" + rawURL + } + // decode rawURL + rawURL, err := url.QueryUnescape(rawURL) + if nil != err { + return "", err + } + url, err := url.Parse(rawURL) + if nil != err { + return "", err + } + // remove www if present + return strings.TrimPrefix(url.Hostname(), "www."), nil +} + +func getDomain(site *openrtb2.Site) string { + if site.Domain != "" { + return site.Domain + } + + hostname := "" + + if site.Page != "" { + pageURL, err := url.Parse(site.Page) + if err == nil && pageURL != nil { + hostname = pageURL.Host + } + } + return hostname +} diff --git a/endpoints/events/vtrack_ow_test.go b/endpoints/events/vtrack_ow_test.go new file mode 100644 index 00000000000..99eb3dd3cbe --- /dev/null +++ b/endpoints/events/vtrack_ow_test.go @@ -0,0 +1,636 @@ +package events + +import ( + "fmt" + "net/url" + "testing" + + "github.com/beevik/etree" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/stretchr/testify/assert" +) + +func TestInjectVideoEventTrackers(t *testing.T) { + type args struct { + externalURL string + genbidID string + bid *openrtb2.Bid + req *openrtb2.BidRequest + } + type want struct { + eventURLs map[string][]string + } + tests := []struct { + name string + args args + want want + }{ + { + name: "linear_creative", + args: args{ + externalURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]", + bid: &openrtb2.Bid{ + AdM: ` + + + + http://example.com/tracking/midpoint + http://example.com/tracking/thirdQuartile + http://example.com/tracking/complete + http://partner.tracking.url + + + `, + }, + req: &openrtb2.BidRequest{App: &openrtb2.App{Bundle: "abc"}}, + }, + want: want{ + eventURLs: map[string][]string{ + // "firstQuartile": {"http://example.com/tracking/firstQuartile?k1=v1&k2=v2", "http://company.tracker.com?eventId=1004&appbundle=abc"}, + // "midpoint": {"http://example.com/tracking/midpoint", "http://company.tracker.com?eventId=1003&appbundle=abc"}, + // "thirdQuartile": {"http://example.com/tracking/thirdQuartile", "http://company.tracker.com?eventId=1005&appbundle=abc"}, + // "complete": {"http://example.com/tracking/complete", "http://company.tracker.com?eventId=1006&appbundle=abc"}, + "firstQuartile": {"http://example.com/tracking/firstQuartile?k1=v1&k2=v2", "http://company.tracker.com?eventId=4&appbundle=abc"}, + "midpoint": {"http://example.com/tracking/midpoint", "http://company.tracker.com?eventId=3&appbundle=abc"}, + "thirdQuartile": {"http://example.com/tracking/thirdQuartile", "http://company.tracker.com?eventId=5&appbundle=abc"}, + "complete": {"http://example.com/tracking/complete", "http://company.tracker.com?eventId=6&appbundle=abc"}, + "start": {"http://company.tracker.com?eventId=2&appbundle=abc", "http://partner.tracking.url"}, + }, + }, + }, + { + name: "non_linear_creative", + args: args{ + externalURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]", + bid: &openrtb2.Bid{ // Adm contains to TrackingEvents tag + AdM: ` + + + http://something.com + + + `, + }, + req: &openrtb2.BidRequest{App: &openrtb2.App{Bundle: "abc"}}, + }, + want: want{ + eventURLs: map[string][]string{ + // "firstQuartile": {"http://something.com", "http://company.tracker.com?eventId=1004&appbundle=abc"}, + // "midpoint": {"http://company.tracker.com?eventId=1003&appbundle=abc"}, + // "thirdQuartile": {"http://company.tracker.com?eventId=1005&appbundle=abc"}, + // "complete": {"http://company.tracker.com?eventId=1006&appbundle=abc"}, + "firstQuartile": {"http://something.com", "http://company.tracker.com?eventId=4&appbundle=abc"}, + "midpoint": {"http://company.tracker.com?eventId=3&appbundle=abc"}, + "thirdQuartile": {"http://company.tracker.com?eventId=5&appbundle=abc"}, + "complete": {"http://company.tracker.com?eventId=6&appbundle=abc"}, + "start": {"http://company.tracker.com?eventId=2&appbundle=abc"}, + }, + }, + }, { + name: "no_traker_url_configured", // expect no injection + args: args{ + externalURL: "", + bid: &openrtb2.Bid{ // Adm contains to TrackingEvents tag + AdM: ` + + + `, + }, + req: &openrtb2.BidRequest{App: &openrtb2.App{Bundle: "abc"}}, + }, + want: want{ + eventURLs: map[string][]string{}, + }, + }, + { + name: "wrapper_vast_xml_from_partner", // expect we are injecting trackers inside wrapper + args: args{ + externalURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]", + bid: &openrtb2.Bid{ // Adm contains to TrackingEvents tag + AdM: ` + + + iabtechlab + http://somevasturl + + + + + + `, + }, + req: &openrtb2.BidRequest{App: &openrtb2.App{Bundle: "abc"}}, + }, + want: want{ + eventURLs: map[string][]string{ + // "firstQuartile": {"http://company.tracker.com?eventId=firstQuartile&appbundle=abc"}, + // "midpoint": {"http://company.tracker.com?eventId=midpoint&appbundle=abc"}, + // "thirdQuartile": {"http://company.tracker.com?eventId=thirdQuartile&appbundle=abc"}, + // "complete": {"http://company.tracker.com?eventId=complete&appbundle=abc"}, + "firstQuartile": {"http://company.tracker.com?eventId=4&appbundle=abc"}, + "midpoint": {"http://company.tracker.com?eventId=3&appbundle=abc"}, + "thirdQuartile": {"http://company.tracker.com?eventId=5&appbundle=abc"}, + "complete": {"http://company.tracker.com?eventId=6&appbundle=abc"}, + "start": {"http://company.tracker.com?eventId=2&appbundle=abc"}, + }, + }, + }, + // { + // name: "vast_tag_uri_response_from_partner", + // args: args{ + // externalURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]", + // bid: &openrtb2.Bid{ // Adm contains to TrackingEvents tag + // AdM: ``, + // }, + // req: &openrtb2.BidRequest{App: &openrtb2.App{Bundle: "abc"}}, + // }, + // want: want{ + // eventURLs: map[string][]string{ + // "firstQuartile": {"http://company.tracker.com?eventId=firstQuartile&appbundle=abc"}, + // "midpoint": {"http://company.tracker.com?eventId=midpoint&appbundle=abc"}, + // "thirdQuartile": {"http://company.tracker.com?eventId=thirdQuartile&appbundle=abc"}, + // "complete": {"http://company.tracker.com?eventId=complete&appbundle=abc"}, + // }, + // }, + // }, + // { + // name: "adm_empty", + // args: args{ + // externalURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]", + // bid: &openrtb2.Bid{ // Adm contains to TrackingEvents tag + // AdM: "", + // NURL: "nurl_contents", + // }, + // req: &openrtb2.BidRequest{App: &openrtb2.App{Bundle: "abc"}}, + // }, + // want: want{ + // eventURLs: map[string][]string{ + // "firstQuartile": {"http://company.tracker.com?eventId=firstQuartile&appbundle=abc"}, + // "midpoint": {"http://company.tracker.com?eventId=midpoint&appbundle=abc"}, + // "thirdQuartile": {"http://company.tracker.com?eventId=thirdQuartile&appbundle=abc"}, + // "complete": {"http://company.tracker.com?eventId=complete&appbundle=abc"}, + // }, + // }, + // }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + vast := "" + if nil != tc.args.bid { + vast = tc.args.bid.AdM // original vast + } + // bind this bid id with imp object + tc.args.req.Imp = []openrtb2.Imp{{ID: "123", Video: &openrtb2.Video{}}} + tc.args.bid.ImpID = tc.args.req.Imp[0].ID + accountID := "" + timestamp := int64(0) + requestingBidder := "test_bidder" + bidderCoreName := "test_core_bidder" + injectedVast, injected, ierr := InjectVideoEventTrackers(tc.args.externalURL, vast, tc.args.bid, tc.args.genbidID, requestingBidder, bidderCoreName, accountID, timestamp, tc.args.req) + + if !injected { + // expect no change in input vast if tracking events are not injected + assert.Equal(t, vast, string(injectedVast)) + assert.NotNil(t, ierr) + } else { + assert.Nil(t, ierr) + } + actualVastDoc := etree.NewDocument() + + err := actualVastDoc.ReadFromBytes(injectedVast) + if nil != err { + assert.Fail(t, err.Error()) + } + + // fmt.Println(string(injectedVast)) + actualTrackingEvents := actualVastDoc.FindElements("VAST/Ad/InLine/Creatives/Creative/Linear/TrackingEvents/Tracking") + actualTrackingEvents = append(actualTrackingEvents, actualVastDoc.FindElements("VAST/Ad/InLine/Creatives/Creative/NonLinearAds/TrackingEvents/Tracking")...) + actualTrackingEvents = append(actualTrackingEvents, actualVastDoc.FindElements("VAST/Ad/Wrapper/Creatives/Creative/Linear/TrackingEvents/Tracking")...) + actualTrackingEvents = append(actualTrackingEvents, actualVastDoc.FindElements("VAST/Ad/Wrapper/Creatives/Creative/NonLinearAds/TrackingEvents/Tracking")...) + + totalURLCount := 0 + for event, URLs := range tc.want.eventURLs { + + for _, expectedURL := range URLs { + present := false + for _, te := range actualTrackingEvents { + if te.SelectAttr("event").Value == event && te.Text() == expectedURL { + present = true + totalURLCount++ + break // expected URL present. check for next expected URL + } + } + if !present { + assert.Fail(t, "Expected tracker URL '"+expectedURL+"' is not present") + } + } + } + // ensure all total of events are injected + assert.Equal(t, totalURLCount, len(actualTrackingEvents), fmt.Sprintf("Expected '%v' event trackers. But found '%v'", len(tc.want.eventURLs), len(actualTrackingEvents))) + + }) + } +} + +func TestGetVideoEventTracking(t *testing.T) { + type args struct { + trackerURL string + bid *openrtb2.Bid + requestingBidder string + gen_bidid string + bidderCoreName string + accountId string + timestamp int64 + req *openrtb2.BidRequest + doc *etree.Document + } + type want struct { + trackerURLMap map[string]string + } + tests := []struct { + name string + args args + want want + }{ + { + name: "valid_scenario", + args: args{ + trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]", + bid: &openrtb2.Bid{ + // AdM: vastXMLWith2Creatives, + }, + req: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Bundle: "someappbundle", + }, + Imp: []openrtb2.Imp{}, + }, + }, + want: want{ + trackerURLMap: map[string]string{ + // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile&appbundle=someappbundle", + // "midpoint": "http://company.tracker.com?eventId=midpoint&appbundle=someappbundle", + // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile&appbundle=someappbundle", + // "complete": "http://company.tracker.com?eventId=complete&appbundle=someappbundle"}, + "firstQuartile": "http://company.tracker.com?eventId=4&appbundle=someappbundle", + "midpoint": "http://company.tracker.com?eventId=3&appbundle=someappbundle", + "thirdQuartile": "http://company.tracker.com?eventId=5&appbundle=someappbundle", + "start": "http://company.tracker.com?eventId=2&appbundle=someappbundle", + "complete": "http://company.tracker.com?eventId=6&appbundle=someappbundle"}, + }, + }, + { + name: "no_macro_value", // expect no replacement + args: args{ + trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]", + bid: &openrtb2.Bid{}, + req: &openrtb2.BidRequest{ + App: &openrtb2.App{}, // no app bundle value + Imp: []openrtb2.Imp{}, + }, + }, + want: want{ + trackerURLMap: map[string]string{ + // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile&appbundle=[DOMAIN]", + // "midpoint": "http://company.tracker.com?eventId=midpoint&appbundle=[DOMAIN]", + // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile&appbundle=[DOMAIN]", + // "complete": "http://company.tracker.com?eventId=complete&appbundle=[DOMAIN]"}, + "firstQuartile": "http://company.tracker.com?eventId=4&appbundle=", + "midpoint": "http://company.tracker.com?eventId=3&appbundle=", + "thirdQuartile": "http://company.tracker.com?eventId=5&appbundle=", + "start": "http://company.tracker.com?eventId=2&appbundle=", + "complete": "http://company.tracker.com?eventId=6&appbundle="}, + }, + }, + { + name: "prefer_company_value_for_standard_macro", + args: args{ + trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]", + req: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Bundle: "myapp", // do not expect this value + }, + Imp: []openrtb2.Imp{}, + Ext: []byte(`{"prebid":{ + "macros": { + "[DOMAIN]": "my_custom_value" + } + }}`), + }, + }, + want: want{ + trackerURLMap: map[string]string{ + // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile&appbundle=my_custom_value", + // "midpoint": "http://company.tracker.com?eventId=midpoint&appbundle=my_custom_value", + // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile&appbundle=my_custom_value", + // "complete": "http://company.tracker.com?eventId=complete&appbundle=my_custom_value"}, + "firstQuartile": "http://company.tracker.com?eventId=4&appbundle=my_custom_value", + "midpoint": "http://company.tracker.com?eventId=3&appbundle=my_custom_value", + "thirdQuartile": "http://company.tracker.com?eventId=5&appbundle=my_custom_value", + "start": "http://company.tracker.com?eventId=2&appbundle=my_custom_value", + "complete": "http://company.tracker.com?eventId=6&appbundle=my_custom_value"}, + }, + }, { + name: "multireplace_macro", + args: args{ + trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]¶meter2=[DOMAIN]", + req: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Bundle: "myapp123", + }, + Imp: []openrtb2.Imp{}, + }, + }, + want: want{ + trackerURLMap: map[string]string{ + // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile&appbundle=myapp123¶meter2=myapp123", + // "midpoint": "http://company.tracker.com?eventId=midpoint&appbundle=myapp123¶meter2=myapp123", + // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile&appbundle=myapp123¶meter2=myapp123", + // "complete": "http://company.tracker.com?eventId=complete&appbundle=myapp123¶meter2=myapp123"}, + "firstQuartile": "http://company.tracker.com?eventId=4&appbundle=myapp123¶meter2=myapp123", + "midpoint": "http://company.tracker.com?eventId=3&appbundle=myapp123¶meter2=myapp123", + "thirdQuartile": "http://company.tracker.com?eventId=5&appbundle=myapp123¶meter2=myapp123", + "start": "http://company.tracker.com?eventId=2&appbundle=myapp123¶meter2=myapp123", + "complete": "http://company.tracker.com?eventId=6&appbundle=myapp123¶meter2=myapp123"}, + }, + }, + { + name: "custom_macro_without_prefix_and_suffix", + args: args{ + trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]¶m1=[CUSTOM_MACRO]", + req: &openrtb2.BidRequest{ + Ext: []byte(`{"prebid":{ + "macros": { + "CUSTOM_MACRO": "my_custom_value" + } + }}`), + Imp: []openrtb2.Imp{}, + }, + }, + want: want{ + trackerURLMap: map[string]string{ + // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile¶m1=[CUSTOM_MACRO]", + // "midpoint": "http://company.tracker.com?eventId=midpoint¶m1=[CUSTOM_MACRO]", + // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile¶m1=[CUSTOM_MACRO]", + // "complete": "http://company.tracker.com?eventId=complete¶m1=[CUSTOM_MACRO]"}, + "firstQuartile": "http://company.tracker.com?eventId=4¶m1=[CUSTOM_MACRO]", + "midpoint": "http://company.tracker.com?eventId=3¶m1=[CUSTOM_MACRO]", + "thirdQuartile": "http://company.tracker.com?eventId=5¶m1=[CUSTOM_MACRO]", + "start": "http://company.tracker.com?eventId=2¶m1=[CUSTOM_MACRO]", + "complete": "http://company.tracker.com?eventId=6¶m1=[CUSTOM_MACRO]"}, + }, + }, + { + name: "empty_macro", + args: args{ + trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]¶m1=[CUSTOM_MACRO]", + req: &openrtb2.BidRequest{ + Ext: []byte(`{"prebid":{ + "macros": { + "": "my_custom_value" + } + }}`), + Imp: []openrtb2.Imp{}, + }, + }, + want: want{ + trackerURLMap: map[string]string{ + // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile¶m1=[CUSTOM_MACRO]", + // "midpoint": "http://company.tracker.com?eventId=midpoint¶m1=[CUSTOM_MACRO]", + // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile¶m1=[CUSTOM_MACRO]", + // "complete": "http://company.tracker.com?eventId=complete¶m1=[CUSTOM_MACRO]"}, + "firstQuartile": "http://company.tracker.com?eventId=4¶m1=[CUSTOM_MACRO]", + "midpoint": "http://company.tracker.com?eventId=3¶m1=[CUSTOM_MACRO]", + "thirdQuartile": "http://company.tracker.com?eventId=5¶m1=[CUSTOM_MACRO]", + "start": "http://company.tracker.com?eventId=2¶m1=[CUSTOM_MACRO]", + "complete": "http://company.tracker.com?eventId=6¶m1=[CUSTOM_MACRO]"}, + }, + }, + { + name: "macro_is_case_sensitive", + args: args{ + trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]¶m1=[CUSTOM_MACRO]", + req: &openrtb2.BidRequest{ + Ext: []byte(`{"prebid":{ + "macros": { + "": "my_custom_value" + } + }}`), + Imp: []openrtb2.Imp{}, + }, + }, + want: want{ + trackerURLMap: map[string]string{ + // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile¶m1=[CUSTOM_MACRO]", + // "midpoint": "http://company.tracker.com?eventId=midpoint¶m1=[CUSTOM_MACRO]", + // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile¶m1=[CUSTOM_MACRO]", + // "complete": "http://company.tracker.com?eventId=complete¶m1=[CUSTOM_MACRO]"}, + "firstQuartile": "http://company.tracker.com?eventId=4¶m1=[CUSTOM_MACRO]", + "midpoint": "http://company.tracker.com?eventId=3¶m1=[CUSTOM_MACRO]", + "thirdQuartile": "http://company.tracker.com?eventId=5¶m1=[CUSTOM_MACRO]", + "start": "http://company.tracker.com?eventId=2¶m1=[CUSTOM_MACRO]", + "complete": "http://company.tracker.com?eventId=6¶m1=[CUSTOM_MACRO]"}, + }, + }, + { + name: "empty_tracker_url", + args: args{trackerURL: " ", req: &openrtb2.BidRequest{Imp: []openrtb2.Imp{}}}, + want: want{trackerURLMap: make(map[string]string)}, + }, + { + name: "site_domain_tracker_url", + args: args{trackerURL: "https://company.tracker.com?operId=8&e=[EVENT_ID]&p=[PBS-ACCOUNT]&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=[PBS-BIDDER]&advertiser_id=[ADVERTISER_NAME]&sURL=[DOMAIN]&pfi=[PLATFORM]&af=[ADTYPE]&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=[AD_UNIT]&bidid=[PBS-BIDID]", + req: &openrtb2.BidRequest{Site: &openrtb2.Site{Name: "test", Domain: "www.test.com", Publisher: &openrtb2.Publisher{ID: "5890"}}, Imp: []openrtb2.Imp{}}}, + want: want{ + map[string]string{ + "complete": "https://company.tracker.com?operId=8&e=6&p=5890&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=&advertiser_id=&sURL=www.test.com&pfi=[PLATFORM]&af=video&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=&bidid=", + "firstQuartile": "https://company.tracker.com?operId=8&e=4&p=5890&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=&advertiser_id=&sURL=www.test.com&pfi=[PLATFORM]&af=video&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=&bidid=", + "midpoint": "https://company.tracker.com?operId=8&e=3&p=5890&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=&advertiser_id=&sURL=www.test.com&pfi=[PLATFORM]&af=video&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=&bidid=", + "start": "https://company.tracker.com?operId=8&e=2&p=5890&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=&advertiser_id=&sURL=www.test.com&pfi=[PLATFORM]&af=video&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=&bidid=", + "thirdQuartile": "https://company.tracker.com?operId=8&e=5&p=5890&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=&advertiser_id=&sURL=www.test.com&pfi=[PLATFORM]&af=video&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=&bidid=", + }, + }, + }, + { + name: "site_page_tracker_url", + args: args{trackerURL: "https://company.tracker.com?operId=8&e=[EVENT_ID]&p=[PBS-ACCOUNT]&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=[PBS-BIDDER]&advertiser_id=[ADVERTISER_NAME]&sURL=[DOMAIN]&pfi=[PLATFORM]&af=[ADTYPE]&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=[AD_UNIT]&bidid=[PBS-BIDID]", + req: &openrtb2.BidRequest{Site: &openrtb2.Site{Name: "test", Page: "https://www.test.com/", Publisher: &openrtb2.Publisher{ID: "5890"}}, Imp: []openrtb2.Imp{}}}, + want: want{ + map[string]string{ + "complete": "https://company.tracker.com?operId=8&e=6&p=5890&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=&advertiser_id=&sURL=www.test.com&pfi=[PLATFORM]&af=video&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=&bidid=", + "firstQuartile": "https://company.tracker.com?operId=8&e=4&p=5890&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=&advertiser_id=&sURL=www.test.com&pfi=[PLATFORM]&af=video&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=&bidid=", + "midpoint": "https://company.tracker.com?operId=8&e=3&p=5890&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=&advertiser_id=&sURL=www.test.com&pfi=[PLATFORM]&af=video&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=&bidid=", + "start": "https://company.tracker.com?operId=8&e=2&p=5890&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=&advertiser_id=&sURL=www.test.com&pfi=[PLATFORM]&af=video&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=&bidid=", + "thirdQuartile": "https://company.tracker.com?operId=8&e=5&p=5890&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=&advertiser_id=&sURL=www.test.com&pfi=[PLATFORM]&af=video&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=&bidid=", + }, + }, + }, + { + name: "all_macros with generated_bidId", // expect encoding for WRAPPER_IMPRESSION_ID macro + args: args{ + trackerURL: "https://company.tracker.com?operId=8&e=[EVENT_ID]&p=[PBS-ACCOUNT]&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=[PBS-BIDDER]&advertiser_id=[ADVERTISER_NAME]&sURL=[DOMAIN]&pfi=[PLATFORM]&af=[ADTYPE]&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=[AD_UNIT]&bidid=[PBS-BIDID]&origbidid=[PBS-ORIG_BIDID]&bc=[BIDDER_CODE]", + req: &openrtb2.BidRequest{ + App: &openrtb2.App{Bundle: "com.someapp.com", Publisher: &openrtb2.Publisher{ID: "5890"}}, + Ext: []byte(`{ + "prebid": { + "macros": { + "[PROFILE_ID]": "100", + "[PROFILE_VERSION]": "2", + "[UNIX_TIMESTAMP]": "1234567890", + "[PLATFORM]": "7", + "[WRAPPER_IMPRESSION_ID]": "abc~!@#$%^&&*()_+{}|:\"<>?[]\\;',./" + } + } + }`), + Imp: []openrtb2.Imp{ + {TagID: "/testadunit/1", ID: "imp_1"}, + }, + }, + bid: &openrtb2.Bid{ADomain: []string{"http://a.com/32?k=v", "b.com"}, ImpID: "imp_1", ID: "test_bid_id"}, + gen_bidid: "random_bid_id", + requestingBidder: "test_bidder:234", + bidderCoreName: "test_core_bidder:234", + }, + want: want{ + trackerURLMap: map[string]string{ + "firstQuartile": "https://company.tracker.com?operId=8&e=4&p=5890&pid=100&v=2&ts=1234567890&pn=test_core_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=random_bid_id&origbidid=test_bid_id&bc=test_bidder%3A234", + "midpoint": "https://company.tracker.com?operId=8&e=3&p=5890&pid=100&v=2&ts=1234567890&pn=test_core_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=random_bid_id&origbidid=test_bid_id&bc=test_bidder%3A234", + "thirdQuartile": "https://company.tracker.com?operId=8&e=5&p=5890&pid=100&v=2&ts=1234567890&pn=test_core_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=random_bid_id&origbidid=test_bid_id&bc=test_bidder%3A234", + "complete": "https://company.tracker.com?operId=8&e=6&p=5890&pid=100&v=2&ts=1234567890&pn=test_core_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=random_bid_id&origbidid=test_bid_id&bc=test_bidder%3A234", + "start": "https://company.tracker.com?operId=8&e=2&p=5890&pid=100&v=2&ts=1234567890&pn=test_core_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=random_bid_id&origbidid=test_bid_id&bc=test_bidder%3A234"}, + }, + }, + { + name: "all_macros with empty generated_bidId", // expect encoding for WRAPPER_IMPRESSION_ID macro + args: args{ + trackerURL: "https://company.tracker.com?operId=8&e=[EVENT_ID]&p=[PBS-ACCOUNT]&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=[PBS-BIDDER]&advertiser_id=[ADVERTISER_NAME]&sURL=[DOMAIN]&pfi=[PLATFORM]&af=[ADTYPE]&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=[AD_UNIT]&bidid=[PBS-BIDID]&origbidid=[PBS-ORIG_BIDID]&bc=[BIDDER_CODE]", + req: &openrtb2.BidRequest{ + App: &openrtb2.App{Bundle: "com.someapp.com", Publisher: &openrtb2.Publisher{ID: "5890"}}, + Ext: []byte(`{ + "prebid": { + "macros": { + "[PROFILE_ID]": "100", + "[PROFILE_VERSION]": "2", + "[UNIX_TIMESTAMP]": "1234567890", + "[PLATFORM]": "7", + "[WRAPPER_IMPRESSION_ID]": "abc~!@#$%^&&*()_+{}|:\"<>?[]\\;',./" + } + } + }`), + Imp: []openrtb2.Imp{ + {TagID: "/testadunit/1", ID: "imp_1"}, + }, + }, + bid: &openrtb2.Bid{ADomain: []string{"http://a.com/32?k=v", "b.com"}, ImpID: "imp_1", ID: "test_bid_id"}, + gen_bidid: "", + requestingBidder: "test_bidder:234", + bidderCoreName: "test_core_bidder:234", + }, + want: want{ + trackerURLMap: map[string]string{ + "firstQuartile": "https://company.tracker.com?operId=8&e=4&p=5890&pid=100&v=2&ts=1234567890&pn=test_core_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=test_bid_id&origbidid=test_bid_id&bc=test_bidder%3A234", + "midpoint": "https://company.tracker.com?operId=8&e=3&p=5890&pid=100&v=2&ts=1234567890&pn=test_core_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=test_bid_id&origbidid=test_bid_id&bc=test_bidder%3A234", + "thirdQuartile": "https://company.tracker.com?operId=8&e=5&p=5890&pid=100&v=2&ts=1234567890&pn=test_core_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=test_bid_id&origbidid=test_bid_id&bc=test_bidder%3A234", + "complete": "https://company.tracker.com?operId=8&e=6&p=5890&pid=100&v=2&ts=1234567890&pn=test_core_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=test_bid_id&origbidid=test_bid_id&bc=test_bidder%3A234", + "start": "https://company.tracker.com?operId=8&e=2&p=5890&pid=100&v=2&ts=1234567890&pn=test_core_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=test_bid_id&origbidid=test_bid_id&bc=test_bidder%3A234"}, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + + if nil == tc.args.bid { + tc.args.bid = &openrtb2.Bid{} + } + + impMap := map[string]*openrtb2.Imp{} + + for _, imp := range tc.args.req.Imp { + impMap[imp.ID] = &imp + } + + eventURLMap := GetVideoEventTracking(tc.args.trackerURL, tc.args.bid, tc.args.gen_bidid, tc.args.requestingBidder, tc.args.bidderCoreName, tc.args.accountId, tc.args.timestamp, tc.args.req, tc.args.doc, impMap) + + for event, eurl := range tc.want.trackerURLMap { + + u, _ := url.Parse(eurl) + expectedValues, _ := url.ParseQuery(u.RawQuery) + u, _ = url.Parse(eventURLMap[event]) + actualValues, _ := url.ParseQuery(u.RawQuery) + for k, ev := range expectedValues { + av := actualValues[k] + for i := 0; i < len(ev); i++ { + assert.Equal(t, ev[i], av[i], fmt.Sprintf("Expected '%v' for '%v'. but found %v", ev[i], k, av[i])) + } + } + + // error out if extra query params + if len(expectedValues) != len(actualValues) { + assert.Equal(t, expectedValues, actualValues, fmt.Sprintf("Expected '%v' query params but found '%v'", len(expectedValues), len(actualValues))) + break + } + } + + // check if new quartile pixels are covered inside test + assert.Equal(t, tc.want.trackerURLMap, eventURLMap) + }) + } +} + +func TestReplaceMacro(t *testing.T) { + type args struct { + trackerURL string + macro string + value string + } + type want struct { + trackerURL string + } + tests := []struct { + name string + args args + want want + }{ + {name: "empty_tracker_url", args: args{trackerURL: "", macro: "[TEST]", value: "testme"}, want: want{trackerURL: ""}}, + {name: "tracker_url_with_macro", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "[TEST]", value: "testme"}, want: want{trackerURL: "http://something.com?test=testme"}}, + {name: "tracker_url_with_invalid_macro", args: args{trackerURL: "http://something.com?test=TEST]", macro: "[TEST]", value: "testme"}, want: want{trackerURL: "http://something.com?test=TEST]"}}, + {name: "tracker_url_with_repeating_macro", args: args{trackerURL: "http://something.com?test=[TEST]&test1=[TEST]", macro: "[TEST]", value: "testme"}, want: want{trackerURL: "http://something.com?test=testme&test1=testme"}}, + {name: "empty_macro", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "", value: "testme"}, want: want{trackerURL: "http://something.com?test=[TEST]"}}, + {name: "macro_without_[", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "TEST]", value: "testme"}, want: want{trackerURL: "http://something.com?test=[TEST]"}}, + {name: "macro_without_]", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "[TEST", value: "testme"}, want: want{trackerURL: "http://something.com?test=[TEST]"}}, + {name: "empty_value", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "[TEST]", value: ""}, want: want{trackerURL: "http://something.com?test="}}, + {name: "nested_macro_value", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "[TEST]", value: "[TEST][TEST]"}, want: want{trackerURL: "http://something.com?test=%5BTEST%5D%5BTEST%5D"}}, + {name: "url_as_macro_value", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "[TEST]", value: "http://iamurl.com"}, want: want{trackerURL: "http://something.com?test=http%3A%2F%2Fiamurl.com"}}, + {name: "macro_with_spaces", args: args{trackerURL: "http://something.com?test=[TEST]", macro: " [TEST] ", value: "http://iamurl.com"}, want: want{trackerURL: "http://something.com?test=http%3A%2F%2Fiamurl.com"}}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + trackerURL := replaceMacro(tc.args.trackerURL, tc.args.macro, tc.args.value) + assert.Equal(t, tc.want.trackerURL, trackerURL) + }) + } + +} +func TestExtractDomain(t *testing.T) { + testCases := []struct { + description string + url string + expectedDomain string + expectedErr error + }{ + {description: "a.com", url: "a.com", expectedDomain: "a.com", expectedErr: nil}, + {description: "a.com/123", url: "a.com/123", expectedDomain: "a.com", expectedErr: nil}, + {description: "http://a.com/123", url: "http://a.com/123", expectedDomain: "a.com", expectedErr: nil}, + {description: "https://a.com/123", url: "https://a.com/123", expectedDomain: "a.com", expectedErr: nil}, + {description: "c.b.a.com", url: "c.b.a.com", expectedDomain: "c.b.a.com", expectedErr: nil}, + {description: "url_encoded_http://c.b.a.com", url: "http%3A%2F%2Fc.b.a.com", expectedDomain: "c.b.a.com", expectedErr: nil}, + {description: "url_encoded_with_www_http://c.b.a.com", url: "http%3A%2F%2Fwww.c.b.a.com", expectedDomain: "c.b.a.com", expectedErr: nil}, + } + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + domain, err := extractDomain(test.url) + assert.Equal(t, test.expectedDomain, domain) + assert.Equal(t, test.expectedErr, err) + }) + } +} diff --git a/endpoints/events/vtrack_test.go b/endpoints/events/vtrack_test.go index d8905d7b443..0d402acebdd 100644 --- a/endpoints/events/vtrack_test.go +++ b/endpoints/events/vtrack_test.go @@ -692,7 +692,6 @@ func getVTrackRequestData(wi bool, wic bool) (db []byte, e error) { return data.Bytes(), e } - func TestGetIntegrationType(t *testing.T) { testCases := []struct { description string diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 9935ea74466..3a496e0124f 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -1205,6 +1205,7 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb2.Imp, aliases map[string]s /* Process all the bidder exts in the request */ disabledBidders := []string{} otherExtElements := 0 + validationFailedBidders := []string{} for bidder, ext := range bidderExts { if isBidderToValidate(bidder) { coreBidder := bidder @@ -1213,7 +1214,10 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb2.Imp, aliases map[string]s } if bidderName, isValid := deps.bidderMap[coreBidder]; isValid { if err := deps.paramsValidator.Validate(bidderName, ext); err != nil { - return []error{fmt.Errorf("request.imp[%d].ext.%s failed validation.\n%v", impIndex, coreBidder, err)} + validationFailedBidders = append(validationFailedBidders, bidder) + msg := fmt.Sprintf("request.imp[%d].ext.%s failed validation.\n%v", impIndex, coreBidder, err) + glog.Errorf("BidderSchemaValidationError: %s", msg) + errL = append(errL, &errortypes.BidderFailedSchemaValidation{Message: msg}) } } else { if msg, isDisabled := deps.disabledBidders[bidder]; isDisabled { @@ -1233,6 +1237,16 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb2.Imp, aliases map[string]s for _, bidder := range disabledBidders { delete(bidderExts, bidder) } + } + + // delete bidders with invalid params + if len(validationFailedBidders) > 0 { + for _, bidder := range validationFailedBidders { + delete(bidderExts, bidder) + } + } + + if len(disabledBidders) > 0 || len(validationFailedBidders) > 0 { extJSON, err := json.Marshal(bidderExts) if err != nil { return []error{err} @@ -1262,6 +1276,8 @@ func isBidderToValidate(bidder string) bool { return false case openrtb_ext.BidderReservedTID: return false + case openrtb_ext.BidderReservedCommerce: + return false default: return true } diff --git a/endpoints/openrtb2/auction_ow_test.go b/endpoints/openrtb2/auction_ow_test.go new file mode 100644 index 00000000000..4cbf4d12998 --- /dev/null +++ b/endpoints/openrtb2/auction_ow_test.go @@ -0,0 +1,103 @@ +package openrtb2 + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + analyticsConf "github.com/prebid/prebid-server/analytics/config" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + metricsConfig "github.com/prebid/prebid-server/metrics/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher" + "github.com/stretchr/testify/assert" +) + +func TestValidateImpExtOW(t *testing.T) { + paramValidator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + panic(err.Error()) + } + + type testCase struct { + description string + impExt json.RawMessage + expectedImpExt string + expectedErrs []error + } + testGroups := []struct { + description string + testCases []testCase + }{ + { + "Invalid bidder params tests", + []testCase{ + { + description: "Impression dropped for bidder with invalid bidder params", + impExt: json.RawMessage(`{"appnexus":{"placement_id":"A"}}`), + expectedImpExt: `{}`, + expectedErrs: []error{&errortypes.BidderFailedSchemaValidation{Message: "request.imp[0].ext.appnexus failed validation.\nplacement_id: Invalid type. Expected: integer, given: string"}, + fmt.Errorf("request.imp[%d].ext must contain at least one bidder", 0)}, + }, + { + description: "Valid Bidder params + Invalid bidder params", + impExt: json.RawMessage(`{"appnexus":{"placement_id":"A"},"pubmatic":{"publisherId":"156209"}}`), + expectedImpExt: `{"pubmatic":{"publisherId":"156209"}}`, + expectedErrs: []error{&errortypes.BidderFailedSchemaValidation{Message: "request.imp[0].ext.appnexus failed validation.\nplacement_id: Invalid type. Expected: integer, given: string"}}, + }, + { + description: "Valid Bidder + Disabled Bidder + Invalid bidder params", + impExt: json.RawMessage(`{"pubmatic":{"publisherId":156209},"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"}}`), + expectedImpExt: `{"appnexus":{"placement_id":555}}`, + expectedErrs: []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}, + &errortypes.BidderFailedSchemaValidation{Message: "request.imp[0].ext.pubmatic failed validation.\npublisherId: Invalid type. Expected: string, given: integer"}}, + }, + { + description: "Valid Bidder + Disabled Bidder + Invalid bidder params", + impExt: json.RawMessage(`{"pubmatic":{"publisherId":156209},"disabledbidder":{"foo":"bar"}}`), + expectedImpExt: `{}`, + expectedErrs: []error{&errortypes.BidderFailedSchemaValidation{Message: "request.imp[0].ext.pubmatic failed validation.\npublisherId: Invalid type. Expected: string, given: integer"}, + &errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}, + fmt.Errorf("request.imp[%d].ext must contain at least one bidder", 0)}, + }, + }, + }, + } + + deps := &endpointDeps{ + fakeUUIDGenerator{}, + &nobidExchange{}, + paramValidator, + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: int64(8096)}, + &metricsConfig.NilMetricsEngine{}, + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{"disabledbidder": "The bidder 'disabledbidder' has been disabled."}, + false, + []byte{}, + openrtb_ext.BuildBidderMap(), + nil, + nil, + hardcodedResponseIPValidator{response: true}, + empty_fetcher.EmptyFetcher{}, + } + + for _, group := range testGroups { + for _, test := range group.testCases { + imp := &openrtb2.Imp{Ext: test.impExt} + + errs := deps.validateImpExt(imp, nil, 0, false, nil) + + if len(test.expectedImpExt) > 0 { + assert.JSONEq(t, test.expectedImpExt, string(imp.Ext), "imp.ext JSON does not match expected. Test: %s. %s\n", group.description, test.description) + } else { + assert.Empty(t, imp.Ext, "imp.ext expected to be empty but was: %s. Test: %s. %s\n", string(imp.Ext), group.description, test.description) + } + assert.ElementsMatch(t, test.expectedErrs, errs, "errs slice does not match expected. Test: %s. %s\n", group.description, test.description) + } + } +} diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 54d326c4d9d..cb05f0cf642 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -167,7 +167,7 @@ func runTestCase(t *testing.T, auctionEndpointHandler httprouter.Handle, test te if assert.NoError(t, err, "Could not unmarshal expected bidResponse taken from test file.\n Test file: %s\n Error:%s\n", testFile, err) { err = json.Unmarshal([]byte(actualJsonBidResponse), &actualBidResponse) if assert.NoError(t, err, "Could not unmarshal actual bidResponse from auction.\n Test file: %s\n Error:%s\n", testFile, err) { - assertBidResponseEqual(t, testFile, expectedBidResponse, actualBidResponse) + assertBidResponseEqual(t, test, testFile, expectedBidResponse, actualBidResponse) } } } @@ -176,7 +176,7 @@ func runTestCase(t *testing.T, auctionEndpointHandler httprouter.Handle, test te // Once unmarshalled, bidResponse objects can't simply be compared with an `assert.Equalf()` call // because tests fail if the elements inside the `bidResponse.SeatBid` and `bidResponse.SeatBid.Bid` // arrays, if any, are not listed in the exact same order in the actual version and in the expected version. -func assertBidResponseEqual(t *testing.T, testFile string, expectedBidResponse openrtb2.BidResponse, actualBidResponse openrtb2.BidResponse) { +func assertBidResponseEqual(t *testing.T, test testCase, testFile string, expectedBidResponse openrtb2.BidResponse, actualBidResponse openrtb2.BidResponse) { //Assert non-array BidResponse fields assert.Equalf(t, expectedBidResponse.ID, actualBidResponse.ID, "BidResponse.ID doesn't match expected. Test: %s\n", testFile) @@ -225,6 +225,10 @@ func assertBidResponseEqual(t *testing.T, testFile string, expectedBidResponse o } assert.Equalf(t, expectedBid.ImpID, actualBidMap[bidID].ImpID, "BidResponse.SeatBid[%s].Bid[%s].ImpID doesn't match expected. Test: %s\n", bidderName, bidID, testFile) assert.Equalf(t, expectedBid.Price, actualBidMap[bidID].Price, "BidResponse.SeatBid[%s].Bid[%s].Price doesn't match expected. Test: %s\n", bidderName, bidID, testFile) + + if test.Config.AssertBidExt { + assert.JSONEq(t, string(expectedBid.Ext), string(actualBidMap[bidID].Ext), "BidResponse.SeatBid[%s].Bid[%s].Ext doesn't match expected. Test: %s\n", bidderName, bidID, testFile) + } } } } @@ -307,7 +311,7 @@ func TestBidRequestAssert(t *testing.T) { } for _, test := range testSuites { - assertBidResponseEqual(t, test.description, test.expectedBidResponse, test.actualBidResponse) + assertBidResponseEqual(t, testCase{Config: &testConfigValues{}}, test.description, test.expectedBidResponse, test.actualBidResponse) } } @@ -2242,7 +2246,7 @@ func TestValidateImpExt(t *testing.T) { } else { assert.Empty(t, imp.Ext, "imp.ext expected to be empty but was: %s. Test: %s. %s\n", string(imp.Ext), group.description, test.description) } - assert.Equal(t, test.expectedErrs, errs, "errs slice does not match expected. Test: %s. %s\n", group.description, test.description) + assert.ElementsMatch(t, test.expectedErrs, errs, "errs slice does not match expected. Test: %s. %s\n", group.description, test.description) } } } diff --git a/endpoints/openrtb2/ctv/combination/adslot_combination_generator.go b/endpoints/openrtb2/ctv/combination/adslot_combination_generator.go new file mode 100644 index 00000000000..19c71856d9e --- /dev/null +++ b/endpoints/openrtb2/ctv/combination/adslot_combination_generator.go @@ -0,0 +1,587 @@ +package combination + +import ( + "math/big" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +//generator holds all the combinations based +//on Video Ad Pod request and Bid Response Max duration +type generator struct { + podMinDuration uint64 // Pod Minimum duration value present in origin Video Ad Pod Request + podMaxDuration uint64 // Pod Maximum duration value present in origin Video Ad Pod Request + minAds uint64 // Minimum Ads value present in origin Video Ad Pod Request + maxAds uint64 // Maximum Ads value present in origin Video Ad Pod Request + slotDurations []uint64 // input slot durations for which + slotDurationAdMap map[uint64]uint64 // map of key = duration, value = no of creatives with given duration + noOfSlots int // Number of slots to be consider (from left to right) + combinationCountMap map[uint64]uint64 //key - number of ads, ranging from 1 to maxads given in request config value - containing no of combinations with repeatation each key can have (without validations) + stats stats // metrics information + combinations [][]uint64 // May contains some/all combinations at given point of time + state snapshot // state configurations in case of lazy loading + order int // Indicates generation order e.g. maxads to min ads +} + +// stats holds the metrics information for given point of time +// such as current combination count, valid combination count, repeatation count +// out of range combination +type stats struct { + currentCombinationCount int // current combination count generated out of totalExpectedCombinations + validCombinationCount int // + repeatationsCount int // no of combinations not considered because containing some/all durations for which only single ad is present + outOfRangeCount int // no of combinations out of range because not satisfied pod min and max range + totalExpectedCombinations uint64 // indicates total number for possible combinations without validations but subtracts repeatations for duration with single ad +} + +// snashot retains the state of iteration +// it is used in determing when next valid combination is requested +// using Next() method +type snapshot struct { + start uint64 // indicates which duration to be used to form combination + index int64 // indicates from which index in combination array we should fill duration given by start + r uint64 // holds the current combination length ranging from minads to maxads + lastCombination []uint64 // holds the last combination iterated + stateUpdated bool // flag indicating whether underneath search method updated the c.state values + valueUpdated bool // indicates whether search method determined and updated next combination + combinationCounter uint64 // holds the index of duration to be filled when 1 cycle of combination ends + resetFlags bool // indicates whether the required flags to reset or not +} + +// Init ...initializes with following +// 1. Determines the number of combinations to be generated +// 2. Intializes the c.state values required for c.Next() and iteratoor +// generationOrder indicates how combinations should be generated. +func (c *generator) Init(podMinDuration, podMaxDuration uint64, config *openrtb_ext.VideoAdPod, durationAdsMap [][2]uint64, generationOrder int) { + + c.podMinDuration = podMinDuration + c.podMaxDuration = podMaxDuration + c.minAds = uint64(*config.MinAds) + c.maxAds = uint64(*config.MaxAds) + + // map of key = duration value = number of ads(must be non zero positive number) + c.slotDurationAdMap = make(map[uint64]uint64, len(c.slotDurations)) + + // iterate and extract duration and number of ads belonging to the duration + // split logic - :: separated + + cnt := 0 + c.slotDurations = make([]uint64, len(durationAdsMap)) + for _, durationNoOfAds := range durationAdsMap { + + c.slotDurations[cnt] = durationNoOfAds[0] + // save duration and no of ads info + c.slotDurationAdMap[durationNoOfAds[0]] = durationNoOfAds[1] + cnt++ + } + + c.noOfSlots = len(c.slotDurations) + c.stats.currentCombinationCount = 0 + c.stats.validCombinationCount = 0 + c.state = snapshot{} + + c.combinationCountMap = make(map[uint64]uint64, c.maxAds) + // compute no of possible combinations (without validations) + // using configurationss + c.stats.totalExpectedCombinations = compute(c, c.maxAds, true) + subtractUnwantedRepeatations(c) + // c.combinations = make([][]uint64, c.totalExpectedCombinations) + // util.Logf("Allow Repeatation = %v", c.allowRepetitationsForEligibleDurations) + // util.Logf("Total possible combinations (without validations) = %v ", c.totalExpectedCombinations) + + /// new states + c.state.start = uint64(0) + c.state.index = 0 + c.state.r = c.minAds + c.order = generationOrder + if c.order == MaxToMin { + c.state.r = c.maxAds + } + c.state.resetFlags = true +} + +//Next - Get next ad slot combination +//returns empty array if next combination is not present +func (c *generator) Next() []uint64 { + var comb []uint64 + if len(c.slotDurations) <= 0 { + return comb + } + if c.state.resetFlags { + reset(c) + c.state.resetFlags = false + } + for { + comb = c.lazyNext() + if len(comb) == 0 || isValidCombination(c, comb) { + break + } + } + return comb +} + +func isValidCombination(c *generator, combination []uint64) bool { + // check if repeatations are allowed + repeationMap := make(map[uint64]uint64, len(c.slotDurations)) + totalAdDuration := uint64(0) + for _, duration := range combination { + repeationMap[uint64(duration)]++ + // check current combination contains repeating durations such that + // count(duration) > count(no of ads aunction engine received for the duration) + currentRepeationCnt := repeationMap[duration] + noOfAdsPresent := c.slotDurationAdMap[duration] + if currentRepeationCnt > noOfAdsPresent { + //util.Logf("count = %v :: Discarding combination '%v' as only '%v' ad is present for duration %v", c.stats.currentCombinationCount, combination, noOfAdsPresent, duration) + c.stats.repeatationsCount++ + return false + } + + // check if sum of durations is withing pod min and max duration + totalAdDuration += duration + } + + if !(totalAdDuration >= c.podMinDuration && totalAdDuration <= c.podMaxDuration) { + // totalAdDuration is not within range of Pod min and max duration + //util.Logf("count = %v :: Discarding combination '%v' as either total Ad duration (%v) < %v (Pod min duration) or > %v (Pod Max duration)", c.stats.currentCombinationCount, combination, totalAdDuration, c.podMinDuration, c.podMaxDuration) + c.stats.outOfRangeCount++ + return false + } + c.stats.validCombinationCount++ + return true +} + +//compute - number of combinations that can be generated based on +//1. minads +//2. maxads +//3. Ordering of durations not matters. i.e. 4,5,6 will not be considered again as 5,4,6 or 6,5,4 +//4. Repeatations are allowed only for those durations where multiple ads are present +// Sum ups number of combinations for each noOfAds (r) based on above criteria and returns the total +// It operates recursively +// c - algorithm config, noOfAds (r) - maxads requested (if recursion=true otherwise any valid value), recursion - whether to do recursion or not. if false then only single combination +// for given noOfAds will be computed +func compute(c *generator, noOfAds uint64, recursion bool) uint64 { + + // can not limit till c.minAds + // because we want to construct + // c.combinationCountMap required by subtractUnwantedRepeatations + if noOfAds <= 0 || len(c.slotDurations) <= 0 { + return 0 + } + var noOfCombinations *big.Int + // Formula + // (r + n - 1)! + // ------------ + // r! (n - 1)! + n := uint64(len(c.slotDurations)) + r := uint64(noOfAds) + d1 := fact(uint64(r)) + d2 := fact(n - 1) + d3 := d1.Mul(&d1, &d2) + nmrt := fact(r + n - 1) + + noOfCombinations = nmrt.Div(&nmrt, d3) + // store pure combination with repeatation in combinationCountMap + c.combinationCountMap[r] = noOfCombinations.Uint64() + //util.Logf("%v", noOfCombinations) + if recursion { + + // add only if it is withing limit of c.minads + nextLevelCombinations := compute(c, noOfAds-1, recursion) + if noOfAds-1 >= c.minAds { + sumOfCombinations := noOfCombinations.Add(noOfCombinations, big.NewInt(int64(nextLevelCombinations))) + return sumOfCombinations.Uint64() + } + + } + return noOfCombinations.Uint64() +} + +//fact computes factorial of given number. +// It is used by compute function +func fact(no uint64) big.Int { + if no == 0 { + return *big.NewInt(int64(1)) + } + var bigNo big.Int + bigNo.SetUint64(no) + + fact := fact(no - 1) + mult := bigNo.Mul(&bigNo, &fact) + + return *mult +} + +//searchAll - searches all valid combinations +// valid combinations are those which satisifies following +// 1. sum of duration is within range of pod min and max values +// 2. Each duration within combination honours number of ads value given in the request +// 3. Number of durations in combination are within range of min and max ads +func (c *generator) searchAll() [][]uint64 { + reset(c) + start := uint64(0) + index := uint64(0) + + if c.order == MinToMax { + for r := c.minAds; r <= c.maxAds; r++ { + data := make([]uint64, r) + c.search(data, start, index, r, false, 0) + } + } + if c.order == MaxToMin { + for r := c.maxAds; r >= c.minAds; r-- { + data := make([]uint64, r) + c.search(data, start, index, r, false, 0) + } + } + // util.Logf("Total combinations generated = %v", c.currentCombinationCount) + // util.Logf("Total combinations expected = %v", c.totalExpectedCombinations) + // result := make([][]uint64, c.totalExpectedCombinations) + result := make([][]uint64, c.stats.validCombinationCount) + copy(result, c.combinations) + c.stats.currentCombinationCount = 0 + return result +} + +//reset the internal counters +func reset(c *generator) { + c.stats.currentCombinationCount = 0 + c.stats.validCombinationCount = 0 + c.stats.repeatationsCount = 0 + c.stats.outOfRangeCount = 0 +} + +//lazyNext performs stateful iteration. Instead of returning all valid combinations +//in one gp, it will return each combination on demand basis. +// valid combinations are those which satisifies following +// 1. sum of duration is within range of pod min and max values +// 2. Each duration within combination honours number of ads value given in the request +// 3. Number of durations in combination are within range of min and max ads +func (c *generator) lazyNext() []uint64 { + start := c.state.start + index := c.state.index + r := c.state.r + // reset last combination + // by deleting previous values + if c.state.lastCombination == nil { + c.combinations = make([][]uint64, 0) + } + data := &c.state.lastCombination + if *data == nil || uint64(len(*data)) != r { + *data = make([]uint64, r) + } + c.state.stateUpdated = false + c.state.valueUpdated = false + var result []uint64 + if (c.order == MinToMax && r <= c.maxAds) || (c.order == MaxToMin && r >= c.minAds) { + // for ; r <= c.maxAds; r++ { + c.search(*data, start, uint64(index), r, true, 0) + c.state.stateUpdated = false // reset + c.state.valueUpdated = false + result = make([]uint64, len(*data)) + copy(result, *data) + } + return result +} + +//search generates the combinations based on min and max number of ads +func (c *generator) search(data []uint64, start, index, r uint64, lazyLoad bool, reursionCount int) []uint64 { + + end := uint64(len(c.slotDurations) - 1) + + // Current combination is ready to be printed, print it + if index == r { + data1 := make([]uint64, len(data)) + for j := uint64(0); j < r; j++ { + data1[j] = data[j] + } + appendComb := true + if !lazyLoad { + appendComb = isValidCombination(c, data1) + } + if appendComb { + c.combinations = append(c.combinations, data1) + c.stats.currentCombinationCount++ + } + //util.Logf("%v", data1) + c.state.valueUpdated = true + return data1 + + } + + for i := start; i <= end && end+1+c.maxAds >= r-index; i++ { + if shouldUpdateAndReturn(c, start, index, r, lazyLoad, reursionCount, i, end) { + return data + } + data[index] = c.slotDurations[i] + currentDuration := i + c.search(data, currentDuration, index+1, r, lazyLoad, reursionCount+1) + } + + if lazyLoad && !c.state.stateUpdated { + c.state.combinationCounter++ + index = uint64(c.state.index) - 1 + updateState(c, lazyLoad, r, reursionCount, end, c.state.combinationCounter, index, c.slotDurations[end]) + } + return data +} + +// getNextElement assuming arr contains unique values +// other wise next elemt will be returned when first matching value of val found +// returns nextValue and its index +func getNextElement(arr []uint64, val uint64) (uint64, uint64) { + for i, e := range arr { + if e == val && i+1 < len(arr) { + return uint64(i) + 1, arr[i+1] + } + } + // assuming durations will never be 0 + return 0, 0 +} + +// updateState - is used in case of lazy loading +// It maintains the state of iterator by updating the required flags +func updateState(c *generator, lazyLoad bool, r uint64, reursionCount int, end uint64, i uint64, index uint64, valueAtEnd uint64) { + + if lazyLoad { + c.state.start = i + // set c.state.index = 0 when + // lastCombination contains, number X len(input) - 1 times starting from last index + // where X = last number present in the input + occurance := getOccurance(c, valueAtEnd) + //c.state.index = int64(c.state.combinationCounter) + // c.state.index = int64(index) + c.state.index = int64(index) + if occurance == r { + c.state.index = 0 + } + + // set c.state.combinationCounter + // c.state.combinationCounter++ + if c.state.combinationCounter >= r || c.state.combinationCounter >= uint64(len(c.slotDurations)) { + // LOGIC : to determine next value + // 1. get the value P at 0th index present in lastCombination + // 2. get the index of P + // 3. determine the next index i.e. index(p) + 1 = q + // 4. if q == r then set to 0 + diff := (uint64(len(c.state.lastCombination)) - occurance) + if diff > 0 { + eleIndex := diff - 1 + c.state.combinationCounter, _ = getNextElement(c.slotDurations, c.state.lastCombination[eleIndex]) + if c.state.combinationCounter == r { + // c.state.combinationCounter = 0 + } + c.state.start = c.state.combinationCounter + } else { + // end of r + } + } + // set r + // increament value of r if occurance == r + if occurance == r { + c.state.start = 0 + c.state.index = 0 + c.state.combinationCounter = 0 + if c.order == MinToMax { + c.state.r++ + } + if c.order == MaxToMin { + c.state.r-- + } + } + c.state.stateUpdated = true + } +} + +//shouldUpdateAndReturn checks if states should be updated in case of lazy loading +//If required it updates the state +func shouldUpdateAndReturn(c *generator, start, index, r uint64, lazyLoad bool, reursionCount int, i, end uint64) bool { + if lazyLoad && c.state.valueUpdated { + if uint64(reursionCount) <= r && !c.state.stateUpdated { + updateState(c, lazyLoad, r, reursionCount, end, i, index, c.slotDurations[end]) + } + return true + } + return false +} + +//getOccurance checks how many time given number is occured in c.state.lastCombination +func getOccurance(c *generator, valToCheck uint64) uint64 { + occurance := uint64(0) + for i := len(c.state.lastCombination) - 1; i >= 0; i-- { + if c.state.lastCombination[i] == valToCheck { + occurance++ + } + } + return occurance +} + +// subtractUnwantedRepeatations ensures subtracting repeating combination counts +// from combinations count computed by compute fuction for each r = min and max ads range +func subtractUnwantedRepeatations(c *generator) { + + series := getRepeatitionBreakUp(c) + + // subtract repeatations from noOfCombinations + // if not allowed for specific duration + totalUnwantedRepeatitions := uint64(0) + + for _, noOfAds := range c.slotDurationAdMap { + + // repeatation is not allowed for given duration + // get how many repeation can have for the duration + // at given level r = no of ads + + // Logic - to find repeatation for given duration at level r + // 1. if r = 1 - repeatition = 0 for any duration + // 2. if r = 2 - repeatition = 1 for any duration + // 3. if r >= 3 - repeatition = noOfCombinations(r) - noOfCombinations(r-2) + // 4. Using tetrahedral series determine the exact repeations w.r.t. noofads + // For Example, if noAds = 6 1 4 10 20 ... + // 1 => 1 repeatation for given number X in combination of 6 + // 4 => 4 repeatations for given number X in combination of 5 + // 10 => 10 repeatations for given number X in combination of 4 (i.e. combination containing ..,X,X,X....) + /* + 4 5 8 7 + 4 5 8 7 + n = 4 r = 1 repeat = 4 no-repeat = 4 0 0 0 0 + n = 4 r = 2 repeat = 10 no-repeat = 6 1 1 1 1 + n = 4 r = 3 repeat = 20 no-repeat = 4 4 4 4 4 + 1+3 1+3 1+3 1+3 + n = 4 r = 4 repeat = 35 no-repeat = 1 10 10 10 10 + 1+3+6 1+3+6 1+3+6 + + 4 5 8 7 18 + n = 5 r = 1 repeat = 5 no-repeat = 5 0 0 0 0 0 + n = 5 r = 2 repeat = 15 no-repeat = 10 1 1 1 1 1 + n = 5 r = 3 repeat = 35 no-repeat = 10 5 5 5 5 5 + 1+4 + n = 5 r = 4 repeat = 70 no-repeat = 5 15 15 15 15 15 + 1+4+10 + n = 5 r = 5 repeat = 126 no-repeat = 1 35 35 35 35 35 + 1+4+10+20 + n = 5 r = 6 repeat = 210 no-repeat = xxx 70 + 1+4+10+20+35 + + + 14 4 + n = 2 r = 1 repeat = 2 0 0 + n = 2 r = 2 repeat = 3 1 1 + + 15 + n = 1 r = 1 repeat = 1 0 + n = 1 r = 2 repeat = 1 1 + n = 1 r = 3 repeat = 1 1 + n = 1 r = 4 repeat = 1 1 + n = 1 r = 5 repeat = 1 1 + + + if r = 1 => r1rpt = 0 + if r = 2 => r2rpt = 1 + + if r >= 3 + + r3rpt = comb(r3 - 2) + r4rpt = comb(r4 - 2) + */ + + for r := c.minAds; r <= c.maxAds; r++ { + if r == 1 { + // duration will no be repeated when noOfAds = 1 + continue // 0 to be subtracted + } + // if r == 2 { + // // each duration will be repeated only once when noOfAds = 2 + // totalUnwantedRepeatitions++ + // // get total no of repeatations for combination of no > noOfAds + // continue + // } + + // r >= 3 + + // find out how many repeatations are allowed for given duration + // if allowedRepeatitions = 3, it means there are r = 3 ads for given duration + // hence, we can allow duration repeated ranging from r= 1 to r= 3 + // i.e. durations can not be repeated beyong r = 3 + // so we should discard the repeations beyond r = 3 i.e. from r = 4 to r = maxads + maxAllowedRepeatitions := noOfAds + + if maxAllowedRepeatitions > c.maxAds { + // maximum we can given upto c.maxads + maxAllowedRepeatitions = c.maxAds + } + + // if maxAllowedRepeatitions = 2 then + // repeatations > 2 should not be considered + // compute not allowed repeatitions + for i := maxAllowedRepeatitions + 1; i <= c.maxAds; i++ { + totalUnwantedRepeatitions += series[i] + } + + } + + } + // subtract all repeatations across all minads and maxads combinations count + c.stats.totalExpectedCombinations -= totalUnwantedRepeatitions +} + +//getRepeatitionBreakUp +func getRepeatitionBreakUp(c *generator) map[uint64]uint64 { + series := make(map[uint64]uint64, c.maxAds) // not using index 0 + ads := c.maxAds + series[ads] = 1 + seriesSum := uint64(1) + // always generate from r = 3 where r is no of ads + ads-- + for r := uint64(3); r <= c.maxAds; r++ { + // get repeations + repeatations := c.combinationCountMap[r-2] + // get next series item + nextItem := repeatations - seriesSum + if repeatations == seriesSum { + nextItem = repeatations + } + series[ads] = nextItem + seriesSum += nextItem + ads-- + } + + return series +} + +// getInvalidCombinatioCount returns no of invalid combination due to one of the following reason +// 1. Contains repeatition of durations, which has only one ad with it +// 2. Sum of duration (combinationo) is out of Pod Min or Pod Max duration +func (c *generator) getInvalidCombinatioCount() int { + return c.stats.repeatationsCount + c.stats.outOfRangeCount +} + +// GetCurrentCombinationCount returns current combination count +// irrespective of whether it is valid combination +func (c *generator) GetCurrentCombinationCount() int { + return c.stats.currentCombinationCount +} + +// GetExpectedCombinationCount returns total number for possible combinations without validations +// but subtracts repeatations for duration with single ad +func (c *generator) GetExpectedCombinationCount() uint64 { + return c.stats.totalExpectedCombinations +} + +// GetOutOfRangeCombinationsCount returns number of combinations currently rejected because of +// not satisfying Pod Minimum and Maximum duration +func (c *generator) GetOutOfRangeCombinationsCount() int { + return c.stats.outOfRangeCount +} + +//GetRepeatedDurationCombinationCount returns number of combinations currently rejected because of containing +//one or more repeatations of duration values, for which partners returned only single ad +func (c *generator) GetRepeatedDurationCombinationCount() int { + return c.stats.repeatationsCount +} + +// GetValidCombinationCount returns the number of valid combinations +// 1. Within range of Pod min and max duration +// 2. Repeatations are inline with input no of ads +func (c *generator) GetValidCombinationCount() int { + return c.stats.validCombinationCount +} diff --git a/endpoints/openrtb2/ctv/combination/adslot_combination_generator_test.go b/endpoints/openrtb2/ctv/combination/adslot_combination_generator_test.go new file mode 100644 index 00000000000..b701a7f1c1f --- /dev/null +++ b/endpoints/openrtb2/ctv/combination/adslot_combination_generator_test.go @@ -0,0 +1,238 @@ +package combination + +import ( + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/util" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +var testBidResponseMaxDurations = []struct { + scenario string + // index 0 = Max Duration of Ad present in Bid Response + // index 1 = Number of Ads for given Max Duration (Index 0) + responseMaxDurations [][2]uint64 + podMinDuration int // Pod Minimum duration value present in origin Video Ad Pod Request + podMaxDuration int // Pod Maximum duration value present in origin Video Ad Pod Request + minAds int // Minimum Ads value present in origin Video Ad Pod Request + maxAds int // Maximum Ads value present in origin Video Ad Pod Request +}{ + { + scenario: "TC1-Single_Value", + responseMaxDurations: [][2]uint64{{14, 1}, {4, 3}}, + podMinDuration: 10, podMaxDuration: 14, minAds: 1, maxAds: 2, + }, { + scenario: "TC2-Multi_Value", + responseMaxDurations: [][2]uint64{{1, 2}, {2, 2}, {3, 2}, {4, 2}, {5, 2}}, + podMinDuration: 10, podMaxDuration: 14, minAds: 1, maxAds: 2, + }, { + scenario: "TC3-max_ads = input_bid_durations", + responseMaxDurations: [][2]uint64{{4, 2}, {5, 2}, {8, 2}, {7, 2}}, + podMinDuration: 10, podMaxDuration: 50, minAds: 2, maxAds: 5, + }, { + scenario: "TC4-max_ads < input_bid_durations (test 1)", + responseMaxDurations: [][2]uint64{{4, 2}, {5, 2}, {8, 2}, {7, 2}}, + podMinDuration: 10, podMaxDuration: 17, minAds: 3, maxAds: 3, + }, { + scenario: "TC5-max_ads (1) < input_bid_durations (test 1)", + responseMaxDurations: [][2]uint64{{4, 2}, {5, 2}, {8, 2}, {7, 2}}, + podMinDuration: 10, podMaxDuration: 14, minAds: 3, maxAds: 1, + }, { + scenario: "TC6-max_ads < input_bid_durations (test 2)", + responseMaxDurations: [][2]uint64{{4, 2}, {5, 2}, {8, 2}, {7, 2}}, + podMinDuration: 10, podMaxDuration: 14, minAds: 3, maxAds: 2, + }, { + scenario: "TC7-max_ads > input_bid_durations (test 1)", + responseMaxDurations: [][2]uint64{{4, 2}, {5, 1}, {8, 2}, {7, 2}}, + podMinDuration: 10, podMaxDuration: 50, minAds: 4, maxAds: 4, + }, + // { + + // // 4 - c1, c2, : 5 - c3 : 6 - c4, c5, 8 : c7 + // scenario: "TC8-max_ads (20 ads) > input_bid_durations (test 2)", + // responseMaxDurations: []uint64{4, 5, 8, 7}, + // podMinDuration: 10, podMaxDuration: 14, minAds: 3, maxAds: 20, + // combinations: [][]int64{{14}}}, + { + + // 4 - c1, c2, : 5 - c3 : 6 - c4, c5, 8 : c7 + scenario: "TC6-max_ads (20 ads) > input_bid_durations-repeatation_not_allowed", + responseMaxDurations: [][2]uint64{{4, 2}, {5, 2}, {8, 2}, {7, 2}}, + podMinDuration: 10, podMaxDuration: 14, minAds: 3, maxAds: 2, + }, + // { + + // // 4 - c1, c2, : 5 - c3 : 6 - c4, c5, 8 : c7 + // scenario: "TC8-max_ads (20 ads) > input_bid_durations (no repitations)", + // responseMaxDurations: []uint64{4, 5, 8, 7}, + // podMinDuration: 10, podMaxDuration: 14, minAds: 3, maxAds: 20, + // combinations: [][]int64{{14}}, + // allowRepetitationsForEligibleDurations: "true", // no repeitations + // }, + + // { + + // // 4 - c1, c2, : 5 - c3 : 6 - c4, c5, 8 : c7 + // scenario: "TC9-max_ads = input_bid_durations = 4", + // responseMaxDurations: []uint64{4, 4, 4, 4}, + // podMinDuration: 10, podMaxDuration: 14, minAds: 3, maxAds: 4, + // combinations: [][]int64{{14}}, allowRepetitationsForEligibleDurations: "true"}, + { + scenario: "TC10-max_ads 0", + responseMaxDurations: [][2]uint64{{4, 2}, {4, 2}, {4, 2}, {4, 2}}, + podMinDuration: 10, podMaxDuration: 14, minAds: 3, maxAds: 0, + }, { + scenario: "TC11-max_ads =5-input-empty", + responseMaxDurations: [][2]uint64{}, + podMinDuration: 10, podMaxDuration: 14, minAds: 3, maxAds: 0, + }, { + scenario: "TC12-max_ads =5-input-empty-no-repeatation", + responseMaxDurations: [][2]uint64{{25, 2}, {30, 2}, {76, 2}, {10, 2}, {88, 2}}, + podMinDuration: 10, podMaxDuration: 229, minAds: 1, maxAds: 4, + }, { + scenario: "TC13-max_ads = input = 10-without-repeatation", + responseMaxDurations: [][2]uint64{{25, 2}, {30, 2}, {76, 2}, {10, 2}, {88, 2}, {34, 2}, {37, 2}, {67, 2}, {89, 2}, {45, 2}}, + podMinDuration: 10, podMaxDuration: 14, minAds: 3, maxAds: 10, + }, { + scenario: "TC14-single duration: single ad", + responseMaxDurations: [][2]uint64{{15, 1}}, + podMinDuration: 10, podMaxDuration: 15, minAds: 1, maxAds: 5, + }, { + scenario: "TC15-exact-pod-duration", + responseMaxDurations: [][2]uint64{{25, 2}, {30, 2}, {76, 2}, {10, 2}, {88, 2}}, + podMinDuration: 200, podMaxDuration: 200, minAds: 8, maxAds: 10, + }, { + scenario: "TC16-50ads", + responseMaxDurations: [][2]uint64{{25, 2}, {30, 2}, {76, 2}, {10, 2}, {88, 2}}, + podMinDuration: 200, podMaxDuration: 200, minAds: 10, maxAds: 10, /*50*/ + }, +} + +func BenchmarkPodDurationCombinationGenerator(b *testing.B) { + for _, test := range testBidResponseMaxDurations { + b.Run(test.scenario, func(b *testing.B) { + c := new(generator) + config := new(openrtb_ext.VideoAdPod) + config.MinAds = &test.minAds + config.MaxAds = &test.maxAds + config.MinDuration = &test.podMinDuration + config.MaxDuration = &test.podMaxDuration + + for n := 0; n < b.N; n++ { + for true { + comb := c.Next() + if nil == comb || len(comb) == 0 { + break + } + } + } + }) + } +} + +// TestMaxToMinCombinationGenerator tests the genreration of +// combinations from min to max combinations +// e.g. +// 1 +// 1 2 +// 1 2 3 +// 1 2 3 4 +func TestMinToMaxCombinationGenerator(t *testing.T) { + for _, test := range testBidResponseMaxDurations { + // if test.scenario != "TC1-Single_Value" { + // continue + // } + // eOut := readExpectedOutput() + // fmt.Println(eOut) + t.Run(test.scenario, func(t *testing.T) { + c := new(generator) + config := new(openrtb_ext.VideoAdPod) + config.MinAds = &test.minAds + config.MaxAds = &test.maxAds + c.Init(uint64(test.podMinDuration), uint64(test.podMaxDuration), config, test.responseMaxDurations, MinToMax) + validator(t, c) + }) + } +} + +// TestMaxToMinCombinationGenerator tests the genreration of +// combinations from max to min combinations +// e.g. +// 1 2 3 4 +// 1 2 3 +// 1 2 +// 1 +func TestMaxToMinCombinationGenerator(t *testing.T) { + for _, test := range testBidResponseMaxDurations { + t.Run(test.scenario, func(t *testing.T) { + c := new(generator) + config := new(openrtb_ext.VideoAdPod) + config.MinAds = &test.minAds + config.MaxAds = &test.maxAds + c.Init(uint64(test.podMinDuration), uint64(test.podMaxDuration), config, test.responseMaxDurations, MaxToMin) + validator(t, c) + }) + } +} + +func validator(t *testing.T, c *generator) { + expectedOutput := c.searchAll() + // determine expected size of expected output + // subtract invalid combinations size + actualOutput := make([][]uint64, len(expectedOutput)) + + cnt := 0 + for true { + comb := c.Next() + if comb == nil || len(comb) == 0 { + break + } + //ctv.Logf("%v", comb) + //fmt.Print("count = ", c.currentCombinationCount, " :: ", comb, "\n") + fmt.Println("e = ", (expectedOutput)[cnt], "\t : a = ", comb) + val := make([]uint64, len(comb)) + copy(val, comb) + actualOutput[cnt] = val + cnt++ + } + + if expectedOutput != nil { + // compare results + for i := uint64(0); i < uint64(len(expectedOutput)); i++ { + if expectedOutput[i] == nil { + continue + } + for j := uint64(0); j < uint64(len(expectedOutput[i])); j++ { + if expectedOutput[i][j] == actualOutput[i][j] { + } else { + + assert.Fail(t, "expectedOutput[", i, "][", j, "] != actualOutput[", i, "][", j, "] ", expectedOutput[i][j], " !=", actualOutput[i][j]) + + } + } + + } + } + + assert.Equal(t, expectedOutput, actualOutput) + assert.ElementsMatch(t, expectedOutput, actualOutput) + + util.Logf("Total combinations generated = %v", c.stats.currentCombinationCount) + util.Logf("Total valid combinations = %v", c.stats.validCombinationCount) + util.Logf("Total repeated combinations = %v", c.stats.repeatationsCount) + util.Logf("Total outofrange combinations = %v", c.stats.outOfRangeCount) + util.Logf("Total combinations expected = %v", c.stats.totalExpectedCombinations) +} + +func readExpectedOutput() map[string][][]int { + file, _ := os.Open("test_input/TC_1,json") + var bytes []byte + file.Read(bytes) + eOut := make(map[string][][]int, 0) + json.Unmarshal(bytes, eOut) + return eOut +} diff --git a/endpoints/openrtb2/ctv/combination/combination.go b/endpoints/openrtb2/ctv/combination/combination.go new file mode 100644 index 00000000000..4f4f1987354 --- /dev/null +++ b/endpoints/openrtb2/ctv/combination/combination.go @@ -0,0 +1,70 @@ +// Package combination generates possible ad pod response +// based on bid response durations. It ensures that generated +// combination is satifying ad pod request configurations like +// Min Pod Duation, Maximum Pod Duration, Minimum number of ads, Maximum number of Ads. +// It also considers number of bids received for given duration +// For Example, if for 60 second duration we have 2 bids then +// then it will ensure combination contains at most 2 repeatations of 60 sec; not more than that +package combination + +import ( + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/types" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// ICombination ... +type ICombination interface { + Get() []int +} + +// Combination ... +type Combination struct { + ICombination + data []int + generator generator + config *openrtb_ext.VideoAdPod + order int // order of combination generator +} + +// NewCombination ... Generates on demand valid combinations +// Valid combinations are those who satisifies +// 1. Pod Min Max duration +// 2. minAds <= size(combination) <= maxads +// 3. If Combination contains repeatition for given duration then +// repeatitions are <= no of ads received for the duration +// Use Get method to start getting valid combinations +func NewCombination(buckets types.BidsBuckets, podMinDuration, podMaxDuration uint64, config *openrtb_ext.VideoAdPod) *Combination { + generator := new(generator) + durationBidsCnts := make([][2]uint64, 0) + for duration, bids := range buckets { + durationBidsCnts = append(durationBidsCnts, [2]uint64{uint64(duration), uint64(len(bids))}) + } + generator.Init(podMinDuration, podMaxDuration, config, durationBidsCnts, MaxToMin) + return &Combination{ + generator: *generator, + config: config, + } +} + +// Get next valid combination +// Retuns empty slice if all combinations are generated +func (c *Combination) Get() []int { + nextComb := c.generator.Next() + nextCombInt := make([]int, len(nextComb)) + cnt := 0 + for _, duration := range nextComb { + nextCombInt[cnt] = int(duration) + cnt++ + } + return nextCombInt +} + +const ( + // MinToMax tells combination generator to generate combinations + // starting from Min Ads to Max Ads + MinToMax = iota + + // MaxToMin tells combination generator to generate combinations + // starting from Max Ads to Min Ads + MaxToMin +) diff --git a/endpoints/openrtb2/ctv/combination/combination_test.go b/endpoints/openrtb2/ctv/combination/combination_test.go new file mode 100644 index 00000000000..14f32eddcb6 --- /dev/null +++ b/endpoints/openrtb2/ctv/combination/combination_test.go @@ -0,0 +1,38 @@ +package combination + +import ( + "testing" + + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/types" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestCombination(t *testing.T) { + buckets := make(types.BidsBuckets) + + dBids := make([]*types.Bid, 0) + for i := 1; i <= 3; i++ { + bid := new(types.Bid) + bid.Duration = 10 * i + dBids = append(dBids, bid) + buckets[bid.Duration] = dBids + } + + config := new(openrtb_ext.VideoAdPod) + config.MinAds = new(int) + *config.MinAds = 2 + config.MaxAds = new(int) + *config.MaxAds = 4 + + c := NewCombination(buckets, 30, 70, config) + + for true { + comb := c.generator.Next() + if nil == comb || len(comb) == 0 { + assert.True(t, nil == comb || len(comb) == 0) + break + } + } + +} diff --git a/endpoints/openrtb2/ctv/combination/test_input/TC_1.json b/endpoints/openrtb2/ctv/combination/test_input/TC_1.json new file mode 100644 index 00000000000..498204657f2 --- /dev/null +++ b/endpoints/openrtb2/ctv/combination/test_input/TC_1.json @@ -0,0 +1,4 @@ +{ + "1" : [[14], [4]], + "2" : [[14,14],[14,4],[4,4]] +} \ No newline at end of file diff --git a/endpoints/openrtb2/ctv/constant/constant.go b/endpoints/openrtb2/ctv/constant/constant.go new file mode 100644 index 00000000000..f39e28d2b96 --- /dev/null +++ b/endpoints/openrtb2/ctv/constant/constant.go @@ -0,0 +1,53 @@ +package constant + +const ( + CTVImpressionIDSeparator = `_` + CTVImpressionIDFormat = `%v` + CTVImpressionIDSeparator + `%v` + CTVUniqueBidIDFormat = `%v-%v` + HTTPPrefix = `http` + + //VAST Constants + VASTDefaultVersion = 2.0 + VASTMaxVersion = 4.0 + VASTDefaultVersionStr = `2.0` + VASTDefaultTag = `` + VASTElement = `VAST` + VASTAdElement = `Ad` + VASTWrapperElement = `Wrapper` + VASTAdTagURIElement = `VASTAdTagURI` + VASTVersionAttribute = `version` + VASTSequenceAttribute = `sequence` + + CTVAdpod = `adpod` + CTVOffset = `offset` +) + +var ( + VASTVersionsStr = []string{"0", "1.0", "2.0", "3.0", "4.0"} +) + +//BidStatus contains bids filtering reason +type BidStatus = int + +const ( + //StatusOK ... + StatusOK BidStatus = 0 + //StatusWinningBid ... + StatusWinningBid BidStatus = 1 + //StatusCategoryExclusion ... + StatusCategoryExclusion BidStatus = 2 + //StatusDomainExclusion ... + StatusDomainExclusion BidStatus = 3 + //StatusDurationMismatch ... + StatusDurationMismatch BidStatus = 4 +) + +// MonitorKey provides the unique key for moniroting the algorithms +type MonitorKey string + +const ( + // CombinationGeneratorV1 ... + CombinationGeneratorV1 MonitorKey = "comp_exclusion_v1" + // CompetitiveExclusionV1 ... + CompetitiveExclusionV1 MonitorKey = "comp_exclusion_v1" +) diff --git a/endpoints/openrtb2/ctv/impressions/by_duration_range_test.go b/endpoints/openrtb2/ctv/impressions/by_duration_range_test.go new file mode 100644 index 00000000000..40760b56c9b --- /dev/null +++ b/endpoints/openrtb2/ctv/impressions/by_duration_range_test.go @@ -0,0 +1,177 @@ +package impressions + +import ( + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestGetImpressionsByDurationRanges(t *testing.T) { + type args struct { + policy openrtb_ext.OWVideoLengthMatchingPolicy + durations []int + maxAds int + adMinDuration int + adMaxDuration int + } + type want struct { + imps [][2]int64 + } + + tests := []struct { + name string + args args + want want + }{ + { + // do not generate impressions + name: "no_adpod_context", + args: args{}, + want: want{ + imps: [][2]int64{}, + }, + }, + { + // do not generate impressions + name: "nil_durations", + args: args{ + durations: nil, + }, + want: want{ + imps: make([][2]int64, 0), + }, + }, + { + // do not generate impressions + name: "empty_durations", + args: args{ + durations: make([]int, 0), + }, + want: want{ + imps: make([][2]int64, 0), + }, + }, + { + name: "zero_valid_durations_under_boundary", + args: args{ + policy: openrtb_ext.OWExactVideoLengthsMatching, + durations: []int{5, 10, 15}, + maxAds: 5, + adMinDuration: 2, + adMaxDuration: 2, + }, + want: want{ + imps: [][2]int64{}, + }, + }, + { + name: "zero_valid_durations_out_of_bound", + args: args{ + policy: openrtb_ext.OWExactVideoLengthsMatching, + durations: []int{5, 10, 15}, + maxAds: 5, + adMinDuration: 20, + adMaxDuration: 20, + }, + want: want{ + imps: [][2]int64{}, + }, + }, + { + name: "valid_durations_less_than_maxAds", + args: args{ + policy: openrtb_ext.OWExactVideoLengthsMatching, + durations: []int{5, 10, 15, 20, 25}, + maxAds: 5, + adMinDuration: 10, + adMaxDuration: 20, + }, + want: want{ + imps: [][2]int64{ + {10, 10}, + {15, 15}, + {20, 20}, + //got repeated because of current video duration impressions are less than maxads + {10, 10}, + {15, 15}, + }, + }, + }, + { + name: "valid_durations_greater_than_maxAds", + args: args{ + policy: openrtb_ext.OWExactVideoLengthsMatching, + durations: []int{5, 10, 15, 20, 25}, + maxAds: 2, + adMinDuration: 10, + adMaxDuration: 20, + }, + want: want{ + imps: [][2]int64{ + {10, 10}, + {15, 15}, + {20, 20}, + }, + }, + }, + { + name: "roundup_policy_valid_durations", + args: args{ + policy: openrtb_ext.OWRoundupVideoLengthMatching, + durations: []int{5, 10, 15, 20, 25}, + maxAds: 5, + adMinDuration: 10, + adMaxDuration: 20, + }, + want: want{ + imps: [][2]int64{ + {10, 10}, + {10, 15}, + {10, 20}, + {10, 10}, + {10, 15}, + }, + }, + }, + { + name: "roundup_policy_zero_valid_durations", + args: args{ + policy: openrtb_ext.OWRoundupVideoLengthMatching, + durations: []int{5, 10, 15, 20, 25}, + maxAds: 5, + adMinDuration: 30, + adMaxDuration: 30, + }, + want: want{ + imps: [][2]int64{}, + }, + }, + { + name: "roundup_policy_valid_max_ads_more_than_max_ads", + args: args{ + policy: openrtb_ext.OWRoundupVideoLengthMatching, + durations: []int{5, 10, 15, 20, 25}, + maxAds: 2, + adMinDuration: 10, + adMaxDuration: 20, + }, + want: want{ + imps: [][2]int64{ + {10, 10}, + {10, 15}, + {10, 20}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := tt.args + gen := newByDurationRanges(args.policy, args.durations, args.maxAds, args.adMinDuration, args.adMaxDuration) + imps := gen.Get() + assert.Equal(t, tt.want.imps, imps) + }) + } +} diff --git a/endpoints/openrtb2/ctv/impressions/by_duration_ranges.go b/endpoints/openrtb2/ctv/impressions/by_duration_ranges.go new file mode 100644 index 00000000000..6812b7d6c6e --- /dev/null +++ b/endpoints/openrtb2/ctv/impressions/by_duration_ranges.go @@ -0,0 +1,91 @@ +package impressions + +import ( + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/util" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// byDurRangeConfig struct will be used for creating impressions object based on list of duration ranges +type byDurRangeConfig struct { + IImpressions //IImpressions interface + policy openrtb_ext.OWVideoLengthMatchingPolicy //duration matching algorithm round/exact + durations []int //durations list of durations in seconds used for creating impressions object + maxAds int //maxAds is number of max impressions can be created + adMinDuration int //adpod slot mininum duration + adMaxDuration int //adpod slot maximum duration +} + +// newByDurationRanges will create new object ob byDurRangeConfig for creating impressions for adpod request +func newByDurationRanges(policy openrtb_ext.OWVideoLengthMatchingPolicy, durations []int, + maxAds, adMinDuration, adMaxDuration int) byDurRangeConfig { + + return byDurRangeConfig{ + policy: policy, + durations: durations, + maxAds: maxAds, + adMinDuration: adMinDuration, + adMaxDuration: adMaxDuration, + } +} + +// Get function returns lists of min,max duration ranges ganerated based on durations +// it will return valid durations, duration must be within podMinDuration and podMaxDuration range +// if len(durations) < maxAds then clone valid durations from starting till we reach maxAds length +func (c *byDurRangeConfig) Get() [][2]int64 { + if len(c.durations) == 0 { + util.Logf("durations is nil. [%v] algorithm returning not generated impressions", c.Algorithm()) + return make([][2]int64, 0) + } + + isRoundupDurationMatchingPolicy := (openrtb_ext.OWRoundupVideoLengthMatching == c.policy) + var minDuration = -1 + var validDurations []int + + for _, dur := range c.durations { + // validate durations (adminduration <= lineitemduration <= admaxduration) (adpod adslot min and max duration) + if !(c.adMinDuration <= dur && dur <= c.adMaxDuration) { + continue // invalid duration + } + + // finding minimum duration for roundup policy, this may include valid or invalid duration + if isRoundupDurationMatchingPolicy && (minDuration == -1 || minDuration >= dur) { + minDuration = dur + } + + validDurations = append(validDurations, dur) + } + + imps := make([][2]int64, 0) + for _, dur := range validDurations { + /* + minimum value is depends on duration matching policy + openrtb_ext.OWAdPodRoundupDurationMatching (round): minduration would be min(duration) + openrtb_ext.OWAdPodExactDurationMatching (exact) or empty: minduration would be same as maxduration + */ + if isRoundupDurationMatchingPolicy { + imps = append(imps, [2]int64{int64(minDuration), int64(dur)}) + } else { + imps = append(imps, [2]int64{int64(dur), int64(dur)}) + } + } + + //calculate max ads + maxAds := c.maxAds + if len(validDurations) > maxAds { + maxAds = len(validDurations) + } + + //adding extra impressions incase of total impressions generated are less than pod max ads. + if len(imps) > 0 { + for i := 0; len(imps) < maxAds; i++ { + imps = append(imps, [2]int64{imps[i][0], imps[i][1]}) + } + } + + return imps +} + +// Algorithm returns MinMaxAlgorithm +func (c *byDurRangeConfig) Algorithm() Algorithm { + return ByDurationRanges +} diff --git a/endpoints/openrtb2/ctv/impressions/helper.go b/endpoints/openrtb2/ctv/impressions/helper.go new file mode 100644 index 00000000000..7fba95d704d --- /dev/null +++ b/endpoints/openrtb2/ctv/impressions/helper.go @@ -0,0 +1,139 @@ +package impressions + +import ( + "math" + + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/util" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// newConfig initializes the generator instance +func newConfig(podMinDuration, podMaxDuration int64, vPod openrtb_ext.VideoAdPod) generator { + config := generator{} + config.totalSlotTime = new(int64) + // configure requested pod + config.requested = pod{ + podMinDuration: podMinDuration, + podMaxDuration: podMaxDuration, + slotMinDuration: int64(*vPod.MinDuration), + slotMaxDuration: int64(*vPod.MaxDuration), + minAds: int64(*vPod.MinAds), + maxAds: int64(*vPod.MaxAds), + } + + // configure internal object (FOR INTERNAL USE ONLY) + // this is used for internal computation and may contains modified values of + // slotMinDuration and slotMaxDuration in multiples of multipleOf factor + // This function will by deault intialize this pod with same values + // as of requestedPod + // There is another function newConfigWithMultipleOf, which computes and assigns + // values to this object + config.internal = internal{ + slotMinDuration: config.requested.slotMinDuration, + slotMaxDuration: config.requested.slotMaxDuration, + } + return config +} + +// newConfigWithMultipleOf initializes the generator instance +// it internally calls newConfig to obtain the generator instance +// then it computes closed to factor basedon 'multipleOf' parameter value +// and accordingly determines the Pod Min/Max and Slot Min/Max values for internal +// computation only. +func newConfigWithMultipleOf(podMinDuration, podMaxDuration int64, vPod openrtb_ext.VideoAdPod, multipleOf int64) generator { + config := newConfig(podMinDuration, podMaxDuration, vPod) + + // try to compute slot level min and max duration values in multiple of + // given number. If computed values are overlapping then prefer requested + if config.requested.slotMinDuration == config.requested.slotMaxDuration { + /*TestCase 30*/ + util.Logf("requested.SlotMinDuration = requested.SlotMaxDuration = %v\n", config.requested.slotMinDuration) + config.internal.slotMinDuration = config.requested.slotMinDuration + config.internal.slotMaxDuration = config.requested.slotMaxDuration + } else { + config.internal.slotMinDuration = getClosestFactorForMinDuration(int64(config.requested.slotMinDuration), multipleOf) + config.internal.slotMaxDuration = getClosestFactorForMaxDuration(int64(config.requested.slotMaxDuration), multipleOf) + config.internal.slotDurationComputed = true + if config.internal.slotMinDuration > config.internal.slotMaxDuration { + // computed slot min duration > computed slot max duration + // avoid overlap and prefer requested values + config.internal.slotMinDuration = config.requested.slotMinDuration + config.internal.slotMaxDuration = config.requested.slotMaxDuration + // update marker indicated slot duation values are not computed + // this required by algorithm in computeTimeForEachAdSlot function + config.internal.slotDurationComputed = false + } + } + return config +} + +// Returns true if num is multipleof second argument. False otherwise +func isMultipleOf(num, multipleOf int64) bool { + return math.Mod(float64(num), float64(multipleOf)) == 0 +} + +// Returns closest factor for num, with respect input multipleOf +// Example: Closest Factor of 9, in multiples of 5 is '10' +func getClosestFactor(num, multipleOf int64) int64 { + return int64(math.Round(float64(num)/float64(multipleOf)) * float64(multipleOf)) +} + +// Returns closestfactor of MinDuration, with respect to multipleOf +// If computed factor < MinDuration then it will ensure and return +// close factor >= MinDuration +func getClosestFactorForMinDuration(MinDuration int64, multipleOf int64) int64 { + closedMinDuration := getClosestFactor(MinDuration, multipleOf) + + if closedMinDuration == 0 { + return multipleOf + } + + if closedMinDuration < MinDuration { + return closedMinDuration + multipleOf + } + + return closedMinDuration +} + +// Returns closestfactor of maxduration, with respect to multipleOf +// If computed factor > maxduration then it will ensure and return +// close factor <= maxduration +func getClosestFactorForMaxDuration(maxduration, multipleOf int64) int64 { + closedMaxDuration := getClosestFactor(maxduration, multipleOf) + if closedMaxDuration == maxduration { + return maxduration + } + + // set closest maxduration closed to masduration + for i := closedMaxDuration; i <= maxduration; { + if closedMaxDuration < maxduration { + closedMaxDuration = i + multipleOf + i = closedMaxDuration + } + } + + if closedMaxDuration > maxduration { + duration := closedMaxDuration - multipleOf + if duration == 0 { + // return input value as is instead of zero to avoid NPE + return maxduration + } + return duration + } + + return closedMaxDuration +} + +// Returns Maximum number out off 2 input numbers +func max(num1, num2 int64) int64 { + + if num1 > num2 { + return num1 + } + + if num2 > num1 { + return num2 + } + // both must be equal here + return num1 +} diff --git a/endpoints/openrtb2/ctv/impressions/impression_generator.go b/endpoints/openrtb2/ctv/impressions/impression_generator.go new file mode 100644 index 00000000000..4f9edef5886 --- /dev/null +++ b/endpoints/openrtb2/ctv/impressions/impression_generator.go @@ -0,0 +1,348 @@ +package impressions + +import ( + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/util" +) + +// generator contains Pod Minimum Duration, Pod Maximum Duration, Slot Minimum Duration and Slot Maximum Duration +// It holds additional attributes required by this algorithm for internal computation. +// It contains Slots attribute. This attribute holds the output of this algorithm +type generator struct { + IImpressions + Slots [][2]int64 // Holds Minimum and Maximum duration (in seconds) for each Ad Slot. Length indicates total number of Ad Slots/ Impressions for given Ad Pod + totalSlotTime *int64 // Total Sum of all Ad Slot durations (in seconds) + freeTime int64 // Remaining Time (in seconds) not allocated. It is compared with RequestedPodMaxDuration + slotsWithZeroTime *int64 // Indicates number of slots with zero time (starting from 1). + // requested holds all the requested information received + requested pod + // internal holds the slot duration values closed to original value and multiples of X. + // It helps in plotting impressions with duration values in multiples of given number + internal internal +} + +// pod for internal computation +// should not be used outside +type pod struct { + minAds int64 + maxAds int64 + slotMinDuration int64 + slotMaxDuration int64 + podMinDuration int64 + podMaxDuration int64 +} + +// internal (FOR INTERNAL USE ONLY) holds the computed values slot min and max duration +// in multiples of given number. It also holds slotDurationComputed flag +// if slotDurationComputed = false, it means values computed were overlapping +type internal struct { + slotMinDuration int64 + slotMaxDuration int64 + slotDurationComputed bool +} + +// Get returns the number of Ad Slots/Impression that input Ad Pod can have. +// It returns List 2D array containing following +// 1. Dimension 1 - Represents the minimum duration of an impression +// 2. Dimension 2 - Represents the maximum duration of an impression +func (config *generator) Get() [][2]int64 { + util.Logf("Pod Config with Internal Computation (using multiples of %v) = %+v\n", multipleOf, config) + totalAds := computeTotalAds(*config) + timeForEachSlot := computeTimeForEachAdSlot(*config, totalAds) + + config.Slots = make([][2]int64, totalAds) + config.slotsWithZeroTime = new(int64) + *config.slotsWithZeroTime = totalAds + util.Logf("Plotted Ad Slots / Impressions of size = %v\n", len(config.Slots)) + // iterate over total time till it is < cfg.RequestedPodMaxDuration + time := int64(0) + util.Logf("Started allocating durations to each Ad Slot / Impression\n") + fillZeroSlotsOnPriority := true + noOfZeroSlotsFilledByLastRun := int64(0) + *config.totalSlotTime = 0 + for time < config.requested.podMaxDuration { + adjustedTime, slotsFull := config.addTime(timeForEachSlot, fillZeroSlotsOnPriority) + time += adjustedTime + timeForEachSlot = computeTimeLeastValue(config.requested.podMaxDuration-time, config.requested.slotMaxDuration-timeForEachSlot) + if slotsFull { + util.Logf("All slots are full of their capacity. validating slots\n") + break + } + + // instruct for filling zero capacity slots on priority if + // 1. shouldAdjustSlotWithZeroDuration returns true + // 2. there are slots with 0 duration + // 3. there is at least ont slot with zero duration filled by last iteration + fillZeroSlotsOnPriority = false + noOfZeroSlotsFilledByLastRun = *config.slotsWithZeroTime - noOfZeroSlotsFilledByLastRun + if config.shouldAdjustSlotWithZeroDuration() && *config.slotsWithZeroTime > 0 && noOfZeroSlotsFilledByLastRun > 0 { + fillZeroSlotsOnPriority = true + } + } + util.Logf("Completed allocating durations to each Ad Slot / Impression\n") + + // validate slots + config.validateSlots() + + // log free time if present to stats server + // also check algoritm computed the no. of ads + if config.requested.podMaxDuration-time > 0 && len(config.Slots) > 0 { + config.freeTime = config.requested.podMaxDuration - time + util.Logf("TO STATS SERVER : Free Time not allocated %v sec", config.freeTime) + } + + util.Logf("\nTotal Impressions = %v, Total Allocated Time = %v sec (out of %v sec, Max Pod Duration)\n%v", len(config.Slots), *config.totalSlotTime, config.requested.podMaxDuration, config.Slots) + return config.Slots +} + +// Returns total number of Ad Slots/ impressions that the Ad Pod can have +func computeTotalAds(cfg generator) int64 { + if cfg.internal.slotMaxDuration <= 0 || cfg.internal.slotMinDuration <= 0 { + util.Logf("Either cfg.slotMaxDuration or cfg.slotMinDuration or both are <= 0. Hence, totalAds = 0") + return 0 + } + minAds := cfg.requested.podMaxDuration / cfg.internal.slotMaxDuration + maxAds := cfg.requested.podMaxDuration / cfg.internal.slotMinDuration + + util.Logf("Computed minAds = %v , maxAds = %v\n", minAds, maxAds) + + totalAds := max(minAds, maxAds) + util.Logf("Computed max(minAds, maxAds) = totalAds = %v\n", totalAds) + + if totalAds < cfg.requested.minAds { + totalAds = cfg.requested.minAds + util.Logf("Computed totalAds < requested minAds (%v). Hence, setting totalAds = minAds = %v\n", cfg.requested.minAds, totalAds) + } + if totalAds > cfg.requested.maxAds { + totalAds = cfg.requested.maxAds + util.Logf("Computed totalAds > requested maxAds (%v). Hence, setting totalAds = maxAds = %v\n", cfg.requested.maxAds, totalAds) + } + util.Logf("Computed Final totalAds = %v [%v <= %v <= %v]\n", totalAds, cfg.requested.minAds, totalAds, cfg.requested.maxAds) + return totalAds +} + +// Returns duration in seconds that can be allocated to each Ad Slot +// Accepts cfg containing algorithm configurations and totalAds containing Total number of +// Ad Slots / Impressions that the Ad Pod can have. +func computeTimeForEachAdSlot(cfg generator, totalAds int64) int64 { + // Compute time for each ad + if totalAds <= 0 { + util.Logf("totalAds = 0, Hence timeForEachSlot = 0") + return 0 + } + timeForEachSlot := cfg.requested.podMaxDuration / totalAds + + util.Logf("Computed timeForEachSlot = %v (podMaxDuration/totalAds) (%v/%v)\n", timeForEachSlot, cfg.requested.podMaxDuration, totalAds) + + if timeForEachSlot < cfg.internal.slotMinDuration { + timeForEachSlot = cfg.internal.slotMinDuration + util.Logf("Computed timeForEachSlot < requested slotMinDuration (%v). Hence, setting timeForEachSlot = slotMinDuration = %v\n", cfg.internal.slotMinDuration, timeForEachSlot) + } + + if timeForEachSlot > cfg.internal.slotMaxDuration { + timeForEachSlot = cfg.internal.slotMaxDuration + util.Logf("Computed timeForEachSlot > requested slotMaxDuration (%v). Hence, setting timeForEachSlot = slotMaxDuration = %v\n", cfg.internal.slotMaxDuration, timeForEachSlot) + } + + // Case - Exact slot duration is given. No scope for finding multiples + // of given number. Prefer to return computed timeForEachSlot + // In such case timeForEachSlot no necessarily to be multiples of given number + if cfg.requested.slotMinDuration == cfg.requested.slotMaxDuration { + util.Logf("requested.slotMinDuration = requested.slotMaxDuration = %v. Hence, not computing multiples of %v value.", cfg.requested.slotMaxDuration, multipleOf) + return timeForEachSlot + } + + // Case II - timeForEachSlot*totalAds > podmaxduration + // In such case prefer to return cfg.podMaxDuration / totalAds + // In such case timeForEachSlot no necessarily to be multiples of given number + if (timeForEachSlot * totalAds) > cfg.requested.podMaxDuration { + util.Logf("timeForEachSlot*totalAds (%v) > cfg.requested.podMaxDuration (%v) ", timeForEachSlot*totalAds, cfg.requested.podMaxDuration) + util.Logf("Hence, not computing multiples of %v value.", multipleOf) + // need that division again + return cfg.requested.podMaxDuration / totalAds + } + + // ensure timeForEachSlot is multipleof given number + if cfg.internal.slotDurationComputed && !isMultipleOf(timeForEachSlot, multipleOf) { + // get close to value of multiple + // here we muse get either cfg.SlotMinDuration or cfg.SlotMaxDuration + // these values are already pre-computed in multiples of given number + timeForEachSlot = getClosestFactor(timeForEachSlot, multipleOf) + util.Logf("Computed closet factor %v, in multiples of %v for timeForEachSlot\n", timeForEachSlot, multipleOf) + } + util.Logf("Computed Final timeForEachSlot = %v [%v <= %v <= %v]\n", timeForEachSlot, cfg.requested.slotMinDuration, timeForEachSlot, cfg.requested.slotMaxDuration) + return timeForEachSlot +} + +// Checks if multipleOf can be used as least time value +// this will ensure eack slot to maximize its time if possible +// if multipleOf can not be used as least value then default input value is returned as is +// accepts time containing, which least value to be computed. +// leastTimeRequiredByEachSlot - indicates the mimimum time that any slot can accept (UOE-5268) +// Returns the least value based on multiple of X +func computeTimeLeastValue(time int64, leastTimeRequiredByEachSlot int64) int64 { + // time if Testcase#6 + // 1. multiple of x - get smallest factor N of multiple of x for time + // 2. not multiple of x - try to obtain smallet no N multipe of x + // ensure N <= timeForEachSlot + leastFactor := multipleOf + if leastFactor < time { + time = leastFactor + } + + // case: check if slots are looking for time < leastFactor + // UOE-5268 + if leastTimeRequiredByEachSlot > 0 && leastTimeRequiredByEachSlot < time { + time = leastTimeRequiredByEachSlot + } + + return time +} + +// Validate the algorithm computations +// 1. Verifies if 2D slice containing Min duration and Max duration values are non-zero +// 2. Idenfies the Ad Slots / Impressions with either Min Duration or Max Duration or both +// having zero value and removes it from 2D slice +// 3. Ensures Minimum Pod duration <= TotalSlotTime <= Maximum Pod Duration +// if any validation fails it removes all the alloated slots and makes is of size 0 +// and sets the freeTime value as RequestedPodMaxDuration +func (config *generator) validateSlots() { + + // default return value if validation fails + emptySlots := make([][2]int64, 0) + if len(config.Slots) == 0 { + return + } + + returnEmptySlots := false + + // check slot with 0 values + // remove them from config.Slots + emptySlotCount := 0 + for index, slot := range config.Slots { + if slot[0] == 0 || slot[1] == 0 { + util.Logf("WARNING:Slot[%v][%v] is having 0 duration\n", index, slot) + emptySlotCount++ + continue + } + + // check slot boundaries + if slot[1] < config.requested.slotMinDuration || slot[1] > config.requested.slotMaxDuration { + util.Logf("ERROR: Slot%v Duration %v sec is out of either requested.slotMinDuration (%v) or requested.slotMaxDuration (%v)\n", index, slot[1], config.requested.slotMinDuration, config.requested.slotMaxDuration) + returnEmptySlots = true + break + } + } + + // remove empty slot + if emptySlotCount > 0 { + optimizedSlots := make([][2]int64, len(config.Slots)-emptySlotCount) + for index, slot := range config.Slots { + if slot[0] == 0 || slot[1] == 0 { + } else { + optimizedSlots[index][0] = slot[0] + optimizedSlots[index][1] = slot[1] + } + } + config.Slots = optimizedSlots + util.Logf("Removed %v empty slots\n", emptySlotCount) + } + + if int64(len(config.Slots)) < config.requested.minAds || int64(len(config.Slots)) > config.requested.maxAds { + util.Logf("ERROR: slotSize %v is either less than Min Ads (%v) or greater than Max Ads (%v)\n", len(config.Slots), config.requested.minAds, config.requested.maxAds) + returnEmptySlots = true + } + + // ensure if min pod duration = max pod duration + // config.TotalSlotTime = pod duration + if config.requested.podMinDuration == config.requested.podMaxDuration && *config.totalSlotTime != config.requested.podMaxDuration { + util.Logf("ERROR: Total Slot Duration %v sec is not matching with Total Pod Duration %v sec\n", *config.totalSlotTime, config.requested.podMaxDuration) + returnEmptySlots = true + } + + // ensure slot duration lies between requested min pod duration and requested max pod duration + // Testcase #15 + if *config.totalSlotTime < config.requested.podMinDuration || *config.totalSlotTime > config.requested.podMaxDuration { + util.Logf("ERROR: Total Slot Duration %v sec is either less than Requested Pod Min Duration (%v sec) or greater than Requested Pod Max Duration (%v sec)\n", *config.totalSlotTime, config.requested.podMinDuration, config.requested.podMaxDuration) + returnEmptySlots = true + } + + if returnEmptySlots { + config.Slots = emptySlots + config.freeTime = config.requested.podMaxDuration + } +} + +// Adds time to possible slots and returns total added time +// +// Checks following for each Ad Slot +// 1. Can Ad Slot adjust the input time +// 2. If addition of new time to any slot not exeeding Total Pod Max Duration +// Performs the following operations +// 1. Populates Minimum duration slot[][0] - Either Slot Minimum Duration or Actual Slot Time computed +// 2. Populates Maximum duration slot[][1] - Always actual Slot Time computed +// 3. Counts the number of Ad Slots / Impressons full with duration capacity. If all Ad Slots / Impressions +// are full of capacity it returns true as second return argument, indicating all slots are full with capacity +// 4. Keeps track of TotalSlotDuration when each new time is added to the Ad Slot +// 5. Keeps track of difference between computed PodMaxDuration and RequestedPodMaxDuration (TestCase #16) and used in step #2 above +// Returns argument 1 indicating total time adusted, argument 2 whether all slots are full of duration capacity +func (config generator) addTime(timeForEachSlot int64, fillZeroSlotsOnPriority bool) (int64, bool) { + time := int64(0) + + // iterate over each ad + slotCountFullWithCapacity := 0 + for ad := int64(0); ad < int64(len(config.Slots)); ad++ { + + slot := &config.Slots[ad] + // check + // 1. time(slot(0)) <= config.SlotMaxDuration + // 2. if adding new time to slot0 not exeeding config.SlotMaxDuration + // 3. if sum(slot time) + timeForEachSlot <= config.RequestedPodMaxDuration + canAdjustTime := (slot[1]+timeForEachSlot) <= config.requested.slotMaxDuration && (slot[1]+timeForEachSlot) >= config.requested.slotMinDuration + totalSlotTimeWithNewTimeLessThanRequestedPodMaxDuration := *config.totalSlotTime+timeForEachSlot <= config.requested.podMaxDuration + + // if fillZeroSlotsOnPriority= true ensure current slot value = 0 + allowCurrentSlot := !fillZeroSlotsOnPriority || (fillZeroSlotsOnPriority && slot[1] == 0) + if slot[1] <= config.internal.slotMaxDuration && canAdjustTime && totalSlotTimeWithNewTimeLessThanRequestedPodMaxDuration && allowCurrentSlot { + slot[0] += timeForEachSlot + + // if we are adjusting the free time which will match up with config.RequestedPodMaxDuration + // then set config.SlotMinDuration as min value for this slot + // TestCase #16 + //if timeForEachSlot == maxPodDurationMatchUpTime { + if timeForEachSlot < multipleOf { + // override existing value of slot[0] here + slot[0] = config.requested.slotMinDuration + } + + // check if this slot duration was zero + if slot[1] == 0 { + // decrememt config.slotsWithZeroTime as we added some time for this slot + *config.slotsWithZeroTime-- + } + + slot[1] += timeForEachSlot + *config.totalSlotTime += timeForEachSlot + time += timeForEachSlot + util.Logf("Slot %v = Added %v sec (New Time = %v)\n", ad, timeForEachSlot, slot[1]) + } + // check slot capabity + // !canAdjustTime - TestCase18 + // UOE-5268 - Check with Requested Slot Max Duration + if slot[1] == config.requested.slotMaxDuration || !canAdjustTime { + // slot is full + slotCountFullWithCapacity++ + } + } + util.Logf("adjustedTime = %v\n ", time) + return time, slotCountFullWithCapacity == len(config.Slots) +} + +//shouldAdjustSlotWithZeroDuration - returns if slot with zero durations should be filled +// Currently it will return true in following condition +// cfg.minAds = cfg.maxads (i.e. Exact number of ads are required) +func (config generator) shouldAdjustSlotWithZeroDuration() bool { + if config.requested.minAds == config.requested.maxAds { + return true + } + return false +} diff --git a/endpoints/openrtb2/ctv/impressions/impressions.go b/endpoints/openrtb2/ctv/impressions/impressions.go new file mode 100644 index 00000000000..a0040a34121 --- /dev/null +++ b/endpoints/openrtb2/ctv/impressions/impressions.go @@ -0,0 +1,116 @@ +// Package impressions provides various algorithms to get the number of impressions +// along with minimum and maximum duration of each impression. +// It uses Ad pod request for it +package impressions + +import ( + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/util" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// Algorithm indicates type of algorithms supported +// Currently it supports +// 1. MaximizeForDuration +// 2. MinMaxAlgorithm +type Algorithm int + +const ( + // MaximizeForDuration algorithm tends towards Ad Pod Maximum Duration, Ad Slot Maximum Duration + // and Maximum number of Ads. Accordingly it computes the number of impressions + MaximizeForDuration Algorithm = iota + // MinMaxAlgorithm algorithm ensures all possible impression breaks are plotted by considering + // minimum as well as maxmimum durations and ads received in the ad pod request. + // It computes number of impressions with following steps + // 1. Passes input configuration as it is (Equivalent of MaximizeForDuration algorithm) + // 2. Ad Pod Duration = Ad Pod Max Duration, Number of Ads = max ads + // 3. Ad Pod Duration = Ad Pod Max Duration, Number of Ads = min ads + // 4. Ad Pod Duration = Ad Pod Min Duration, Number of Ads = max ads + // 5. Ad Pod Duration = Ad Pod Min Duration, Number of Ads = min ads + MinMaxAlgorithm + // ByDurationRanges algorithm plots the impression objects based on expected video duration + // ranges reveived in the input prebid-request. Based on duration matching policy + // it will generate the impression objects. in case 'exact' duration matching impression + // min duration = max duration. In case 'round up' this algorithm will not be executed.Instead + ByDurationRanges +) + +// MonitorKey provides the unique key for moniroting the impressions algorithm +var MonitorKey = map[Algorithm]string{ + MaximizeForDuration: `a1_max`, + MinMaxAlgorithm: `a2_min_max`, + ByDurationRanges: `a3_duration`, +} + +// Value use to compute Ad Slot Durations and Pod Durations for internal computation +// Right now this value is set to 5, based on passed data observations +// Observed that typically video impression contains contains minimum and maximum duration in multiples of 5 +var multipleOf = int64(5) + +// IImpressions ... +type IImpressions interface { + Get() [][2]int64 + Algorithm() Algorithm // returns algorithm used for computing number of impressions +} + +// NewImpressions generate object of impression generator +// based on input algorithm type +// if invalid algorithm type is passed, it returns default algorithm which will compute +// impressions based on minimum ad slot duration +func NewImpressions(podMinDuration, podMaxDuration int64, reqAdPod *openrtb_ext.ExtRequestAdPod, vPod *openrtb_ext.VideoAdPod, algorithm Algorithm) IImpressions { + switch algorithm { + case MaximizeForDuration: + util.Logf("Selected ImpGen Algorithm - 'MaximizeForDuration'") + g := newMaximizeForDuration(podMinDuration, podMaxDuration, *vPod) + return &g + + case MinMaxAlgorithm: + util.Logf("Selected ImpGen Algorithm - 'MinMaxAlgorithm'") + g := newMinMaxAlgorithm(podMinDuration, podMaxDuration, *vPod) + return &g + + case ByDurationRanges: + util.Logf("Selected ImpGen Algorithm - 'ByDurationRanges'") + + g := newByDurationRanges(reqAdPod.VideoLengthMatching, reqAdPod.VideoLengths, + int(*vPod.MaxAds), + *vPod.MinDuration, *vPod.MaxDuration) + + return &g + } + + // return default algorithm with slot durations set to minimum slot duration + util.Logf("Selected 'DefaultAlgorithm'") + defaultGenerator := newConfig(podMinDuration, podMinDuration, openrtb_ext.VideoAdPod{ + MinAds: vPod.MinAds, + MaxAds: vPod.MaxAds, + MinDuration: vPod.MinDuration, + MaxDuration: vPod.MinDuration, // sending slot minduration as max duration + }) + return &defaultGenerator +} + +// SelectAlgorithm is factory function which will return valid Algorithm based on adpod parameters +// Return Value: +// - MinMaxAlgorithm (default) +// - ByDurationRanges: if reqAdPod extension has VideoLengths and VideoLengthMatchingPolicy is "exact" algorithm +func SelectAlgorithm(reqAdPod *openrtb_ext.ExtRequestAdPod) Algorithm { + if nil != reqAdPod { + if len(reqAdPod.VideoLengths) > 0 && + (openrtb_ext.OWExactVideoLengthsMatching == reqAdPod.VideoLengthMatching || openrtb_ext.OWRoundupVideoLengthMatching == reqAdPod.VideoLengthMatching) { + return ByDurationRanges + } + } + return MinMaxAlgorithm +} + +// Duration indicates the position +// where the required min or max duration value can be found +// within given impression object +type Duration int + +const ( + // MinDuration represents index value where we can get minimum duration of given impression object + MinDuration Duration = iota + // MaxDuration represents index value where we can get maximum duration of given impression object + MaxDuration +) diff --git a/endpoints/openrtb2/ctv/impressions/impressions_test.go b/endpoints/openrtb2/ctv/impressions/impressions_test.go new file mode 100644 index 00000000000..d2d70a0c7e5 --- /dev/null +++ b/endpoints/openrtb2/ctv/impressions/impressions_test.go @@ -0,0 +1,147 @@ +// Package impressions provides various algorithms to get the number of impressions +// along with minimum and maximum duration of each impression. +// It uses Ad pod request for it +package impressions + +import ( + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestSelectAlgorithm(t *testing.T) { + type args struct { + reqAdPod *openrtb_ext.ExtRequestAdPod + } + tests := []struct { + name string + args args + want Algorithm + }{ + { + name: "default", + args: args{}, + want: MinMaxAlgorithm, + }, + { + name: "missing_videolengths", + args: args{reqAdPod: &openrtb_ext.ExtRequestAdPod{}}, + want: MinMaxAlgorithm, + }, + { + name: "roundup_matching_algo", + args: args{reqAdPod: &openrtb_ext.ExtRequestAdPod{ + VideoLengths: []int{15, 20}, + VideoLengthMatching: openrtb_ext.OWRoundupVideoLengthMatching, + }}, + want: ByDurationRanges, + }, + { + name: "exact_matching_algo", + args: args{reqAdPod: &openrtb_ext.ExtRequestAdPod{ + VideoLengths: []int{15, 20}, + VideoLengthMatching: openrtb_ext.OWExactVideoLengthsMatching, + }}, + want: ByDurationRanges, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := SelectAlgorithm(tt.args.reqAdPod) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestNewImpressions(t *testing.T) { + intPtr := func(v int) *int { return &v } + + type args struct { + podMinDuration int64 + podMaxDuration int64 + reqAdPod *openrtb_ext.ExtRequestAdPod + vPod *openrtb_ext.VideoAdPod + algorithm Algorithm + } + tests := []struct { + name string + args args + want Algorithm + }{ + { + name: "Default-MaximizeForDuration", + args: args{ + podMinDuration: 15, + podMaxDuration: 90, + reqAdPod: &openrtb_ext.ExtRequestAdPod{}, + vPod: &openrtb_ext.VideoAdPod{ + MinAds: intPtr(1), + MaxAds: intPtr(2), + MinDuration: intPtr(5), + MaxDuration: intPtr(10), + }, + algorithm: Algorithm(-1), + }, + want: MaximizeForDuration, + }, + { + name: "MaximizeForDuration", + args: args{ + podMinDuration: 15, + podMaxDuration: 90, + reqAdPod: &openrtb_ext.ExtRequestAdPod{}, + vPod: &openrtb_ext.VideoAdPod{ + MinAds: intPtr(1), + MaxAds: intPtr(2), + MinDuration: intPtr(5), + MaxDuration: intPtr(10), + }, + algorithm: MaximizeForDuration, + }, + want: MaximizeForDuration, + }, + { + name: "MinMaxAlgorithm", + args: args{ + podMinDuration: 15, + podMaxDuration: 90, + reqAdPod: &openrtb_ext.ExtRequestAdPod{}, + vPod: &openrtb_ext.VideoAdPod{ + MinAds: intPtr(1), + MaxAds: intPtr(2), + MinDuration: intPtr(5), + MaxDuration: intPtr(10), + }, + algorithm: MinMaxAlgorithm, + }, + want: MinMaxAlgorithm, + }, + { + name: "ByDurationRanges", + args: args{ + podMinDuration: 15, + podMaxDuration: 90, + reqAdPod: &openrtb_ext.ExtRequestAdPod{ + VideoLengths: []int{10, 15}, + }, + vPod: &openrtb_ext.VideoAdPod{ + MinAds: intPtr(1), + MaxAds: intPtr(2), + MinDuration: intPtr(5), + MaxDuration: intPtr(10), + }, + algorithm: ByDurationRanges, + }, + want: ByDurationRanges, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewImpressions(tt.args.podMinDuration, tt.args.podMaxDuration, tt.args.reqAdPod, tt.args.vPod, tt.args.algorithm) + assert.Equal(t, tt.want, got.Algorithm()) + }) + } +} diff --git a/endpoints/openrtb2/ctv/impressions/maximize_for_duration.go b/endpoints/openrtb2/ctv/impressions/maximize_for_duration.go new file mode 100644 index 00000000000..b51116744b6 --- /dev/null +++ b/endpoints/openrtb2/ctv/impressions/maximize_for_duration.go @@ -0,0 +1,25 @@ +package impressions + +import ( + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/util" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// newMaximizeForDuration Constucts the generator object from openrtb_ext.VideoAdPod +// It computes durations for Ad Slot and Ad Pod in multiple of X +func newMaximizeForDuration(podMinDuration, podMaxDuration int64, vPod openrtb_ext.VideoAdPod) generator { + config := newConfigWithMultipleOf(podMinDuration, podMaxDuration, vPod, multipleOf) + + util.Logf("Computed podMinDuration = %v in multiples of %v (requestedPodMinDuration = %v)\n", config.requested.podMinDuration, multipleOf, config.requested.podMinDuration) + util.Logf("Computed podMaxDuration = %v in multiples of %v (requestedPodMaxDuration = %v)\n", config.requested.podMaxDuration, multipleOf, config.requested.podMaxDuration) + util.Logf("Computed slotMinDuration = %v in multiples of %v (requestedSlotMinDuration = %v)\n", config.internal.slotMinDuration, multipleOf, config.requested.slotMinDuration) + util.Logf("Computed slotMaxDuration = %v in multiples of %v (requestedSlotMaxDuration = %v)\n", config.internal.slotMaxDuration, multipleOf, *vPod.MaxDuration) + util.Logf("Requested minAds = %v\n", config.requested.minAds) + util.Logf("Requested maxAds = %v\n", config.requested.maxAds) + return config +} + +// Algorithm returns MaximizeForDuration +func (config generator) Algorithm() Algorithm { + return MaximizeForDuration +} diff --git a/endpoints/openrtb2/ctv/impressions/maximize_for_duration_test.go b/endpoints/openrtb2/ctv/impressions/maximize_for_duration_test.go new file mode 100644 index 00000000000..c252573cf68 --- /dev/null +++ b/endpoints/openrtb2/ctv/impressions/maximize_for_duration_test.go @@ -0,0 +1,465 @@ +package impressions + +import ( + "testing" + + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/impressions/testdata" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +type TestAdPod struct { + vPod openrtb_ext.VideoAdPod + podMinDuration int64 + podMaxDuration int64 +} + +type expected struct { + impressionCount int + // Time remaining after ad breaking is done + // if no ad breaking i.e. 0 then freeTime = pod.maxduration + freeTime int64 + adSlotTimeInSec []int64 + + // close bounds + closedMinDuration int64 // pod + closedMaxDuration int64 // pod + closedSlotMinDuration int64 // ad slot + closedSlotMaxDuration int64 // ad slot +} + +var impressionsTests = []struct { + scenario string // Testcase scenario + out expected // Testcase execpted output +}{ + {scenario: "TC2", out: expected{ + impressionCount: 6, + freeTime: 0.0, + closedMinDuration: 5, + closedMaxDuration: 90, + closedSlotMinDuration: 15, + closedSlotMaxDuration: 15, + }}, + {scenario: "TC3", out: expected{ + impressionCount: 4, + freeTime: 30.0, closedMinDuration: 5, + closedMaxDuration: 90, + closedSlotMinDuration: 15, + closedSlotMaxDuration: 15, + }}, + {scenario: "TC4", out: expected{ + impressionCount: 1, + freeTime: 0.0, closedMinDuration: 5, + closedMaxDuration: 15, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 15, + }}, + {scenario: "TC5", out: expected{ + impressionCount: 2, + freeTime: 0.0, closedMinDuration: 5, + closedMaxDuration: 15, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 15, + }}, + {scenario: "TC6", out: expected{ + impressionCount: 8, + freeTime: 0.0, closedMinDuration: 5, + closedMaxDuration: 90, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 15, + }}, + {scenario: "TC7", out: expected{ + impressionCount: 1, + freeTime: 15.0, closedMinDuration: 15, + closedMaxDuration: 30, + closedSlotMinDuration: 10, + closedSlotMaxDuration: 15, + }}, + {scenario: "TC8", out: expected{ + impressionCount: 3, + freeTime: 0.0, closedMinDuration: 35, + closedMaxDuration: 35, + closedSlotMinDuration: 10, + closedSlotMaxDuration: 35, + }}, + {scenario: "TC9", out: expected{ + impressionCount: 0, + freeTime: 35, closedMinDuration: 35, + closedMaxDuration: 35, + closedSlotMinDuration: 10, + closedSlotMaxDuration: 35, + }}, + {scenario: "TC10", out: expected{ + impressionCount: 6, + freeTime: 0.0, closedMinDuration: 35, + closedMaxDuration: 65, + closedSlotMinDuration: 10, + closedSlotMaxDuration: 35, + }}, + {scenario: "TC11", out: expected{ + impressionCount: 0, //7, + freeTime: 0, closedMinDuration: 35, + closedMaxDuration: 65, + closedSlotMinDuration: 10, + closedSlotMaxDuration: 35, + }}, + {scenario: "TC12", out: expected{ + impressionCount: 10, + freeTime: 0.0, closedMinDuration: 100, + closedMaxDuration: 100, + closedSlotMinDuration: 10, + closedSlotMaxDuration: 35, + }}, + {scenario: "TC13", out: expected{ + impressionCount: 0, + freeTime: 60, closedMinDuration: 60, + closedMaxDuration: 60, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 5, + }}, + {scenario: "TC14", out: expected{ + impressionCount: 6, + freeTime: 6, closedMinDuration: 30, + closedMaxDuration: 60, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 5, + }}, + {scenario: "TC15", out: expected{ + impressionCount: 5, + freeTime: 15, closedMinDuration: 30, + closedMaxDuration: 60, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 5, + }}, + {scenario: "TC16", out: expected{ + impressionCount: 13, + freeTime: 0, closedMinDuration: 126, + closedMaxDuration: 126, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 10, + }}, + {scenario: "TC17", out: expected{ + impressionCount: 13, + freeTime: 0, closedMinDuration: 130, + closedMaxDuration: 125, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 10, + }}, + {scenario: "TC18", out: expected{ + impressionCount: 0, + freeTime: 125, closedMinDuration: 125, + closedMaxDuration: 125, + closedSlotMinDuration: 4, + closedSlotMaxDuration: 4, + }}, + {scenario: "TC19", out: expected{ + impressionCount: 0, + freeTime: 90, closedMinDuration: 90, + closedMaxDuration: 90, + closedSlotMinDuration: 7, // overlapping case. Hence as is + closedSlotMaxDuration: 9, + }}, + {scenario: "TC20", out: expected{ + impressionCount: 9, + freeTime: 0, closedMinDuration: 90, + closedMaxDuration: 90, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 10, + }}, + {scenario: "TC21", out: expected{ + impressionCount: 9, + freeTime: 89, closedMinDuration: 5, + closedMaxDuration: 170, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 5, + }}, + {scenario: "TC23", out: expected{ + impressionCount: 12, + freeTime: 0, closedMinDuration: 120, + closedMaxDuration: 120, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 15, + }}, + {scenario: "TC24", out: expected{ + impressionCount: 2, + freeTime: 0, closedMinDuration: 134, + closedMaxDuration: 134, + closedSlotMinDuration: 60, + closedSlotMaxDuration: 90, + }}, + {scenario: "TC25", out: expected{ + impressionCount: 2, + freeTime: 0, + + closedMinDuration: 88, + closedMaxDuration: 88, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 80, + }}, + {scenario: "TC26", out: expected{ + impressionCount: 2, + freeTime: 0, + + closedMinDuration: 90, + closedMaxDuration: 90, + closedSlotMinDuration: 45, + closedSlotMaxDuration: 45, + }}, + {scenario: "TC27", out: expected{ + impressionCount: 3, + freeTime: 0, + + closedMinDuration: 5, + closedMaxDuration: 90, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 45, + }}, + {scenario: "TC28", out: expected{ + impressionCount: 6, + freeTime: 0, + + closedMinDuration: 5, + closedMaxDuration: 180, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 90, + }}, + {scenario: "TC29", out: expected{ + impressionCount: 3, + freeTime: 0, closedMinDuration: 5, + closedMaxDuration: 65, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 35, + }}, + {scenario: "TC30", out: expected{ + impressionCount: 3, + freeTime: 123, closedMinDuration: 123, + closedMaxDuration: 123, + closedSlotMinDuration: 34, + closedSlotMaxDuration: 34, + }}, + {scenario: "TC31", out: expected{ + impressionCount: 3, + freeTime: 123, closedMinDuration: 123, + closedMaxDuration: 123, + closedSlotMinDuration: 31, + closedSlotMaxDuration: 31, + }}, {scenario: "TC32", out: expected{ + impressionCount: 0, + freeTime: 134, closedMinDuration: 134, + closedMaxDuration: 134, + closedSlotMinDuration: 63, + closedSlotMaxDuration: 63, + }}, + {scenario: "TC33", out: expected{ + impressionCount: 4, + freeTime: 0, closedMinDuration: 147, + closedMaxDuration: 147, + closedSlotMinDuration: 30, + closedSlotMaxDuration: 60, + }}, + {scenario: "TC34", out: expected{ + impressionCount: 3, + freeTime: 12, closedMinDuration: 90, + closedMaxDuration: 100, + closedSlotMinDuration: 30, + closedSlotMaxDuration: 30, + }}, {scenario: "TC35", out: expected{ + impressionCount: 0, + freeTime: 102, closedMinDuration: 90, + closedMaxDuration: 100, + closedSlotMinDuration: 30, + closedSlotMaxDuration: 40, + }}, {scenario: "TC36", out: expected{ + impressionCount: 2, + freeTime: 0, closedMinDuration: 90, + closedMaxDuration: 90, + closedSlotMinDuration: 45, + closedSlotMaxDuration: 45, + }}, {scenario: "TC37", out: expected{ + impressionCount: 2, + freeTime: 0, closedMinDuration: 10, + closedMaxDuration: 45, + closedSlotMinDuration: 20, + closedSlotMaxDuration: 45, + }}, {scenario: "TC38", out: expected{ + impressionCount: 0, + freeTime: 0, closedMinDuration: 90, + closedMaxDuration: 90, + closedSlotMinDuration: 20, + closedSlotMaxDuration: 45, + }}, {scenario: "TC39", out: expected{ + impressionCount: 4, + freeTime: 0, closedMinDuration: 60, + closedMaxDuration: 90, + closedSlotMinDuration: 20, + closedSlotMaxDuration: 45, + }}, {scenario: "TC40", out: expected{ + impressionCount: 10, + freeTime: 0, closedMinDuration: 95, + closedMaxDuration: 95, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 45, + }}, {scenario: "TC41", out: expected{ + impressionCount: 0, + freeTime: 123, closedMinDuration: 95, + closedMaxDuration: 120, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 45, + }}, {scenario: "TC42", out: expected{ + impressionCount: 1, + freeTime: 0, closedMinDuration: 1, + closedMaxDuration: 1, + closedSlotMinDuration: 1, + closedSlotMaxDuration: 1, + }}, {scenario: "TC43", out: expected{ + impressionCount: 0, + freeTime: 2, closedMinDuration: 2, + closedMaxDuration: 2, + closedSlotMinDuration: 2, + closedSlotMaxDuration: 2, + }}, {scenario: "TC44", out: expected{ + impressionCount: 0, + freeTime: 0, closedMinDuration: 0, + closedMaxDuration: 0, + closedSlotMinDuration: 0, + closedSlotMaxDuration: 0, + }}, {scenario: "TC45", out: expected{ + impressionCount: 0, + freeTime: 0, closedMinDuration: 5, + closedMaxDuration: -5, + closedSlotMinDuration: -3, // overlapping hence will as is + closedSlotMaxDuration: -4, + }}, {scenario: "TC46", out: expected{ + impressionCount: 0, + freeTime: 0, closedMinDuration: -1, + closedMaxDuration: -1, + closedSlotMinDuration: -1, + closedSlotMaxDuration: -1, + }}, {scenario: "TC47", out: expected{ + impressionCount: 1, + freeTime: 0, closedMinDuration: 6, + closedMaxDuration: 6, + closedSlotMinDuration: 6, + closedSlotMaxDuration: 6, + }}, {scenario: "TC48", out: expected{ + impressionCount: 2, + freeTime: 0, closedMinDuration: 12, + closedMaxDuration: 12, + closedSlotMinDuration: 6, + closedSlotMaxDuration: 6, + }}, {scenario: "TC49", out: expected{ + impressionCount: 0, + freeTime: 12, closedMinDuration: 12, + closedMaxDuration: 12, + closedSlotMinDuration: 7, + closedSlotMaxDuration: 7, + }}, {scenario: "TC50", out: expected{ + impressionCount: 0, + freeTime: 0, closedMinDuration: 1, + closedMaxDuration: 1, + closedSlotMinDuration: 1, + closedSlotMaxDuration: 1, + }}, {scenario: "TC51", out: expected{ + impressionCount: 3, + freeTime: 4, closedMinDuration: 35, + closedMaxDuration: 40, + closedSlotMinDuration: 11, + closedSlotMaxDuration: 13, + }}, + {scenario: "TC52", out: expected{ + impressionCount: 3, + freeTime: 0, closedMinDuration: 70, + closedMaxDuration: 70, + closedSlotMinDuration: 15, + closedSlotMaxDuration: 15, + }}, {scenario: "TC53", out: expected{ + impressionCount: 3, + freeTime: 0, closedMinDuration: 126, + closedMaxDuration: 126, + closedSlotMinDuration: 5, + closedSlotMaxDuration: 20, + }}, {scenario: "TC55", out: expected{ + impressionCount: 6, + freeTime: 2, closedMinDuration: 1, + closedMaxDuration: 74, + closedSlotMinDuration: 12, + closedSlotMaxDuration: 12, + }}, {scenario: "TC56", out: expected{ + impressionCount: 1, + freeTime: 0, closedMinDuration: 126, + closedMaxDuration: 126, + closedSlotMinDuration: 126, + closedSlotMaxDuration: 126, + }}, {scenario: "TC57", out: expected{ + impressionCount: 1, + freeTime: 0, closedMinDuration: 126, + closedMaxDuration: 126, + closedSlotMinDuration: 126, + closedSlotMaxDuration: 126, + }}, {scenario: "TC58", out: expected{ + impressionCount: 4, + freeTime: 0, closedMinDuration: 30, + closedMaxDuration: 90, + closedSlotMinDuration: 15, + closedSlotMaxDuration: 45, + }}, + {scenario: "TC59", out: expected{ + impressionCount: 1, + freeTime: 45, closedMinDuration: 30, + closedMaxDuration: 90, + closedSlotMinDuration: 15, + closedSlotMaxDuration: 45, + }}, +} + +func TestGetImpressionsA1(t *testing.T) { + for _, impTest := range impressionsTests { + t.Run(impTest.scenario, func(t *testing.T) { + in := testdata.Input[impTest.scenario] + p := newTestPod(int64(in[0]), int64(in[1]), in[2], in[3], in[4], in[5]) + + cfg := newMaximizeForDuration(p.podMinDuration, p.podMaxDuration, p.vPod) + imps := cfg.Get() + expected := impTest.out + expectedImpressionBreak := testdata.Scenario[impTest.scenario].MaximizeForDuration + // assert.Equal(t, expected.impressionCount, len(pod.Slots), "expected impression count = %v . But Found %v", expectedImpressionCount, len(pod.Slots)) + assert.Equal(t, expected.freeTime, cfg.freeTime, "expected Free Time = %v . But Found %v", expected.freeTime, cfg.freeTime) + // assert.Equal(t, expected.closedMinDuration, cfg.requested.podMinDuration, "expected closedMinDuration= %v . But Found %v", expected.closedMinDuration, cfg.requested.podMinDuration) + // assert.Equal(t, expected.closedMaxDuration, cfg.requested.podMaxDuration, "expected closedMinDuration= %v . But Found %v", expected.closedMaxDuration, cfg.requested.podMaxDuration) + assert.Equal(t, expected.closedSlotMinDuration, cfg.internal.slotMinDuration, "expected closedSlotMinDuration= %v . But Found %v", expected.closedSlotMinDuration, cfg.internal.slotMinDuration) + assert.Equal(t, expected.closedSlotMaxDuration, cfg.internal.slotMaxDuration, "expected closedSlotMinDuration= %v . But Found %v", expected.closedSlotMaxDuration, cfg.internal.slotMaxDuration) + assert.Equal(t, expectedImpressionBreak, imps, "2darray mismatch") + assert.Equal(t, MaximizeForDuration, cfg.Algorithm()) + }) + } +} + +/* Benchmarking Tests */ +func BenchmarkGetImpressions(b *testing.B) { + for _, impTest := range impressionsTests { + b.Run(impTest.scenario, func(b *testing.B) { + in := testdata.Input[impTest.scenario] + p := newTestPod(int64(in[0]), int64(in[1]), in[2], in[3], in[4], in[5]) + for n := 0; n < b.N; n++ { + cfg := newMaximizeForDuration(p.podMinDuration, p.podMaxDuration, p.vPod) + cfg.Get() + } + }) + } +} + +func newTestPod(podMinDuration, podMaxDuration int64, slotMinDuration, slotMaxDuration, minAds, maxAds int) *TestAdPod { + testPod := TestAdPod{} + + pod := openrtb_ext.VideoAdPod{} + + pod.MinDuration = &slotMinDuration + pod.MaxDuration = &slotMaxDuration + pod.MinAds = &minAds + pod.MaxAds = &maxAds + + testPod.vPod = pod + testPod.podMinDuration = podMinDuration + testPod.podMaxDuration = podMaxDuration + return &testPod +} diff --git a/endpoints/openrtb2/ctv/impressions/min_max_algorithm.go b/endpoints/openrtb2/ctv/impressions/min_max_algorithm.go new file mode 100644 index 00000000000..0d2f43301f2 --- /dev/null +++ b/endpoints/openrtb2/ctv/impressions/min_max_algorithm.go @@ -0,0 +1,190 @@ +package impressions + +import ( + "fmt" + "math" + "strconv" + "strings" + "sync" + + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/util" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// keyDelim used as separator in forming key of maxExpectedDurationMap +var keyDelim = "," + +type config struct { + IImpressions + generator []generator + // maxExpectedDurationMap contains key = min , max duration, value = 0 -no of impressions, 1 + // this map avoids the unwanted repeatations of impressions generated + // Example, + // Step 1 : {{2, 17}, {15, 15}, {15, 15}, {10, 10}, {10, 10}, {10, 10}} + // Step 2 : {{2, 17}, {15, 15}, {15, 15}, {10, 10}, {10, 10}, {10, 10}} + // Step 3 : {{25, 25}, {25, 25}, {2, 22}, {5, 5}} + // Step 4 : {{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}} + // Step 5 : {{15, 15}, {15, 15}, {15, 15}, {15, 15}} + // Optimized Output : {{2, 17}, {15, 15},{15, 15},{15, 15},{15, 15},{10, 10},{10, 10},{10, 10},{10, 10},{10, 10},{10, 10},{25, 25}, {25, 25},{2, 22}, {5, 5}} + // This map will contains : {2, 17} = 1, {15, 15} = 4, {10, 10} = 6, {25, 25} = 2, {2, 22} = 1, {5, 5} =1 + maxExpectedDurationMap map[string][2]int + requested pod +} + +// newMinMaxAlgorithm constructs instance of MinMaxAlgorithm +// It computes durations for Ad Slot and Ad Pod in multiple of X +// it also considers minimum configurations present in the request +func newMinMaxAlgorithm(podMinDuration, podMaxDuration int64, p openrtb_ext.VideoAdPod) config { + generator := make([]generator, 0) + // step 1 - same as Algorithm1 + generator = append(generator, initGenerator(podMinDuration, podMaxDuration, p, *p.MinAds, *p.MaxAds)) + // step 2 - pod duration = pod max, no of ads = max ads + generator = append(generator, initGenerator(podMaxDuration, podMaxDuration, p, *p.MaxAds, *p.MaxAds)) + // step 3 - pod duration = pod max, no of ads = min ads + generator = append(generator, initGenerator(podMaxDuration, podMaxDuration, p, *p.MinAds, *p.MinAds)) + // step 4 - pod duration = pod min, no of ads = max ads + generator = append(generator, initGenerator(podMinDuration, podMinDuration, p, *p.MaxAds, *p.MaxAds)) + // step 5 - pod duration = pod min, no of ads = min ads + generator = append(generator, initGenerator(podMinDuration, podMinDuration, p, *p.MinAds, *p.MinAds)) + + return config{generator: generator, requested: generator[0].requested} +} + +func initGenerator(podMinDuration, podMaxDuration int64, p openrtb_ext.VideoAdPod, minAds, maxAds int) generator { + config := newConfigWithMultipleOf(podMinDuration, podMaxDuration, newVideoAdPod(p, minAds, maxAds), multipleOf) + return config +} + +func newVideoAdPod(p openrtb_ext.VideoAdPod, minAds, maxAds int) openrtb_ext.VideoAdPod { + return openrtb_ext.VideoAdPod{MinDuration: p.MinDuration, + MaxDuration: p.MaxDuration, + MinAds: &minAds, + MaxAds: &maxAds} +} + +// Get ... +func (c *config) Get() [][2]int64 { + imps := make([][2]int64, 0) + wg := new(sync.WaitGroup) // ensures each step generating impressions is finished + impsChan := make(chan [][2]int64, len(c.generator)) + for i := 0; i < len(c.generator); i++ { + wg.Add(1) + go get(c.generator[i], impsChan, wg) + } + + // ensure impressions channel is closed + // when all go routines are executed + func() { + defer close(impsChan) + wg.Wait() + }() + + c.maxExpectedDurationMap = make(map[string][2]int, 0) + util.Logf("Step wise breakup ") + for impressions := range impsChan { + for index, impression := range impressions { + impKey := getKey(impression) + setMaximumRepeatations(c, impKey, index+1 == len(impressions)) + } + util.Logf("%v", impressions) + } + + // for impressions array + indexOffset := 0 + for impKey := range c.maxExpectedDurationMap { + totalRepeations := c.getRepeations(impKey) + for repeation := 1; repeation <= totalRepeations; repeation++ { + imps = append(imps, getImpression(impKey)) + } + // if exact pod duration is provided then do not compute + // min duration. Instead expect min duration same as max duration + // It must be set by underneath algorithm + if c.requested.podMinDuration != c.requested.podMaxDuration { + computeMinDuration(*c, imps[:], indexOffset, indexOffset+totalRepeations) + } + indexOffset += totalRepeations + } + return imps +} + +// getImpression constructs the impression object with min and max duration +// from input impression key +func getImpression(key string) [2]int64 { + decodedKey := strings.Split(key, keyDelim) + minDuration, _ := strconv.Atoi(decodedKey[MinDuration]) + maxDuration, _ := strconv.Atoi(decodedKey[MaxDuration]) + return [2]int64{int64(minDuration), int64(maxDuration)} +} + +// setMaximumRepeatations avoids unwanted repeatations of impression object. Using following logic +// maxExpectedDurationMap value contains 2 types of storage +// 1. value[0] - represents current counter where final repeataions are stored +// 2. value[1] - local storage used by each impression object to add more repeatations if required +// impKey - key used to obtained already added repeatations for given impression +// updateCurrentCounter - if true and if current local storage value > repeatations then repeations will be +// updated as current counter +func setMaximumRepeatations(c *config, impKey string, updateCurrentCounter bool) { + // update maxCounter of each impression + value := c.maxExpectedDurationMap[impKey] + value[1]++ // increment max counter (contains no of repeatations for given iteration) + c.maxExpectedDurationMap[impKey] = value + // if val(maxCounter) > actual store then consider temporary value as actual value + if updateCurrentCounter { + for k := range c.maxExpectedDurationMap { + val := c.maxExpectedDurationMap[k] + if val[1] > val[0] { + val[0] = val[1] + } + // clear maxCounter + val[1] = 0 + c.maxExpectedDurationMap[k] = val // reassign + } + } + +} + +// getKey returns the key used for refering values of maxExpectedDurationMap +// key is computed based on input impression object having min and max durations +func getKey(impression [2]int64) string { + return fmt.Sprintf("%v%v%v", impression[MinDuration], keyDelim, impression[MaxDuration]) +} + +// getRepeations returns number of repeatations at that time that this algorithm will +// return w.r.t. input impressionKey +func (c config) getRepeations(impressionKey string) int { + return c.maxExpectedDurationMap[impressionKey][0] +} + +// get is internal function that actually computes the number of impressions +// based on configrations present in c +func get(c generator, ch chan [][2]int64, wg *sync.WaitGroup) { + defer wg.Done() + imps := c.Get() + util.Logf("A2 Impressions = %v\n", imps) + ch <- imps +} + +// Algorithm returns MinMaxAlgorithm +func (c config) Algorithm() Algorithm { + return MinMaxAlgorithm +} + +func computeMinDuration(c config, impressions [][2]int64, start int, end int) { + r := c.requested + // 5/2 => q = 2 , r = 1 => 2.5 => 3 + minDuration := int64(math.Round(float64(r.podMinDuration) / float64(r.minAds))) + for i := start; i < end; i++ { + impression := &impressions[i] + // ensure imp duration boundaries + // if boundaries are not honoured keep min duration which is computed as is + if minDuration >= r.slotMinDuration && minDuration <= impression[MaxDuration] { + // override previous value + impression[MinDuration] = minDuration + } else { + // boundaries are not matching keep min value as is + util.Logf("False : minDuration (%v) >= r.slotMinDuration (%v) && minDuration (%v) <= impression[MaxDuration] (%v)", minDuration, r.slotMinDuration, minDuration, impression[MaxDuration]) + util.Logf("Hence, setting request level slot minduration (%v) ", r.slotMinDuration) + impression[MinDuration] = r.slotMinDuration + } + } +} diff --git a/endpoints/openrtb2/ctv/impressions/min_max_algorithm_test.go b/endpoints/openrtb2/ctv/impressions/min_max_algorithm_test.go new file mode 100644 index 00000000000..a1af101626f --- /dev/null +++ b/endpoints/openrtb2/ctv/impressions/min_max_algorithm_test.go @@ -0,0 +1,601 @@ +package impressions + +import ( + "sort" + "testing" + + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/impressions/testdata" + "github.com/stretchr/testify/assert" +) + +type expectedOutputA2 struct { + step1 [][2]int64 // input passed as is + step2 [][2]int64 // pod duration = pod max duration, no of ads = maxads + step3 [][2]int64 // pod duration = pod max duration, no of ads = minads + step4 [][2]int64 // pod duration = pod min duration, no of ads = maxads + step5 [][2]int64 // pod duration = pod min duration, no of ads = minads +} + +var impressionsTestsA2 = []struct { + scenario string // Testcase scenario + //in []int // Testcase input + out expectedOutputA2 // Testcase execpted output +}{ + {scenario: "TC2", out: expectedOutputA2{ + step1: [][2]int64{{15, 15}, {15, 15}, {15, 15}, {15, 15}, {15, 15}, {15, 15}}, + step2: [][2]int64{{11, 13}, {11, 11}, {11, 11}, {11, 11}, {11, 11}, {11, 11}, {11, 11}, {11, 11}}, + step3: [][2]int64{}, // 90 90 15 15 2 2 + step4: [][2]int64{}, // 1,1, 15,15, 8 8 + step5: [][2]int64{}, // 1,1, 15,15, 2 2 + }}, + {scenario: "TC3", out: expectedOutputA2{ + step1: [][2]int64{{15, 15}, {15, 15}, {15, 15}, {15, 15}}, + step2: [][2]int64{}, // 90 90 15 15 4 4 + step3: [][2]int64{}, // 90 90 15 15 2 2 + step4: [][2]int64{}, // 1 1 15 15 4 4 + step5: [][2]int64{}, // 1 1 15 15 2 2 + }}, + {scenario: "TC4", out: expectedOutputA2{ + step1: [][2]int64{{15, 15}}, + step2: [][2]int64{{15, 15}}, // 15 15 5 15 1 1 + step3: [][2]int64{{15, 15}}, // 15 15 5 15 1 1 + step4: [][2]int64{{1, 1}}, // 1 1 5 15 1 1 + step5: [][2]int64{{1, 1}}, // 1 1 5 15 1 1 + }}, + {scenario: "TC5", out: expectedOutputA2{ + step1: [][2]int64{{10, 10}, {5, 5}}, + step2: [][2]int64{{10, 10}, {5, 5}}, // 15, 15, 5, 15, 2, 2 + step3: [][2]int64{{15, 15}}, // 15, 15, 5, 15, 1, 1 + step4: [][2]int64{}, // 1, 1, 5, 15, 2, 2 + step5: [][2]int64{{1, 1}}, // 1, 1, 5, 15, 1, 1 + }}, + {scenario: "TC6", out: expectedOutputA2{ + // 5, 90, 5, 15, 1, 8 + step1: [][2]int64{{15, 15}, {15, 15}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + // 90, 90, 5, 15, 8, 8 + step2: [][2]int64{{15, 15}, {15, 15}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + // 90, 90, 5, 15, 1, 1 + step3: [][2]int64{}, + // 1, 1, 5, 15, 8, 8 + step4: [][2]int64{}, + // 1, 1, 5, 15, 1, 1 + step5: [][2]int64{{1, 1}}, + }}, + {scenario: "TC7", out: expectedOutputA2{ + // 15, 30, 10, 15, 1, 1 + step1: [][2]int64{{15, 15}}, + // 30, 30, 10, 15, 1, 1 + step2: [][2]int64{}, + // 30, 30, 10, 15, 1, 1 + step3: [][2]int64{}, + // 15, 15, 10, 15, 1, 1 + step4: [][2]int64{{15, 15}}, + // 15, 15, 10, 15, 1, 1 + step5: [][2]int64{{15, 15}}, + }}, + {scenario: "TC8", out: expectedOutputA2{ + // 35, 35, 10, 35, 3, 40 + step1: [][2]int64{{15, 15}, {10, 10}, {10, 10}}, + // 35, 35, 10, 35, 40, 40 + step2: [][2]int64{}, + // 35, 35, 10, 35, 3, 3 + step3: [][2]int64{{15, 15}, {10, 10}, {10, 10}}, + // 35, 35, 10, 35, 40, 40 + step4: [][2]int64{}, + // 35, 35, 10, 35, 3, 3 + step5: [][2]int64{{15, 15}, {10, 10}, {10, 10}}, + }}, + {scenario: "TC9", out: expectedOutputA2{ + // 35, 35, 10, 35, 6, 40 + step1: [][2]int64{}, + // 35, 35, 10, 35, 40, 40 + step2: [][2]int64{}, + // 35, 35, 10, 35, 6, 6 + step3: [][2]int64{}, + // 35, 35, 10, 35, 40, 40 + step4: [][2]int64{}, + // 35, 35, 10, 35, 6, 6 + step5: [][2]int64{}, + }}, + {scenario: "TC10", out: expectedOutputA2{ + // 35, 65, 10, 35, 6, 40 + step1: [][2]int64{{15, 15}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + // 65, 65, 10, 35, 40, 40 + step2: [][2]int64{}, + // 65, 65, 10, 35, 6, 6 + step3: [][2]int64{{15, 15}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + // 35, 35, 10, 35, 40, 40 + step4: [][2]int64{}, + // 35, 35, 10, 35, 6, 6 + step5: [][2]int64{}, + }}, + {scenario: "TC11", out: expectedOutputA2{ + // 35, 65, 10, 35, 7, 40 + step1: [][2]int64{{9, 11}, {9, 9}, {9, 9}, {9, 9}, {9, 9}, {9, 9}, {9, 9}}, + // 65, 65, 10, 35, 40, 40 + step2: [][2]int64{}, + // 65, 65, 10, 35, 7, 7 + step3: [][2]int64{{9, 11}, {9, 9}, {9, 9}, {9, 9}, {9, 9}, {9, 9}, {9, 9}}, + // 35, 35, 10, 35, 40, 40 + step4: [][2]int64{}, + // 35, 35, 10, 35, 7, 7 + step5: [][2]int64{}, + }}, + {scenario: "TC12", out: expectedOutputA2{ + step1: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + step2: [][2]int64{}, + step3: [][2]int64{{20, 20}, {20, 20}, {15, 15}, {15, 15}, {15, 15}, {15, 15}}, + step4: [][2]int64{}, + step5: [][2]int64{{20, 20}, {20, 20}, {15, 15}, {15, 15}, {15, 15}, {15, 15}}, + }}, + {scenario: "TC13", out: expectedOutputA2{ + step1: [][2]int64{}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC14", out: expectedOutputA2{ + step1: [][2]int64{{5, 9}, {5, 9}, {5, 9}, {5, 9}, {5, 9}, {5, 9}}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{{5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}}, + step5: [][2]int64{}, + }}, + {scenario: "TC15", out: expectedOutputA2{ + step1: [][2]int64{{5, 9}, {5, 9}, {5, 9}, {5, 9}, {5, 9}}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{{5, 9}, {5, 6}, {5, 5}, {5, 5}, {5, 5}}, + step5: [][2]int64{}, + }}, + {scenario: "TC27", out: expectedOutputA2{ + step1: [][2]int64{{30, 30}, {30, 30}, {30, 30}}, + step2: [][2]int64{{30, 30}, {30, 30}, {30, 30}}, + step3: [][2]int64{{45, 45}, {45, 45}}, + step4: [][2]int64{}, + step5: [][2]int64{{2, 3}, {2, 2}}, + }}, + {scenario: "TC16", out: expectedOutputA2{ + step1: [][2]int64{{1, 12}, {1, 12}, {1, 12}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + step2: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {1, 6}}, + step3: [][2]int64{}, + step4: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {1, 6}}, + step5: [][2]int64{}, + }}, + {scenario: "TC17", out: expectedOutputA2{ + step1: [][2]int64{{1, 12}, {1, 12}, {1, 12}, {1, 12}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + step2: [][2]int64{{1, 11}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {6, 7}}, + step3: [][2]int64{}, + step4: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {6, 7}}, + step5: [][2]int64{}, + }}, + {scenario: "TC18", out: expectedOutputA2{ + step1: [][2]int64{}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC19", out: expectedOutputA2{ + step1: [][2]int64{}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC20", out: expectedOutputA2{ + step1: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC21", out: expectedOutputA2{ + step1: [][2]int64{{3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC23", out: expectedOutputA2{ + step1: [][2]int64{{4, 14}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{{4, 13}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}}, + step5: [][2]int64{}, + }}, + {scenario: "TC24", out: expectedOutputA2{ + step1: [][2]int64{{60, 69}, {65, 65}}, + step2: [][2]int64{}, + step3: [][2]int64{{60, 69}, {65, 65}}, + step4: [][2]int64{}, + step5: [][2]int64{{60, 69}, {65, 65}}, + }}, + {scenario: "TC25", out: expectedOutputA2{ + step1: [][2]int64{{1, 68}, {20, 20}}, + step2: [][2]int64{{1, 68}, {20, 20}}, + step3: [][2]int64{{1, 68}, {20, 20}}, + step4: [][2]int64{{1, 68}, {20, 20}}, + step5: [][2]int64{{1, 68}, {20, 20}}, + }}, + {scenario: "TC26", out: expectedOutputA2{ + step1: [][2]int64{{45, 45}, {45, 45}}, + step2: [][2]int64{}, + step3: [][2]int64{{45, 45}, {45, 45}}, + step4: [][2]int64{}, + step5: [][2]int64{{45, 45}, {45, 45}}, + }}, + {scenario: "TC27", out: expectedOutputA2{ + step1: [][2]int64{{30, 30}, {30, 30}, {30, 30}}, + step2: [][2]int64{{30, 30}, {30, 30}, {30, 30}}, + step3: [][2]int64{{45, 45}, {45, 45}}, + step4: [][2]int64{}, + step5: [][2]int64{{2, 3}, {2, 2}}, + }}, + {scenario: "TC28", out: expectedOutputA2{ + step1: [][2]int64{{30, 30}, {30, 30}, {30, 30}, {30, 30}, {30, 30}, {30, 30}}, + step2: [][2]int64{{30, 30}, {30, 30}, {30, 30}, {30, 30}, {30, 30}, {30, 30}}, + step3: [][2]int64{{90, 90}, {90, 90}}, + step4: [][2]int64{}, + step5: [][2]int64{{2, 3}, {2, 2}}, + }}, + {scenario: "TC29", out: expectedOutputA2{ + step1: [][2]int64{{25, 25}, {20, 20}, {20, 20}}, + step2: [][2]int64{{25, 25}, {20, 20}, {20, 20}}, + step3: [][2]int64{{35, 35}, {30, 30}}, + step4: [][2]int64{}, + step5: [][2]int64{{2, 3}, {2, 2}}, + }}, + {scenario: "TC30", out: expectedOutputA2{ + step1: [][2]int64{}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC31", out: expectedOutputA2{ + step1: [][2]int64{}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC32", out: expectedOutputA2{ + step1: [][2]int64{}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC33", out: expectedOutputA2{ + step1: [][2]int64{{30, 42}, {35, 35}, {35, 35}, {35, 35}}, + step2: [][2]int64{}, + step3: [][2]int64{{30, 42}, {35, 35}, {35, 35}, {35, 35}}, + step4: [][2]int64{}, + step5: [][2]int64{{30, 42}, {35, 35}, {35, 35}, {35, 35}}, + }}, + {scenario: "TC34", out: expectedOutputA2{ + step1: [][2]int64{{30, 30}, {30, 30}, {30, 30}}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC35", out: expectedOutputA2{ + step1: [][2]int64{}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC36", out: expectedOutputA2{ + step1: [][2]int64{{45, 45}, {45, 45}}, + step2: [][2]int64{}, + step3: [][2]int64{{45, 45}, {45, 45}}, + step4: [][2]int64{}, + step5: [][2]int64{{45, 45}, {45, 45}}, + }}, + {scenario: "TC37", out: expectedOutputA2{ + step1: [][2]int64{{25, 25}, {20, 20}}, + step2: [][2]int64{}, + step3: [][2]int64{{25, 25}, {20, 20}}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC38", out: expectedOutputA2{ + step1: [][2]int64{{25, 25}, {25, 25}, {20, 20}, {20, 20}}, + step2: [][2]int64{}, + step3: [][2]int64{{45, 45}, {45, 45}}, + step4: [][2]int64{}, + step5: [][2]int64{{45, 45}, {45, 45}}, + }}, + {scenario: "TC39", out: expectedOutputA2{ + step1: [][2]int64{{25, 25}, {25, 25}, {20, 20}, {20, 20}}, + step2: [][2]int64{}, + step3: [][2]int64{{45, 45}, {45, 45}}, + step4: [][2]int64{}, + step5: [][2]int64{{30, 30}, {30, 30}}, + }}, + {scenario: "TC40", out: expectedOutputA2{ + step1: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {5, 5}}, + step2: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {5, 5}}, + step3: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {5, 5}}, + step4: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {5, 5}}, + step5: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {5, 5}}, + }}, + {scenario: "TC41", out: expectedOutputA2{ + step1: [][2]int64{}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}}, + step5: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}}, + }}, + {scenario: "TC42", out: expectedOutputA2{ + step1: [][2]int64{{1, 1}}, + step2: [][2]int64{{1, 1}}, + step3: [][2]int64{{1, 1}}, + step4: [][2]int64{{1, 1}}, + step5: [][2]int64{{1, 1}}, + }}, + {scenario: "TC43", out: expectedOutputA2{ + step1: [][2]int64{}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC44", out: expectedOutputA2{ + step1: [][2]int64{}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC45", out: expectedOutputA2{ + step1: [][2]int64{}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC46", out: expectedOutputA2{ + step1: [][2]int64{}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC47", out: expectedOutputA2{ + step1: [][2]int64{{6, 6}}, + step2: [][2]int64{{6, 6}}, + step3: [][2]int64{{6, 6}}, + step4: [][2]int64{{6, 6}}, + step5: [][2]int64{{6, 6}}, + }}, + {scenario: "TC48", out: expectedOutputA2{ + step1: [][2]int64{{6, 6}, {6, 6}}, + step2: [][2]int64{{6, 6}, {6, 6}}, + step3: [][2]int64{}, + step4: [][2]int64{{6, 6}, {6, 6}}, + step5: [][2]int64{}, + }}, + {scenario: "TC49", out: expectedOutputA2{ + step1: [][2]int64{}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC50", out: expectedOutputA2{ + step1: [][2]int64{{1, 1}}, + step2: [][2]int64{{1, 1}}, + step3: [][2]int64{{1, 1}}, + step4: [][2]int64{{1, 1}}, + step5: [][2]int64{{1, 1}}, + }}, + {scenario: "TC51", out: expectedOutputA2{ + step1: [][2]int64{{13, 13}, {13, 13}, {13, 13}}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC52", out: expectedOutputA2{ + step1: [][2]int64{{12, 18}, {12, 18}, {12, 18}, {12, 18}}, + step2: [][2]int64{{12, 18}, {12, 18}, {12, 18}, {12, 18}}, + step3: [][2]int64{}, + step4: [][2]int64{{12, 18}, {12, 18}, {12, 17}, {15, 15}}, + step5: [][2]int64{}, + }}, + {scenario: "TC53", out: expectedOutputA2{ + step1: [][2]int64{{20, 20}, {20, 20}, {20, 20}, {20, 20}, {20, 20}, {20, 20}, {1, 6}}, + step2: [][2]int64{{20, 20}, {20, 20}, {20, 20}, {20, 20}, {20, 20}, {20, 20}, {1, 6}}, + step3: [][2]int64{}, + step4: [][2]int64{{20, 20}, {20, 20}, {20, 20}, {20, 20}, {20, 20}, {20, 20}, {1, 6}}, + step5: [][2]int64{}, + }}, + // {1, 74, 12, 12, 1, 6} + {scenario: "TC55", out: expectedOutputA2{ + step1: [][2]int64{{12, 12}, {12, 12}, {12, 12}, {12, 12}, {12, 12}, {12, 12}}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{}, + step5: [][2]int64{}, + }}, + {scenario: "TC56", out: expectedOutputA2{ + step1: [][2]int64{{126, 126}}, + step2: [][2]int64{{126, 126}}, + step3: [][2]int64{{126, 126}}, + step4: [][2]int64{{126, 126}}, + step5: [][2]int64{{126, 126}}, + }}, + {scenario: "TC57", out: expectedOutputA2{ + step1: [][2]int64{{126, 126}}, + step2: [][2]int64{}, + step3: [][2]int64{{126, 126}}, + step4: [][2]int64{}, + step5: [][2]int64{{126, 126}}, + }}, + {scenario: "TC58", out: expectedOutputA2{ + step1: [][2]int64{{25, 25}, {25, 25}, {20, 20}, {20, 20}}, + step2: [][2]int64{{25, 25}, {25, 25}, {20, 20}, {20, 20}}, + step3: [][2]int64{{45, 45}, {45, 45}}, + step4: [][2]int64{}, + step5: [][2]int64{{15, 15}, {15, 15}}, + }}, + {scenario: "TC59", out: expectedOutputA2{ + step1: [][2]int64{{45, 45}}, + step2: [][2]int64{}, + step3: [][2]int64{}, + step4: [][2]int64{{30, 30}}, + step5: [][2]int64{{30, 30}}, + }}, + // {scenario: "TC1" , out: expectedOutputA2{ + // step1: [][2]int64{}, + // step2: [][2]int64{}, + // step3: [][2]int64{}, + // step4: [][2]int64{}, + // step5: [][2]int64{}, + // }}, + // Testcases with realistic scenarios + + // {scenario: "TC_3_to_4_Ads_Of_5_to_10_Sec" /*in: []int{15, 40, 5, 10, 3, 4},*/, out: expectedOutputA2{ + // // 15, 40, 5, 10, 3, 4 + // step1: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}}, + // // 40, 40, 5, 10, 4, 4 + // step2: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}}, + // // 40, 40, 5, 10, 3, 3 + // step3: [][2]int64{}, + // // 15, 15, 5, 10, 4, 4 + // step4: [][2]int64{}, + // // 15, 15, 5, 10, 3, 3 + // step5: [][2]int64{{5, 5}, {5, 5}, {5, 5}}, + // }}, + // {scenario: "TC_4_to_6_Ads_Of_2_to_25_Sec" /*in: []int{60, 77, 2, 25, 4, 6}, */, out: expectedOutputA2{ + // // 60, 77, 2, 25, 4, 6 + // step1: [][2]int64{{2, 17}, {15, 15}, {15, 15}, {10, 10}, {10, 10}, {10, 10}}, + // // 77, 77, 5, 25, 6, 6 + // step2: [][2]int64{{2, 17}, {15, 15}, {15, 15}, {10, 10}, {10, 10}, {10, 10}}, + // // 77, 77, 5, 25, 4, 4 + // step3: [][2]int64{{25, 25}, {25, 25}, {2, 22}, {5, 5}}, + // // 60, 60, 5, 25, 6, 6 + // step4: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + // // 60, 60, 5, 25, 4, 4 + // step5: [][2]int64{{15, 15}, {15, 15}, {15, 15}, {15, 15}}, + // }}, + + // {scenario: "TC_2_to_6_ads_of_15_to_45_sec" /*in: []int{60, 90, 15, 45, 2, 6},*/, out: expectedOutputA2{ + // // 60, 90, 15, 45, 2, 6 + // step1: [][2]int64{{15, 15}, {15, 15}, {15, 15}, {15, 15}, {15, 15}, {15, 15}}, + // // 90, 90, 15, 45, 6, 6 + // step2: [][2]int64{{15, 15}, {15, 15}, {15, 15}, {15, 15}, {15, 15}, {15, 15}}, + // // 90, 90, 15, 45, 2, 2 + // step3: [][2]int64{{45, 45}, {45, 45}}, + // // 60, 60, 15, 45, 6, 6 + // step4: [][2]int64{}, + // // 60, 60, 15, 45, 2, 2 + // step5: [][2]int64{{30, 30}, {30, 30}}, + // }}, + +} + +func TestGetImpressionsA2(t *testing.T) { + for _, impTest := range impressionsTestsA2 { + t.Run(impTest.scenario, func(t *testing.T) { + in := testdata.Input[impTest.scenario] + p := newTestPod(int64(in[0]), int64(in[1]), in[2], in[3], in[4], in[5]) + a2 := newMinMaxAlgorithm(p.podMinDuration, p.podMaxDuration, p.vPod) + expectedMergedOutput := make([][2]int64, 0) + // explictly looping in order to check result of individual generator + for step, gen := range a2.generator { + switch step { + case 0: // algo1 equaivalent + assert.Equal(t, impTest.out.step1, gen.Get()) + expectedMergedOutput = appendOptimized(expectedMergedOutput, impTest.out.step1) + break + case 1: // pod duration = pod max duration, no of ads = maxads + assert.Equal(t, impTest.out.step2, gen.Get()) + expectedMergedOutput = appendOptimized(expectedMergedOutput, impTest.out.step2) + break + case 2: // pod duration = pod max duration, no of ads = minads + assert.Equal(t, impTest.out.step3, gen.Get()) + expectedMergedOutput = appendOptimized(expectedMergedOutput, impTest.out.step3) + break + case 3: // pod duration = pod min duration, no of ads = maxads + assert.Equal(t, impTest.out.step4, gen.Get()) + expectedMergedOutput = appendOptimized(expectedMergedOutput, impTest.out.step4) + break + case 4: // pod duration = pod min duration, no of ads = minads + assert.Equal(t, impTest.out.step5, gen.Get()) + expectedMergedOutput = appendOptimized(expectedMergedOutput, impTest.out.step5) + break + } + + } + // also verify merged output + expectedMergedOutput = testdata.Scenario[impTest.scenario].MinMaxAlgorithm + out := sortOutput(a2.Get()) + //fmt.Println(out) + assert.Equal(t, sortOutput(expectedMergedOutput), out) + }) + } +} + +func BenchmarkGetImpressionsA2(b *testing.B) { + for _, impTest := range impressionsTestsA2 { + for i := 0; i < b.N; i++ { + in := testdata.Input[impTest.scenario] + p := newTestPod(int64(in[0]), int64(in[1]), in[2], in[3], in[4], in[5]) + a2 := newMinMaxAlgorithm(p.podMinDuration, p.podMaxDuration, p.vPod) + a2.Get() + } + } +} + +func sortOutput(imps [][2]int64) [][2]int64 { + sort.Slice(imps, func(i, j int) bool { + return imps[i][1] < imps[j][1] + }) + return imps +} + +func appendOptimized(slice [][2]int64, elems [][2]int64) [][2]int64 { + m := make(map[string]int, 0) + keys := make([]string, 0) + for _, sel := range slice { + k := getKey(sel) + m[k]++ + keys = append(keys, k) + } + elemsmap := make(map[string]int, 0) + for _, ele := range elems { + elemsmap[getKey(ele)]++ + } + + for k := range elemsmap { + if elemsmap[k] > m[k] { + m[k] = elemsmap[k] + } + + keyPresent := false + for _, kl := range keys { + if kl == k { + keyPresent = true + break + } + } + + if !keyPresent { + keys = append(keys, k) + } + } + + optimized := make([][2]int64, 0) + for k, v := range m { + for i := 1; i <= v; i++ { + optimized = append(optimized, getImpression(k)) + } + } + return optimized +} diff --git a/endpoints/openrtb2/ctv/impressions/testdata/input.go b/endpoints/openrtb2/ctv/impressions/testdata/input.go new file mode 100644 index 00000000000..3ee64544b95 --- /dev/null +++ b/endpoints/openrtb2/ctv/impressions/testdata/input.go @@ -0,0 +1,61 @@ +package testdata + +// Input Test Input +var Input = map[string][]int{ + "TC2": {1, 90, 11, 15, 2, 8}, + "TC3": {1, 90, 11, 15, 2, 4}, + "TC4": {1, 15, 1, 15, 1, 1}, + "TC5": {1, 15, 1, 15, 1, 2}, + "TC6": {1, 90, 1, 15, 1, 8}, + "TC7": {15, 30, 8, 15, 1, 1}, + "TC8": {35, 35, 10, 35, 3, 40}, + "TC9": {35, 35, 10, 35, 6, 40}, + "TC10": {35, 65, 10, 35, 6, 40}, + "TC11": {35, 65, 9, 35, 7, 40}, + "TC12": {100, 100, 10, 35, 6, 40}, + "TC13": {60, 60, 5, 9, 1, 6}, + "TC14": {30, 60, 5, 9, 1, 6}, + "TC15": {30, 60, 5, 9, 1, 5}, + "TC16": {126, 126, 1, 12, 7, 13}, /* Exact Pod Duration */ + "TC17": {127, 128, 1, 12, 7, 13}, + "TC18": {125, 125, 4, 4, 1, 1}, + "TC19": {90, 90, 7, 9, 3, 5}, + "TC20": {90, 90, 5, 10, 1, 11}, + "TC21": {2, 170, 3, 9, 4, 9}, + "TC23": {118, 124, 4, 17, 6, 15}, + "TC24": {134, 134, 60, 90, 2, 3}, + "TC25": {88, 88, 1, 80, 2, 2}, + "TC26": {90, 90, 45, 45, 2, 3}, + "TC27": {5, 90, 2, 45, 2, 3}, + "TC28": {5, 180, 2, 90, 2, 6}, + "TC29": {5, 65, 2, 35, 2, 3}, + "TC30": {123, 123, 34, 34, 3, 3}, + "TC31": {123, 123, 31, 31, 3, 3}, + "TC32": {134, 134, 63, 63, 2, 3}, + "TC33": {147, 147, 30, 60, 4, 6}, + "TC34": {88, 102, 30, 30, 3, 3}, + "TC35": {88, 102, 30, 42, 3, 3}, + "TC36": {90, 90, 45, 45, 2, 5}, + "TC37": {10, 45, 20, 45, 2, 5}, + "TC38": {90, 90, 20, 45, 2, 5}, + "TC39": {60, 90, 20, 45, 2, 5}, + "TC40": {95, 95, 5, 45, 10, 10}, + "TC41": {95, 123, 5, 45, 13, 13}, + "TC42": {1, 1, 1, 1, 1, 1}, + "TC43": {2, 2, 2, 2, 2, 2}, + "TC44": {0, 0, 0, 0, 0, 0}, + "TC45": {-1, -2, -3, -4, -5, -6}, + "TC46": {-1, -1, -1, -1, -1, -1}, + "TC47": {6, 6, 6, 6, 1, 1}, + "TC48": {12, 12, 6, 6, 1, 2}, + "TC49": {12, 12, 7, 7, 1, 2}, + "TC50": {1, 1, 1, 1, 1, 1}, + "TC51": {31, 43, 11, 13, 2, 3}, + "TC52": {68, 72, 12, 18, 2, 4}, + "TC53": {126, 126, 1, 20, 1, 7}, + "TC55": {1, 74, 12, 12, 1, 6}, + "TC56": {126, 126, 126, 126, 1, 1}, + "TC57": {126, 126, 126, 126, 1, 3}, + "TC58": {30, 90, 15, 45, 2, 4}, + "TC59": {30, 90, 15, 45, 1, 1}, +} diff --git a/endpoints/openrtb2/ctv/impressions/testdata/output.go b/endpoints/openrtb2/ctv/impressions/testdata/output.go new file mode 100644 index 00000000000..d7e854fc575 --- /dev/null +++ b/endpoints/openrtb2/ctv/impressions/testdata/output.go @@ -0,0 +1,236 @@ +package testdata + +type eout struct { + MaximizeForDuration [][2]int64 + MinMaxAlgorithm [][2]int64 +} + +// Scenario returns expected impression breaks for given algorithm and for given +// test scenario +var Scenario = map[string]eout{ + + "TC2": { + MaximizeForDuration: [][2]int64{{15, 15}, {15, 15}, {15, 15}, {15, 15}, {15, 15}, {15, 15}}, + MinMaxAlgorithm: [][2]int64{{11, 13}, {11, 11}, {11, 11}, {11, 11}, {11, 11}, {11, 11}, {11, 11}, {11, 11}, {11, 15}, {11, 15}, {11, 15}, {11, 15}, {11, 15}, {11, 15}}}, + + "TC3": { + MaximizeForDuration: [][2]int64{{15, 15}, {15, 15}, {15, 15}, {15, 15}}, + MinMaxAlgorithm: [][2]int64{{11, 15}, {11, 15}, {11, 15}, {11, 15}}, + }, + "TC4": { + MaximizeForDuration: [][2]int64{{15, 15}}, + MinMaxAlgorithm: [][2]int64{{1, 15}, {1, 1}}, + }, + "TC5": { + MaximizeForDuration: [][2]int64{{10, 10}, {5, 5}}, + MinMaxAlgorithm: [][2]int64{{1, 1}, {1, 5}, {1, 15}, {1, 10}}, + }, + "TC6": { + MaximizeForDuration: [][2]int64{{15, 15}, {15, 15}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + MinMaxAlgorithm: [][2]int64{{1, 15}, {1, 15}, {1, 10}, {1, 10}, {1, 10}, {1, 10}, {1, 10}, {1, 10}, {1, 1}}, + }, + "TC7": { + MaximizeForDuration: [][2]int64{{15, 15}}, + MinMaxAlgorithm: [][2]int64{{15, 15}}, + }, + "TC8": { + MaximizeForDuration: [][2]int64{{15, 15}, {10, 10}, {10, 10}}, + MinMaxAlgorithm: [][2]int64{{10, 10}, {10, 10}, {15, 15}}, + }, + "TC9": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{}, + }, + "TC10": { + MaximizeForDuration: [][2]int64{{15, 15}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + MinMaxAlgorithm: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 15}}, + }, + "TC11": { + MaximizeForDuration: [][2]int64{{9, 11}, {9, 9}, {9, 9}, {9, 9}, {9, 9}, {9, 9}, {9, 9}}, + MinMaxAlgorithm: [][2]int64{{9, 11}, {9, 9}, {9, 9}, {9, 9}, {9, 9}, {9, 9}, {9, 9}}, + }, + "TC12": { + MaximizeForDuration: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + MinMaxAlgorithm: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {20, 20}, {20, 20}, {15, 15}, {15, 15}, {15, 15}, {15, 15}}, + }, + "TC13": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{}, + }, + "TC14": { + MaximizeForDuration: [][2]int64{{5, 9}, {5, 9}, {5, 9}, {5, 9}, {5, 9}, {5, 9}}, + MinMaxAlgorithm: [][2]int64{{5, 9}, {5, 9}, {5, 9}, {5, 9}, {5, 9}, {5, 9}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}}, + }, + "TC15": { + MaximizeForDuration: [][2]int64{{5, 9}, {5, 9}, {5, 9}, {5, 9}, {5, 9}}, + MinMaxAlgorithm: [][2]int64{{5, 9}, {5, 9}, {5, 9}, {5, 9}, {5, 9}, {5, 6}, {5, 5}, {5, 5}, {5, 5}}, + }, + "TC16": { + MaximizeForDuration: [][2]int64{{1, 12}, {1, 12}, {1, 12}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + MinMaxAlgorithm: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {1, 6}, {1, 12}, {1, 12}, {1, 12}}, + }, + "TC17": { + MaximizeForDuration: [][2]int64{{1, 12}, {1, 12}, {1, 12}, {1, 12}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + MinMaxAlgorithm: [][2]int64{{1, 11}, {1, 7}, {1, 12}, {1, 12}, {1, 12}, {1, 12}, {1, 10}, {1, 10}, {1, 10}, {1, 10}, {1, 10}, {1, 10}, {1, 10}, {1, 10}, {1, 10}, {1, 10}, {1, 10}, {1, 10}}, + }, + "TC18": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{}, + }, + "TC19": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{}, + }, + "TC20": { + MaximizeForDuration: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + MinMaxAlgorithm: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + }, + "TC21": { + MaximizeForDuration: [][2]int64{{3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}}, + MinMaxAlgorithm: [][2]int64{{3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}, {3, 9}}, + }, + "TC23": { + MaximizeForDuration: [][2]int64{{4, 14}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}}, + MinMaxAlgorithm: [][2]int64{{4, 13}, {4, 5}, {4, 5}, {4, 5}, {4, 5}, {4, 5}, {4, 5}, {4, 5}, {4, 14}, {4, 10}, {4, 10}, {4, 10}, {4, 10}, {4, 10}, {4, 10}, {4, 10}, {4, 10}, {4, 10}, {4, 10}, {4, 10}}, + }, + "TC24": { + MaximizeForDuration: [][2]int64{{60, 69}, {65, 65}}, + MinMaxAlgorithm: [][2]int64{{60, 69}, {65, 65}}, + }, + "TC25": { + MaximizeForDuration: [][2]int64{{1, 68}, {20, 20}}, + MinMaxAlgorithm: [][2]int64{{1, 68}, {20, 20}}, + }, + "TC26": { + MaximizeForDuration: [][2]int64{{45, 45}, {45, 45}}, + MinMaxAlgorithm: [][2]int64{{45, 45}, {45, 45}}, + }, + "TC27": { + MaximizeForDuration: [][2]int64{{30, 30}, {30, 30}, {30, 30}}, + MinMaxAlgorithm: [][2]int64{{3, 3}, {2, 2}, {3, 30}, {3, 30}, {3, 30}, {3, 45}, {3, 45}}, + }, + "TC28": { + MaximizeForDuration: [][2]int64{{30, 30}, {30, 30}, {30, 30}, {30, 30}, {30, 30}, {30, 30}}, + MinMaxAlgorithm: [][2]int64{{3, 90}, {3, 90}, {3, 3}, {2, 2}, {3, 30}, {3, 30}, {3, 30}, {3, 30}, {3, 30}, {3, 30}}, + }, + "TC29": { + MaximizeForDuration: [][2]int64{{25, 25}, {20, 20}, {20, 20}}, + MinMaxAlgorithm: [][2]int64{{3, 25}, {3, 20}, {3, 20}, {3, 3}, {2, 2}, {3, 35}, {3, 30}}, + }, + "TC30": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{}, + }, + "TC31": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{}, + }, + "TC32": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{}, + }, + "TC33": { + MaximizeForDuration: [][2]int64{{30, 42}, {35, 35}, {35, 35}, {35, 35}}, + MinMaxAlgorithm: [][2]int64{{30, 42}, {35, 35}, {35, 35}, {35, 35}}, + }, + "TC34": { + MaximizeForDuration: [][2]int64{{30, 30}, {30, 30}, {30, 30}}, + MinMaxAlgorithm: [][2]int64{{30, 30}, {30, 30}, {30, 30}}, + }, + "TC35": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{}, + }, + "TC36": { + MaximizeForDuration: [][2]int64{{45, 45}, {45, 45}}, + MinMaxAlgorithm: [][2]int64{{45, 45}, {45, 45}}, + }, + "TC37": { + MaximizeForDuration: [][2]int64{{25, 25}, {20, 20}}, + MinMaxAlgorithm: [][2]int64{{20, 20}, {20, 25}}, + }, + "TC38": { + MaximizeForDuration: [][2]int64{{25, 25}, {25, 25}, {20, 20}, {20, 20}}, + MinMaxAlgorithm: [][2]int64{{25, 25}, {25, 25}, {20, 20}, {20, 20}, {45, 45}, {45, 45}}, + }, + "TC39": { + MaximizeForDuration: [][2]int64{{25, 25}, {25, 25}, {20, 20}, {20, 20}}, + MinMaxAlgorithm: [][2]int64{{30, 45}, {30, 45}, {30, 30}, {30, 30}, {20, 25}, {20, 25}, {20, 20}, {20, 20}}, + }, + "TC40": { + MaximizeForDuration: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {5, 5}}, + MinMaxAlgorithm: [][2]int64{{10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {10, 10}, {5, 5}}, + }, + "TC41": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{{7, 10}, {7, 10}, {7, 10}, {7, 10}, {7, 10}, {7, 10}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}}, + }, + "TC42": { + MaximizeForDuration: [][2]int64{{1, 1}}, + MinMaxAlgorithm: [][2]int64{{1, 1}}, + }, + "TC43": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{}, + }, + "TC44": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{}, + }, + "TC45": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{}, + }, + "TC46": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{}, + }, + "TC47": { + MaximizeForDuration: [][2]int64{{6, 6}}, + MinMaxAlgorithm: [][2]int64{{6, 6}}, + }, + "TC48": { + MaximizeForDuration: [][2]int64{{6, 6}, {6, 6}}, + MinMaxAlgorithm: [][2]int64{{6, 6}, {6, 6}}, + }, + "TC49": { + MaximizeForDuration: [][2]int64{}, + MinMaxAlgorithm: [][2]int64{}, + }, + "TC50": { + MaximizeForDuration: [][2]int64{{1, 1}}, + MinMaxAlgorithm: [][2]int64{{1, 1}}, + }, + "TC51": { + MaximizeForDuration: [][2]int64{{13, 13}, {13, 13}, {13, 13}}, + MinMaxAlgorithm: [][2]int64{{11, 13}, {11, 13}, {11, 13}}, + }, + "TC52": { + MaximizeForDuration: [][2]int64{{12, 18}, {12, 18}, {12, 18}, {12, 18}}, + MinMaxAlgorithm: [][2]int64{{12, 17}, {12, 15}, {12, 18}, {12, 18}, {12, 18}, {12, 18}}, + }, + "TC53": { + MaximizeForDuration: [][2]int64{{20, 20}, {20, 20}, {20, 20}, {20, 20}, {20, 20}, {20, 20}, {1, 6}}, + MinMaxAlgorithm: [][2]int64{{1, 6}, {20, 20}, {20, 20}, {20, 20}, {20, 20}, {20, 20}, {20, 20}}, + }, + "TC55": { + MaximizeForDuration: [][2]int64{{12, 12}, {12, 12}, {12, 12}, {12, 12}, {12, 12}, {12, 12}}, + MinMaxAlgorithm: [][2]int64{{12, 12}, {12, 12}, {12, 12}, {12, 12}, {12, 12}, {12, 12}}, + }, + "TC56": { + MaximizeForDuration: [][2]int64{{126, 126}}, + MinMaxAlgorithm: [][2]int64{{126, 126}}, + }, + "TC57": { + MaximizeForDuration: [][2]int64{{126, 126}}, + MinMaxAlgorithm: [][2]int64{{126, 126}}, + }, + "TC58": { + MaximizeForDuration: [][2]int64{{25, 25}, {25, 25}, {20, 20}, {20, 20}}, + MinMaxAlgorithm: [][2]int64{{15, 15}, {15, 15}, {15, 20}, {15, 20}, {15, 25}, {15, 25}, {15, 45}, {15, 45}}, + }, + "TC59": { + MaximizeForDuration: [][2]int64{{45, 45}}, + MinMaxAlgorithm: [][2]int64{{30, 30}, {30, 45}}, + }, +} diff --git a/endpoints/openrtb2/ctv/response/adpod_generator copy.go.bak b/endpoints/openrtb2/ctv/response/adpod_generator copy.go.bak new file mode 100644 index 00000000000..df5b7e36be0 --- /dev/null +++ b/endpoints/openrtb2/ctv/response/adpod_generator copy.go.bak @@ -0,0 +1,276 @@ +package ctv + +import ( + "context" + "time" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +/********************* AdPodGenerator Functions *********************/ + +//IAdPodGenerator interface for generating AdPod from Ads +type IAdPodGenerator interface { + GetAdPodBids() *AdPodBid +} +type filteredBids struct { + bid *Bid + reasonCode FilterReasonCode +} +type highestCombination struct { + bids []*Bid + price float64 + categoryScore map[string]int + domainScore map[string]int + filteredBids []filteredBids +} + +//AdPodGenerator AdPodGenerator +type AdPodGenerator struct { + IAdPodGenerator + buckets BidsBuckets + comb ICombination + adpod *openrtb_ext.VideoAdPod +} + +//NewAdPodGenerator will generate adpod based on configuration +func NewAdPodGenerator(buckets BidsBuckets, comb ICombination, adpod *openrtb_ext.VideoAdPod) *AdPodGenerator { + return &AdPodGenerator{ + buckets: buckets, + comb: comb, + adpod: adpod, + } +} + +//GetAdPodBids will return Adpod based on configurations +func (o *AdPodGenerator) GetAdPodBids() *AdPodBid { + + isTimedOutORReceivedAllResponses := false + responseCount := 0 + totalRequest := 0 + maxRequests := 5 + responseCh := make(chan *highestCombination, maxRequests) + var results []*highestCombination + + timeout := 50 * time.Millisecond + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + for totalRequest < maxRequests { + durations := o.comb.Get() + if len(durations) == 0 { + break + } + + totalRequest++ + go o.getUniqueBids(responseCh, durations) + } + + for !isTimedOutORReceivedAllResponses { + select { + case <-ctx.Done(): + isTimedOutORReceivedAllResponses = true + case hbc := <-responseCh: + responseCount++ + if nil != hbc { + results = append(results, hbc) + } + if responseCount == totalRequest { + isTimedOutORReceivedAllResponses = true + } + } + } + + go cleanupResponseChannel(responseCh, totalRequest-responseCount) + + if 0 == len(results) { + return nil + } + + //Get Max Response + var maxResult *highestCombination + for _, result := range results { + if nil == maxResult || maxResult.price < result.price { + maxResult = result + } + + for _, rc := range result.filteredBids { + if CTVRCDidNotGetChance == rc.bid.FilterReasonCode { + rc.bid.FilterReasonCode = rc.reasonCode + } + } + } + + adpodBid := &AdPodBid{ + Bids: maxResult.bids[:], + Price: maxResult.price, + ADomain: make([]string, 0), + Cat: make([]string, 0), + } + + //Get Unique Domains + for domain := range maxResult.domainScore { + adpodBid.ADomain = append(adpodBid.ADomain, domain) + } + + //Get Unique Categories + for cat := range maxResult.categoryScore { + adpodBid.Cat = append(adpodBid.Cat, cat) + } + + return adpodBid +} + +func cleanupResponseChannel(responseCh <-chan *highestCombination, responseCount int) { + for responseCount > 0 { + <-responseCh + responseCount-- + } +} + +func (o *AdPodGenerator) getUniqueBids(responseCh chan<- *highestCombination, durationSequence []int) { + data := [][]*Bid{} + combinations := []int{} + + uniqueDuration := 0 + for index, duration := range durationSequence { + if 0 != index && durationSequence[index-1] == duration { + combinations[uniqueDuration-1]++ + continue + } + data = append(data, o.buckets[duration][:]) + combinations = append(combinations, 1) + uniqueDuration++ + } + hbc := findUniqueCombinations(data[:], combinations[:], *o.adpod.IABCategoryExclusionPercent, *o.adpod.AdvertiserExclusionPercent) + responseCh <- hbc +} + +func findUniqueCombinations(data [][]*Bid, combination []int, maxCategoryScore, maxDomainScore int) *highestCombination { + // number of arrays + n := len(combination) + totalBids := 0 + // to keep track of next element in each of the n arrays + // indices is initialized + indices := make([][]int, len(combination)) + for i := 0; i < len(combination); i++ { + indices[i] = make([]int, combination[i]) + for j := 0; j < combination[i]; j++ { + indices[i][j] = j + totalBids++ + } + } + + hc := &highestCombination{} + var ehc *highestCombination + var rc FilterReasonCode + inext, jnext := n-1, 0 + var filterBids []filteredBids + + // maintain highest price combination + for true { + + ehc, inext, jnext, rc = evaluate(data[:], indices[:], totalBids, maxCategoryScore, maxDomainScore) + if nil != ehc { + if nil == hc || hc.price < ehc.price { + hc = ehc + } else { + // if you see current combination price lower than the highest one then break the loop + break + } + } else { + //Filtered Bid + filterBids = append(filterBids, filteredBids{bid: data[inext][indices[inext][jnext]], reasonCode: rc}) + } + + if -1 == inext { + inext, jnext = n-1, 0 + } + + // find the rightmost array that has more + // elements left after the current element + // in that array + inext, jnext := n-1, 0 + + for inext >= 0 { + jnext = len(indices[inext]) - 1 + for jnext >= 0 && (indices[inext][jnext]+1 > (len(data[inext]) - len(indices[inext]) + jnext)) { + jnext-- + } + if jnext >= 0 { + break + } + inext-- + } + + // no such array is found so no more combinations left + if inext < 0 { + break + } + + // if found move to next element in that array + indices[inext][jnext]++ + + // for all arrays to the right of this + // array current index again points to + // first element + jnext++ + for i := inext; i < len(combination); i++ { + for j := jnext; j < combination[i]; j++ { + if i == inext { + indices[i][j] = indices[i][j-1] + 1 + } else { + indices[i][j] = j + } + } + jnext = 0 + } + } + + //setting filteredBids + if nil != hc { + hc.filteredBids = filterBids[:] + } + return hc +} + +func evaluate(bids [][]*Bid, indices [][]int, totalBids int, maxCategoryScore, maxDomainScore int) (*highestCombination, int, int, FilterReasonCode) { + + hbc := &highestCombination{ + bids: make([]*Bid, totalBids), + price: 0, + categoryScore: make(map[string]int), + domainScore: make(map[string]int), + } + pos := 0 + + for inext := range indices { + for jnext := range indices[inext] { + bid := bids[inext][indices[inext][jnext]] + + hbc.bids[pos] = bid + pos++ + + //Price + hbc.price = hbc.price + bid.Price + + //Categories + for _, cat := range bid.Cat { + hbc.categoryScore[cat]++ + if (hbc.categoryScore[cat] * 100 / totalBids) > maxCategoryScore { + return nil, inext, jnext, CTVRCCategoryExclusion + } + } + + //Domain + for _, domain := range bid.ADomain { + hbc.domainScore[domain]++ + if (hbc.domainScore[domain] * 100 / totalBids) > maxDomainScore { + return nil, inext, jnext, CTVRCDomainExclusion + } + } + } + } + + return hbc, -1, -1, CTVRCWinningBid +} diff --git a/endpoints/openrtb2/ctv/response/adpod_generator.go b/endpoints/openrtb2/ctv/response/adpod_generator.go new file mode 100644 index 00000000000..7b62079c401 --- /dev/null +++ b/endpoints/openrtb2/ctv/response/adpod_generator.go @@ -0,0 +1,386 @@ +package response + +import ( + "fmt" + "sync" + "time" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/combination" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/constant" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/types" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/util" + "github.com/prebid/prebid-server/metrics" + "github.com/prebid/prebid-server/openrtb_ext" +) + +/********************* AdPodGenerator Functions *********************/ + +//IAdPodGenerator interface for generating AdPod from Ads +type IAdPodGenerator interface { + GetAdPodBids() *types.AdPodBid +} +type filteredBid struct { + bid *types.Bid + status constant.BidStatus +} +type highestCombination struct { + bids []*types.Bid + bidIDs []string + durations []int + price float64 + categoryScore map[string]int + domainScore map[string]int + filteredBids map[string]*filteredBid + timeTakenCompExcl time.Duration // time taken by comp excl + timeTakenCombGen time.Duration // time taken by combination generator + nDealBids int +} + +//AdPodGenerator AdPodGenerator +type AdPodGenerator struct { + IAdPodGenerator + request *openrtb2.BidRequest + impIndex int + buckets types.BidsBuckets + comb combination.ICombination + adpod *openrtb_ext.VideoAdPod + met metrics.MetricsEngine +} + +//NewAdPodGenerator will generate adpod based on configuration +func NewAdPodGenerator(request *openrtb2.BidRequest, impIndex int, buckets types.BidsBuckets, comb combination.ICombination, adpod *openrtb_ext.VideoAdPod, met metrics.MetricsEngine) *AdPodGenerator { + return &AdPodGenerator{ + request: request, + impIndex: impIndex, + buckets: buckets, + comb: comb, + adpod: adpod, + met: met, + } +} + +//GetAdPodBids will return Adpod based on configurations +func (o *AdPodGenerator) GetAdPodBids() *types.AdPodBid { + defer util.TimeTrack(time.Now(), fmt.Sprintf("Tid:%v ImpId:%v adpodgenerator", o.request.ID, o.request.Imp[o.impIndex].ID)) + + results := o.getAdPodBids(10 * time.Millisecond) + adpodBid := o.getMaxAdPodBid(results) + + return adpodBid +} + +func (o *AdPodGenerator) cleanup(wg *sync.WaitGroup, responseCh chan *highestCombination) { + defer func() { + close(responseCh) + for extra := range responseCh { + if nil != extra { + util.Logf("Tid:%v ImpId:%v Delayed Response Durations:%v Bids:%v", o.request.ID, o.request.Imp[o.impIndex].ID, extra.durations, extra.bidIDs) + } + } + }() + wg.Wait() +} + +func (o *AdPodGenerator) getAdPodBids(timeout time.Duration) []*highestCombination { + start := time.Now() + defer util.TimeTrack(start, fmt.Sprintf("Tid:%v ImpId:%v getAdPodBids", o.request.ID, o.request.Imp[o.impIndex].ID)) + + maxRoutines := 3 + isTimedOutORReceivedAllResponses := false + results := []*highestCombination{} + responseCh := make(chan *highestCombination, maxRoutines) + wg := new(sync.WaitGroup) // ensures each step generating impressions is finished + lock := sync.Mutex{} + ticker := time.NewTicker(timeout) + + combinationCount := 0 + for i := 0; i < maxRoutines; i++ { + wg.Add(1) + go func() { + for !isTimedOutORReceivedAllResponses { + combGenStartTime := time.Now() + lock.Lock() + durations := o.comb.Get() + lock.Unlock() + combGenElapsedTime := time.Since(combGenStartTime) + + if len(durations) == 0 { + break + } + hbc := o.getUniqueBids(durations) + hbc.timeTakenCombGen = combGenElapsedTime + responseCh <- hbc + util.Logf("Tid:%v GetUniqueBids Durations:%v Price:%v DealBids:%v Time:%v Bids:%v", o.request.ID, hbc.durations[:], hbc.price, hbc.nDealBids, hbc.timeTakenCompExcl, hbc.bidIDs[:]) + } + wg.Done() + }() + } + + // ensure impressions channel is closed + // when all go routines are executed + go o.cleanup(wg, responseCh) + + totalTimeByCombGen := int64(0) + totalTimeByCompExcl := int64(0) + for !isTimedOutORReceivedAllResponses { + select { + case hbc, ok := <-responseCh: + + if false == ok { + isTimedOutORReceivedAllResponses = true + break + } + if nil != hbc { + combinationCount++ + totalTimeByCombGen += int64(hbc.timeTakenCombGen) + totalTimeByCompExcl += int64(hbc.timeTakenCompExcl) + results = append(results, hbc) + } + case <-ticker.C: + isTimedOutORReceivedAllResponses = true + util.Logf("Tid:%v ImpId:%v GetAdPodBids Timeout Reached %v", o.request.ID, o.request.Imp[o.impIndex].ID, timeout) + } + } + + defer ticker.Stop() + + labels := metrics.PodLabels{ + AlgorithmName: string(constant.CombinationGeneratorV1), + NoOfCombinations: new(int), + } + *labels.NoOfCombinations = combinationCount + o.met.RecordPodCombGenTime(labels, time.Duration(totalTimeByCombGen)) + + compExclLabels := metrics.PodLabels{ + AlgorithmName: string(constant.CompetitiveExclusionV1), + NoOfResponseBids: new(int), + } + *compExclLabels.NoOfResponseBids = 0 + for _, ads := range o.buckets { + *compExclLabels.NoOfResponseBids += len(ads) + } + o.met.RecordPodCompititveExclusionTime(compExclLabels, time.Duration(totalTimeByCompExcl)) + + return results[:] +} + +func (o *AdPodGenerator) getMaxAdPodBid(results []*highestCombination) *types.AdPodBid { + if 0 == len(results) { + util.Logf("Tid:%v ImpId:%v NoBid", o.request.ID, o.request.Imp[o.impIndex].ID) + return nil + } + + //Get Max Response + var maxResult *highestCombination + for _, result := range results { + for _, rc := range result.filteredBids { + if constant.StatusOK == rc.bid.Status { + rc.bid.Status = rc.status + } + } + if len(result.bidIDs) == 0 { + continue + } + + if nil == maxResult || + (maxResult.nDealBids < result.nDealBids) || + (maxResult.nDealBids == result.nDealBids && maxResult.price < result.price) { + maxResult = result + } + } + + if nil == maxResult { + util.Logf("Tid:%v ImpId:%v All Combination Filtered in Ad Exclusion", o.request.ID, o.request.Imp[o.impIndex].ID) + return nil + } + + adpodBid := &types.AdPodBid{ + Bids: maxResult.bids[:], + Price: maxResult.price, + ADomain: make([]string, 0), + Cat: make([]string, 0), + } + + //Get Unique Domains + for domain := range maxResult.domainScore { + adpodBid.ADomain = append(adpodBid.ADomain, domain) + } + + //Get Unique Categories + for cat := range maxResult.categoryScore { + adpodBid.Cat = append(adpodBid.Cat, cat) + } + + util.Logf("Tid:%v ImpId:%v Selected Durations:%v Price:%v Bids:%v", o.request.ID, o.request.Imp[o.impIndex].ID, maxResult.durations[:], maxResult.price, maxResult.bidIDs[:]) + + return adpodBid +} + +func (o *AdPodGenerator) getUniqueBids(durationSequence []int) *highestCombination { + startTime := time.Now() + data := [][]*types.Bid{} + combinations := []int{} + + defer util.TimeTrack(startTime, fmt.Sprintf("Tid:%v ImpId:%v getUniqueBids:%v", o.request.ID, o.request.Imp[o.impIndex].ID, durationSequence)) + + uniqueDuration := 0 + for index, duration := range durationSequence { + if 0 != index && durationSequence[index-1] == duration { + combinations[uniqueDuration-1]++ + continue + } + data = append(data, o.buckets[duration][:]) + combinations = append(combinations, 1) + uniqueDuration++ + } + hbc := findUniqueCombinations(data[:], combinations[:], *o.adpod.IABCategoryExclusionPercent, *o.adpod.AdvertiserExclusionPercent) + hbc.durations = durationSequence[:] + hbc.timeTakenCompExcl = time.Since(startTime) + + return hbc +} + +func findUniqueCombinations(data [][]*types.Bid, combination []int, maxCategoryScore, maxDomainScore int) *highestCombination { + // number of arrays + n := len(combination) + totalBids := 0 + // to keep track of next element in each of the n arrays + // indices is initialized + indices := make([][]int, len(combination)) + for i := 0; i < len(combination); i++ { + indices[i] = make([]int, combination[i]) + for j := 0; j < combination[i]; j++ { + indices[i][j] = j + totalBids++ + } + } + + hc := &highestCombination{} + var ehc *highestCombination + var rc constant.BidStatus + inext, jnext := n-1, 0 + filterBids := map[string]*filteredBid{} + + // maintain highest price combination + for true { + + ehc, inext, jnext, rc = evaluate(data[:], indices[:], totalBids, maxCategoryScore, maxDomainScore) + if nil != ehc { + if nil == hc || (hc.nDealBids == ehc.nDealBids && hc.price < ehc.price) || (hc.nDealBids < ehc.nDealBids) { + hc = ehc + } else { + // if you see current combination price lower than the highest one then break the loop + break + } + } else { + //Filtered Bid + for i := 0; i <= inext; i++ { + for j := 0; j < combination[i] && !(i == inext && j > jnext); j++ { + bid := data[i][indices[i][j]] + if _, ok := filterBids[bid.ID]; !ok { + filterBids[bid.ID] = &filteredBid{bid: bid, status: rc} + } + } + } + } + + if -1 == inext { + inext, jnext = n-1, 0 + } + + // find the rightmost array that has more + // elements left after the current element + // in that array + inext, jnext := n-1, 0 + + for inext >= 0 { + jnext = len(indices[inext]) - 1 + for jnext >= 0 && (indices[inext][jnext]+1 > (len(data[inext]) - len(indices[inext]) + jnext)) { + jnext-- + } + if jnext >= 0 { + break + } + inext-- + } + + // no such array is found so no more combinations left + if inext < 0 { + break + } + + // if found move to next element in that array + indices[inext][jnext]++ + + // for all arrays to the right of this + // array current index again points to + // first element + jnext++ + for i := inext; i < len(combination); i++ { + for j := jnext; j < combination[i]; j++ { + if i == inext { + indices[i][j] = indices[i][j-1] + 1 + } else { + indices[i][j] = j + } + } + jnext = 0 + } + } + + //setting filteredBids + if nil != filterBids { + hc.filteredBids = filterBids + } + return hc +} + +func evaluate(bids [][]*types.Bid, indices [][]int, totalBids int, maxCategoryScore, maxDomainScore int) (*highestCombination, int, int, constant.BidStatus) { + + hbc := &highestCombination{ + bids: make([]*types.Bid, totalBids), + bidIDs: make([]string, totalBids), + price: 0, + categoryScore: make(map[string]int), + domainScore: make(map[string]int), + nDealBids: 0, + } + pos := 0 + + for inext := range indices { + for jnext := range indices[inext] { + bid := bids[inext][indices[inext][jnext]] + + hbc.bids[pos] = bid + hbc.bidIDs[pos] = bid.ID + pos++ + + //nDealBids + if bid.DealTierSatisfied { + hbc.nDealBids++ + } + + //Price + hbc.price = hbc.price + bid.Price + + //Categories + for _, cat := range bid.Cat { + hbc.categoryScore[cat]++ + if hbc.categoryScore[cat] > 1 && (hbc.categoryScore[cat]*100/totalBids) > maxCategoryScore { + return nil, inext, jnext, constant.StatusCategoryExclusion + } + } + + //Domain + for _, domain := range bid.ADomain { + hbc.domainScore[domain]++ + if hbc.domainScore[domain] > 1 && (hbc.domainScore[domain]*100/totalBids) > maxDomainScore { + return nil, inext, jnext, constant.StatusDomainExclusion + } + } + } + } + + return hbc, -1, -1, constant.StatusWinningBid +} diff --git a/endpoints/openrtb2/ctv/response/adpod_generator_test.go b/endpoints/openrtb2/ctv/response/adpod_generator_test.go new file mode 100644 index 00000000000..8e7d12e8d94 --- /dev/null +++ b/endpoints/openrtb2/ctv/response/adpod_generator_test.go @@ -0,0 +1,398 @@ +package response + +import ( + "sort" + "testing" + + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/constant" + + "github.com/stretchr/testify/assert" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/types" +) + +func Test_findUniqueCombinations(t *testing.T) { + type args struct { + data [][]*types.Bid + combination []int + maxCategoryScore int + maxDomainScore int + } + tests := []struct { + name string + args args + want *highestCombination + }{ + { + name: "sample", + args: args{ + data: [][]*types.Bid{ + { + { + Bid: &openrtb2.Bid{ID: "3-ed72b572-ba62-4220-abba-c19c0bf6346b", Price: 6.339115524232314}, + DealTierSatisfied: true, + }, + { + Bid: &openrtb2.Bid{ID: "4-ed72b572-ba62-4220-abba-c19c0bf6346b", Price: 3.532468782358357}, + DealTierSatisfied: true, + }, + { + Bid: &openrtb2.Bid{ID: "7-VIDEO12-89A1-41F1-8708-978FD3C0912A", Price: 5}, + DealTierSatisfied: false, + }, + { + Bid: &openrtb2.Bid{ID: "8-VIDEO12-89A1-41F1-8708-978FD3C0912A", Price: 5}, + DealTierSatisfied: false, + }, + }, //20 + + { + { + Bid: &openrtb2.Bid{ID: "2-ed72b572-ba62-4220-abba-c19c0bf6346b", Price: 3.4502433547413878}, + DealTierSatisfied: true, + }, + { + Bid: &openrtb2.Bid{ID: "1-ed72b572-ba62-4220-abba-c19c0bf6346b", Price: 3.329644588311827}, + DealTierSatisfied: true, + }, + { + Bid: &openrtb2.Bid{ID: "5-VIDEO12-89A1-41F1-8708-978FD3C0912A", Price: 5}, + DealTierSatisfied: false, + }, + { + Bid: &openrtb2.Bid{ID: "6-VIDEO12-89A1-41F1-8708-978FD3C0912A", Price: 5}, + DealTierSatisfied: false, + }, + }, //25 + }, + + combination: []int{2, 2}, + maxCategoryScore: 100, + maxDomainScore: 100, + }, + want: &highestCombination{ + bidIDs: []string{"3-ed72b572-ba62-4220-abba-c19c0bf6346b", "4-ed72b572-ba62-4220-abba-c19c0bf6346b", "2-ed72b572-ba62-4220-abba-c19c0bf6346b", "1-ed72b572-ba62-4220-abba-c19c0bf6346b"}, + price: 16.651472249643884, + nDealBids: 4, + }, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := findUniqueCombinations(tt.args.data, tt.args.combination, tt.args.maxCategoryScore, tt.args.maxDomainScore) + assert.Equal(t, tt.want.bidIDs, got.bidIDs, "bidIDs") + assert.Equal(t, tt.want.nDealBids, got.nDealBids, "nDealBids") + assert.Equal(t, tt.want.price, got.price, "price") + }) + } +} + +func TestAdPodGenerator_getMaxAdPodBid(t *testing.T) { + type fields struct { + request *openrtb2.BidRequest + impIndex int + } + type args struct { + results []*highestCombination + } + tests := []struct { + name string + fields fields + args args + want *types.AdPodBid + }{ + { + name: `EmptyResults`, + fields: fields{ + request: &openrtb2.BidRequest{ID: `req-1`, Imp: []openrtb2.Imp{{ID: `imp-1`}}}, + impIndex: 0, + }, + args: args{ + results: nil, + }, + want: nil, + }, + { + name: `AllBidsFiltered`, + fields: fields{ + request: &openrtb2.BidRequest{ID: `req-1`, Imp: []openrtb2.Imp{{ID: `imp-1`}}}, + impIndex: 0, + }, + args: args{ + results: []*highestCombination{ + { + filteredBids: map[string]*filteredBid{ + `bid-1`: {bid: &types.Bid{Bid: &openrtb2.Bid{ID: `bid-1`}}, status: constant.StatusCategoryExclusion}, + `bid-2`: {bid: &types.Bid{Bid: &openrtb2.Bid{ID: `bid-2`}}, status: constant.StatusCategoryExclusion}, + `bid-3`: {bid: &types.Bid{Bid: &openrtb2.Bid{ID: `bid-3`}}, status: constant.StatusCategoryExclusion}, + }, + }, + }, + }, + want: nil, + }, + { + name: `SingleResponse`, + fields: fields{ + request: &openrtb2.BidRequest{ID: `req-1`, Imp: []openrtb2.Imp{{ID: `imp-1`}}}, + impIndex: 0, + }, + args: args{ + results: []*highestCombination{ + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-1`}}, + {Bid: &openrtb2.Bid{ID: `bid-2`}}, + {Bid: &openrtb2.Bid{ID: `bid-3`}}, + }, + bidIDs: []string{`bid-1`, `bid-2`, `bid-3`}, + price: 20, + nDealBids: 0, + categoryScore: map[string]int{ + `cat-1`: 1, + `cat-2`: 1, + }, + domainScore: map[string]int{ + `domain-1`: 1, + `domain-2`: 1, + }, + filteredBids: map[string]*filteredBid{ + `bid-4`: {bid: &types.Bid{Bid: &openrtb2.Bid{ID: `bid-4`}}, status: constant.StatusCategoryExclusion}, + }, + }, + }, + }, + want: &types.AdPodBid{ + Bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-1`}}, + {Bid: &openrtb2.Bid{ID: `bid-2`}}, + {Bid: &openrtb2.Bid{ID: `bid-3`}}, + }, + Cat: []string{`cat-1`, `cat-2`}, + ADomain: []string{`domain-1`, `domain-2`}, + Price: 20, + }, + }, + { + name: `MultiResponse-AllNonDealBids`, + fields: fields{ + request: &openrtb2.BidRequest{ID: `req-1`, Imp: []openrtb2.Imp{{ID: `imp-1`}}}, + impIndex: 0, + }, + args: args{ + results: []*highestCombination{ + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-11`}}, + }, + bidIDs: []string{`bid-11`}, + price: 10, + nDealBids: 0, + }, + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-21`}}, + }, + bidIDs: []string{`bid-21`}, + price: 20, + nDealBids: 0, + }, + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-31`}}, + }, + bidIDs: []string{`bid-31`}, + price: 10, + nDealBids: 0, + }, + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-41`}}, + }, + bidIDs: []string{`bid-41`}, + price: 15, + nDealBids: 0, + }, + }, + }, + want: &types.AdPodBid{ + Bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-21`}}, + }, + Cat: []string{}, + ADomain: []string{}, + Price: 20, + }, + }, + { + name: `MultiResponse-AllDealBids-SameCount`, + fields: fields{ + request: &openrtb2.BidRequest{ID: `req-1`, Imp: []openrtb2.Imp{{ID: `imp-1`}}}, + impIndex: 0, + }, + args: args{ + results: []*highestCombination{ + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-11`}}, + }, + bidIDs: []string{`bid-11`}, + price: 10, + nDealBids: 1, + }, + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-21`}}, + }, + bidIDs: []string{`bid-21`}, + price: 20, + nDealBids: 1, + }, + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-31`}}, + }, + bidIDs: []string{`bid-31`}, + price: 10, + nDealBids: 1, + }, + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-41`}}, + }, + bidIDs: []string{`bid-41`}, + price: 15, + nDealBids: 1, + }, + }, + }, + want: &types.AdPodBid{ + Bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-21`}}, + }, + Cat: []string{}, + ADomain: []string{}, + Price: 20, + }, + }, + { + name: `MultiResponse-AllDealBids-DifferentCount`, + fields: fields{ + request: &openrtb2.BidRequest{ID: `req-1`, Imp: []openrtb2.Imp{{ID: `imp-1`}}}, + impIndex: 0, + }, + args: args{ + results: []*highestCombination{ + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-11`}}, + }, + bidIDs: []string{`bid-11`}, + price: 10, + nDealBids: 2, + }, + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-21`}}, + }, + bidIDs: []string{`bid-21`}, + price: 20, + nDealBids: 1, + }, + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-31`}}, + }, + bidIDs: []string{`bid-31`}, + price: 10, + nDealBids: 3, + }, + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-41`}}, + }, + bidIDs: []string{`bid-41`}, + price: 15, + nDealBids: 2, + }, + }, + }, + want: &types.AdPodBid{ + Bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-31`}}, + }, + Cat: []string{}, + ADomain: []string{}, + Price: 10, + }, + }, + { + name: `MultiResponse-Mixed-DealandNonDealBids`, + fields: fields{ + request: &openrtb2.BidRequest{ID: `req-1`, Imp: []openrtb2.Imp{{ID: `imp-1`}}}, + impIndex: 0, + }, + args: args{ + results: []*highestCombination{ + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-11`}}, + }, + bidIDs: []string{`bid-11`}, + price: 10, + nDealBids: 2, + }, + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-21`}}, + }, + bidIDs: []string{`bid-21`}, + price: 20, + nDealBids: 0, + }, + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-31`}}, + }, + bidIDs: []string{`bid-31`}, + price: 10, + nDealBids: 3, + }, + { + bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-41`}}, + }, + bidIDs: []string{`bid-41`}, + price: 15, + nDealBids: 0, + }, + }, + }, + want: &types.AdPodBid{ + Bids: []*types.Bid{ + {Bid: &openrtb2.Bid{ID: `bid-31`}}, + }, + Cat: []string{}, + ADomain: []string{}, + Price: 10, + }, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &AdPodGenerator{ + request: tt.fields.request, + impIndex: tt.fields.impIndex, + } + got := o.getMaxAdPodBid(tt.args.results) + if nil != got { + sort.Strings(got.ADomain) + sort.Strings(got.Cat) + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/endpoints/openrtb2/ctv/response/adpod_generator_test.go.bak b/endpoints/openrtb2/ctv/response/adpod_generator_test.go.bak new file mode 100644 index 00000000000..fa3590a1e16 --- /dev/null +++ b/endpoints/openrtb2/ctv/response/adpod_generator_test.go.bak @@ -0,0 +1,124 @@ +package ctv + +/* +func (o *AdPodGenerator) getUniqueBids(responseCh chan<- *highestCombination, durationSequence []int) { + data := [][]*Bid{} + combinations := []int{} + + for index, duration := range durationSequence { + data[index] = o.buckets[duration][:] + } + + responseCh <- findUniqueCombinations(data[:], *o.adpod.IABCategoryExclusionPercent, *o.adpod.AdvertiserExclusionPercent) +} +*/ + +// Todo: this function is still returning (B3 B4) and (B4 B3), need to work on it +// func findUniqueCombinations(arr [][]Bid) ([][]Bid) { +func findUniqueCombinationsOld(arr [][]*Bid, maxCategoryScore, maxDomainScore int) *highestCombination { + // number of arrays + n := len(arr) + // to keep track of next element in each of the n arrays + indices := make([]int, n) + // indices is initialized with all zeros + + // maintain highest price combination + var ehc *highestCombination + var rc FilterReasonCode + next := n - 1 + hc := &highestCombination{price: 0} + for true { + + row := []*Bid{} + // We do not want the same bid to appear twice in a combination + bidsInRow := make(map[string]bool) + good := true + + for i := 0; i < n; i++ { + if _, present := bidsInRow[arr[i][indices[i]].ID]; !present { + row = append(row, arr[i][indices[i]]) + bidsInRow[arr[i][indices[i]].ID] = true + } else { + good = false + break + } + } + + if good { + // output = append(output, row) + // give a call for exclusion checking here only + ehc, next, rc = evaluateOld(row, maxCategoryScore, maxDomainScore) + if nil != ehc { + if nil == hc || hc.price < ehc.price { + hc = ehc + } else { + // if you see current combination price lower than the highest one then break the loop + return hc + } + } else { + arr[next][indices[next]].FilterReasonCode = rc + } + } + + // find the rightmost array that has more + // elements left after the current element + // in that array + if -1 == next { + next = n - 1 + } + + for next >= 0 && (indices[next]+1 >= len(arr[next])) { + next-- + } + + // no such array is found so no more combinations left + if next < 0 { + // return output + return nil + } + + // if found move to next element in that array + indices[next]++ + + // for all arrays to the right of this + // array current index again points to + // first element + for i := next + 1; i < n; i++ { + indices[i] = 0 + } + } + // return output + return hc +} + +func evaluateOld(bids []*Bid, maxCategoryScore, maxDomainScore int) (*highestCombination, int, FilterReasonCode) { + + hbc := &highestCombination{ + bids: bids, + price: 0, + categoryScore: make(map[string]int), + domainScore: make(map[string]int), + } + + totalBids := len(bids) + + for index, bid := range bids { + hbc.price = hbc.price + bid.Price + + for _, cat := range bid.Cat { + hbc.categoryScore[cat]++ + if (hbc.categoryScore[cat] * 100 / totalBids) > maxCategoryScore { + return nil, index, CTVRCCategoryExclusion + } + } + + for _, domain := range bid.ADomain { + hbc.domainScore[domain]++ + if (hbc.domainScore[domain] * 100 / totalBids) > maxDomainScore { + return nil, index, CTVRCDomainExclusion + } + } + } + + return hbc, -1, CTVRCWinningBid +} diff --git a/endpoints/openrtb2/ctv/types/adpod_types.go b/endpoints/openrtb2/ctv/types/adpod_types.go new file mode 100644 index 00000000000..3cd0398ba7a --- /dev/null +++ b/endpoints/openrtb2/ctv/types/adpod_types.go @@ -0,0 +1,63 @@ +package types + +import ( + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/constant" + "github.com/prebid/prebid-server/openrtb_ext" +) + +//Bid openrtb bid object with extra parameters +type Bid struct { + *openrtb2.Bid + openrtb_ext.ExtBid + Duration int + Status constant.BidStatus + DealTierSatisfied bool +} + +//ExtCTVBidResponse object for ctv bid resposne object +type ExtCTVBidResponse struct { + openrtb_ext.ExtBidResponse + AdPod *BidResponseAdPodExt `json:"adpod,omitempty"` +} + +//BidResponseAdPodExt object for ctv bidresponse adpod object +type BidResponseAdPodExt struct { + Response openrtb2.BidResponse `json:"bidresponse,omitempty"` + Config map[string]*ImpData `json:"config,omitempty"` +} + +//AdPodBid combination contains ImpBid +type AdPodBid struct { + Bids []*Bid + Price float64 + Cat []string + ADomain []string + OriginalImpID string + SeatName string +} + +//AdPodBids combination contains ImpBid +type AdPodBids []*AdPodBid + +//BidsBuckets bids bucket +type BidsBuckets map[int][]*Bid + +//ImpAdPodConfig configuration for creating ads in adpod +type ImpAdPodConfig struct { + ImpID string `json:"id,omitempty"` + SequenceNumber int8 `json:"seq,omitempty"` + MinDuration int64 `json:"minduration,omitempty"` + MaxDuration int64 `json:"maxduration,omitempty"` +} + +//ImpData example +type ImpData struct { + //AdPodGenerator + ImpID string `json:"-"` + Bid *AdPodBid `json:"-"` + VideoExt *openrtb_ext.ExtVideoAdPod `json:"vidext,omitempty"` + Config []*ImpAdPodConfig `json:"imp,omitempty"` + BlockedVASTTags map[string][]string `json:"blockedtags,omitempty"` + Error *openrtb_ext.ExtBidderMessage `json:"ec,omitempty"` +} diff --git a/endpoints/openrtb2/ctv/util/util.go b/endpoints/openrtb2/ctv/util/util.go new file mode 100644 index 00000000000..63b9e0009f1 --- /dev/null +++ b/endpoints/openrtb2/ctv/util/util.go @@ -0,0 +1,141 @@ +package util + +import ( + "encoding/json" + "fmt" + "math" + "sort" + "strconv" + "strings" + "time" + + "github.com/buger/jsonparser" + "github.com/golang/glog" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/constant" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/types" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +var ( + //prebid_ctv_errors + UnableToGenerateImpressionsError = &errortypes.AdpodPrefiltering{Message: `prebid_ctv unable to generate impressions for adpod`} + + //prebid_ctv_warnings + DurationMismatchWarning = &openrtb_ext.ExtBidderMessage{Code: errortypes.AdpodPostFilteringWarningCode, Message: `prebid_ctv all bids filtered while matching lineitem duration`} + UnableToGenerateAdPodWarning = &openrtb_ext.ExtBidderMessage{Code: errortypes.AdpodPostFilteringWarningCode, Message: `prebid_ctv unable to generate adpod from bids combinations`} +) + +func GetDurationWiseBidsBucket(bids []*types.Bid) types.BidsBuckets { + result := types.BidsBuckets{} + + for i, bid := range bids { + if constant.StatusOK == bid.Status { + result[bid.Duration] = append(result[bid.Duration], bids[i]) + } + } + + for k, v := range result { + //sort.Slice(v[:], func(i, j int) bool { return v[i].Price > v[j].Price }) + sortBids(v[:]) + result[k] = v + } + + return result +} + +func sortBids(bids []*types.Bid) { + sort.Slice(bids, func(i, j int) bool { + if bids[i].DealTierSatisfied == bids[j].DealTierSatisfied { + return bids[i].Price > bids[j].Price + } + return bids[i].DealTierSatisfied + }) +} + +// GetDealTierSatisfied ... +func GetDealTierSatisfied(ext *openrtb_ext.ExtBid) bool { + return ext != nil && ext.Prebid != nil && ext.Prebid.DealTierSatisfied +} + +func DecodeImpressionID(id string) (string, int) { + index := strings.LastIndex(id, constant.CTVImpressionIDSeparator) + if index == -1 { + return id, 0 + } + + sequence, err := strconv.Atoi(id[index+1:]) + if nil != err || 0 == sequence { + return id, 0 + } + + return id[:index], sequence +} + +func GetCTVImpressionID(impID string, seqNo int) string { + return fmt.Sprintf(constant.CTVImpressionIDFormat, impID, seqNo) +} + +func GetUniqueBidID(bidID string, id int) string { + return fmt.Sprintf(constant.CTVUniqueBidIDFormat, id, bidID) +} + +var Logf = func(msg string, args ...interface{}) { + if glog.V(3) { + glog.Infof(msg, args...) + } + //fmt.Printf(msg+"\n", args...) +} + +func JLogf(msg string, obj interface{}) { + if glog.V(3) { + data, _ := json.Marshal(obj) + glog.Infof("[OPENWRAP] %v:%v", msg, string(data)) + } +} + +func TimeTrack(start time.Time, name string) { + elapsed := time.Since(start) + Logf("[TIMETRACK] %s took %s", name, elapsed) + //eg: defer TimeTrack(time.Now(), "factorial") +} + +// GetTargeting returns the value of targeting key associated with bidder +// it is expected that bid.Ext contains prebid.targeting map +// if value not present or any error occured empty value will be returned +// along with error. +func GetTargeting(key openrtb_ext.TargetingKey, bidder openrtb_ext.BidderName, bid openrtb2.Bid) (string, error) { + bidderSpecificKey := key.BidderKey(openrtb_ext.BidderName(bidder), 20) + return jsonparser.GetString(bid.Ext, "prebid", "targeting", bidderSpecificKey) +} + +// GetNearestDuration will return nearest duration value present in ImpAdPodConfig objects +// it will return -1 if it doesn't found any match +func GetNearestDuration(duration int64, config []*types.ImpAdPodConfig) int64 { + tmp := int64(-1) + diff := int64(math.MaxInt64) + for _, c := range config { + tdiff := (c.MaxDuration - duration) + if tdiff == 0 { + tmp = c.MaxDuration + break + } + if tdiff > 0 && tdiff <= diff { + tmp = c.MaxDuration + diff = tdiff + } + } + return tmp +} + +// ErrToBidderMessage will return error message in ExtBidderMessage format +func ErrToBidderMessage(err error) *openrtb_ext.ExtBidderMessage { + if err == nil { + return nil + } + return &openrtb_ext.ExtBidderMessage{ + Code: errortypes.ReadCode(err), + Message: err.Error(), + } +} diff --git a/endpoints/openrtb2/ctv/util/util_test.go b/endpoints/openrtb2/ctv/util/util_test.go new file mode 100644 index 00000000000..2d3e452e38a --- /dev/null +++ b/endpoints/openrtb2/ctv/util/util_test.go @@ -0,0 +1,342 @@ +package util + +import ( + "fmt" + "testing" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/types" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestDecodeImpressionID(t *testing.T) { + type args struct { + id string + } + type want struct { + id string + seq int + } + tests := []struct { + name string + args args + want want + }{ + { + name: "TC1", + args: args{id: "impid"}, + want: want{id: "impid", seq: 0}, + }, + { + name: "TC2", + args: args{id: "impid_1"}, + want: want{id: "impid", seq: 1}, + }, + { + name: "TC1", + args: args{id: "impid_1_2"}, + want: want{id: "impid_1", seq: 2}, + }, + { + name: "TC1", + args: args{id: "impid_1_x"}, + want: want{id: "impid_1_x", seq: 0}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + id, seq := DecodeImpressionID(tt.args.id) + assert.Equal(t, tt.want.id, id) + assert.Equal(t, tt.want.seq, seq) + }) + } +} + +func TestSortByDealPriority(t *testing.T) { + + type testbid struct { + id string + price float64 + isDealBid bool + } + + testcases := []struct { + scenario string + bids []testbid + expectedBidIDOrdering []string + }{ + /* tests based on truth table */ + { + scenario: "all_deal_bids_do_price_based_sort", + bids: []testbid{ + {id: "DB_$5", price: 5.0, isDealBid: true}, // Deal bid with low price + {id: "DB_$10", price: 10.0, isDealBid: true}, // Deal bid with high price + }, + expectedBidIDOrdering: []string{"DB_$10", "DB_$5"}, // sort by price among deal bids + }, + { + scenario: "normal_and_deal_bid_mix_case_1", + bids: []testbid{ + {id: "DB_$15", price: 15.0, isDealBid: true}, // Deal bid with low price + {id: "B_$30", price: 30.0, isDealBid: false}, // Normal bid with high price + }, + expectedBidIDOrdering: []string{"DB_$15", "B_$30"}, // no sort expected. Deal bid is already 1st in order + }, + { + scenario: "normal_and_deal_bid_mix_case_2", // deal bids are not at start position in order + bids: []testbid{ + {id: "B_$30", price: 30.0, isDealBid: false}, // Normal bid with high price + {id: "DB_$15", price: 15.0, isDealBid: true}, // Deal bid with low price + }, + expectedBidIDOrdering: []string{"DB_$15", "B_$30"}, // sort based on deal bid + }, + { + scenario: "all_normal_bids_sort_by_price_case_1", + bids: []testbid{ + {id: "B_$5", price: 5.0, isDealBid: false}, + {id: "B_$10", price: 10.0, isDealBid: false}, + }, + expectedBidIDOrdering: []string{"B_$10", "B_$5"}, // sort by price + }, + { + scenario: "all_normal_bids_sort_by_price_case_2", // already sorted by highest price + bids: []testbid{ + {id: "B_$10", price: 10.0, isDealBid: false}, + {id: "B_$5", price: 5.0, isDealBid: false}, + }, + expectedBidIDOrdering: []string{"B_$10", "B_$5"}, // no sort required as already sorted + }, + /* use cases */ + { + scenario: "deal_bids_with_same_price", + bids: []testbid{ + {id: "DB2_$10", price: 10.0, isDealBid: true}, + {id: "DB1_$10", price: 10.0, isDealBid: true}, + }, + expectedBidIDOrdering: []string{"DB2_$10", "DB1_$10"}, // no sort expected + }, + /* more than 2 Bids testcases */ + { + scenario: "4_bids_with_first_and_last_are_deal_bids", + bids: []testbid{ + {id: "DB_$15", price: 15.0, isDealBid: true}, // deal bid with low CPM than another bid + {id: "B_$40", price: 40.0, isDealBid: false}, // normal bid with highest CPM + {id: "B_$3", price: 3.0, isDealBid: false}, + {id: "DB_$20", price: 20.0, isDealBid: true}, // deal bid with high cpm than another deal bid + }, + expectedBidIDOrdering: []string{"DB_$20", "DB_$15", "B_$40", "B_$3"}, + }, + { + scenario: "deal_bids_and_normal_bids_with_same_price", + bids: []testbid{ + {id: "B1_$7", price: 7.0, isDealBid: false}, + {id: "DB2_$7", price: 7.0, isDealBid: true}, + {id: "B3_$7", price: 7.0, isDealBid: false}, + {id: "DB1_$7", price: 7.0, isDealBid: true}, + {id: "B2_$7", price: 7.0, isDealBid: false}, + }, + expectedBidIDOrdering: []string{"DB2_$7", "DB1_$7", "B1_$7", "B3_$7", "B2_$7"}, // no sort expected + }, + } + + newBid := func(bid testbid) *types.Bid { + return &types.Bid{ + Bid: &openrtb2.Bid{ + ID: bid.id, + Price: bid.price, + //Ext: json.RawMessage(`{"prebid":{ "dealTierSatisfied" : ` + bid.isDealBid + ` }}`), + }, + DealTierSatisfied: bid.isDealBid, + } + } + + for _, test := range testcases { + // if test.scenario != "deal_bids_and_normal_bids_with_same_price" { + // continue + // } + fmt.Println("Scenario : ", test.scenario) + bids := []*types.Bid{} + for _, bid := range test.bids { + bids = append(bids, newBid(bid)) + } + for _, bid := range bids { + fmt.Println(bid.ID, ",", bid.Price, ",", bid.DealTierSatisfied) + } + sortBids(bids[:]) + fmt.Println("After sort") + actual := []string{} + for _, bid := range bids { + fmt.Println(bid.ID, ",", bid.Price, ", ", bid.DealTierSatisfied) + actual = append(actual, bid.ID) + } + assert.Equal(t, test.expectedBidIDOrdering, actual, test.scenario+" failed") + fmt.Println("") + } +} + +func TestGetTargeting(t *testing.T) { + var tests = []struct { + scenario string // Testcase scenario + targeting string + bidder string + key openrtb_ext.TargetingKey + expectValue string + expectError bool + }{ + {"no hb_bidder, expect error", "", "", openrtb_ext.HbCategoryDurationKey, "", true}, + {"hb_bidder present, no key present", `{"x" : "y"}`, "appnexus", openrtb_ext.HbCategoryDurationKey, "", true}, + {"hb_bidder present, required key present (of length 20)", `{"x" : "y", "hb_pb_cat_dur_appnex" : "5.00_sports_10s"}`, "appnexus", openrtb_ext.HbCategoryDurationKey, "5.00_sports_10s", false}, + } + + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + bid := new(openrtb2.Bid) + bid.Ext = []byte(`{"prebid" : { "targeting" : ` + test.targeting + `}}`) + value, err := GetTargeting(test.key, openrtb_ext.BidderName(test.bidder), *bid) + if test.expectError { + assert.NotNil(t, err) + assert.Empty(t, value) + } + assert.Equal(t, test.expectValue, value) + }) + } +} + +func TestGetNearestDuration(t *testing.T) { + type args struct { + duration int64 + config []*types.ImpAdPodConfig + } + tests := []struct { + name string + args args + wantDuration int64 + }{ + // TODO: Add test cases. + { + name: "sorted_array_exact_match", + args: args{ + duration: 20, + config: []*types.ImpAdPodConfig{ + {MaxDuration: 10}, + {MaxDuration: 20}, + {MaxDuration: 30}, + {MaxDuration: 40}, + }, + }, + wantDuration: 20, + }, + { + name: "sorted_array_first_element", + args: args{ + duration: 5, + config: []*types.ImpAdPodConfig{ + {MaxDuration: 10}, + {MaxDuration: 20}, + {MaxDuration: 30}, + {MaxDuration: 40}, + }, + }, + wantDuration: 10, + }, + { + name: "sorted_array_not_found", + args: args{ + duration: 45, + config: []*types.ImpAdPodConfig{ + {MaxDuration: 10}, + {MaxDuration: 20}, + {MaxDuration: 30}, + {MaxDuration: 40}, + }, + }, + wantDuration: -1, + }, + { + name: "unsorted_array_exact_match", + args: args{ + duration: 10, + config: []*types.ImpAdPodConfig{ + {MaxDuration: 40}, + {MaxDuration: 20}, + {MaxDuration: 10}, + {MaxDuration: 30}, + }, + }, + wantDuration: 10, + }, + { + name: "unsorted_array_round_to_minimum", + args: args{ + duration: 5, + config: []*types.ImpAdPodConfig{ + {MaxDuration: 40}, + {MaxDuration: 20}, + {MaxDuration: 10}, + {MaxDuration: 30}, + }, + }, + wantDuration: 10, + }, + { + name: "unsorted_array_invalid", + args: args{ + duration: 45, + config: []*types.ImpAdPodConfig{ + {MaxDuration: 40}, + {MaxDuration: 20}, + {MaxDuration: 10}, + {MaxDuration: 30}, + }, + }, + wantDuration: -1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + duration := GetNearestDuration(tt.args.duration, tt.args.config) + assert.Equal(t, tt.wantDuration, duration) + }) + } +} + +func TestErrToBidderMessage(t *testing.T) { + type args struct { + err error + } + tests := []struct { + name string + args args + want *openrtb_ext.ExtBidderMessage + }{ + { + name: `nil_check`, + args: args{err: nil}, + want: nil, + }, + { + name: `normal_error`, + args: args{err: fmt.Errorf(`normal_error`)}, + want: &openrtb_ext.ExtBidderMessage{ + Code: errortypes.UnknownErrorCode, + Message: `normal_error`, + }, + }, + { + name: `prebid_ctv_error`, + args: args{err: &errortypes.Timeout{Message: `timeout`}}, + want: &openrtb_ext.ExtBidderMessage{ + Code: errortypes.TimeoutErrorCode, + Message: `timeout`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ErrToBidderMessage(tt.args.err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/endpoints/openrtb2/ctv_auction.go b/endpoints/openrtb2/ctv_auction.go new file mode 100644 index 00000000000..f500f5b55f2 --- /dev/null +++ b/endpoints/openrtb2/ctv_auction.go @@ -0,0 +1,1157 @@ +package openrtb2 + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "time" + + "github.com/beevik/etree" + "github.com/buger/jsonparser" + uuid "github.com/gofrs/uuid" + "github.com/golang/glog" + "github.com/julienschmidt/httprouter" + "github.com/mxmCherry/openrtb/v16/openrtb2" + accountService "github.com/prebid/prebid-server/account" + "github.com/prebid/prebid-server/analytics" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/endpoints/events" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/combination" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/constant" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/impressions" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/response" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/types" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/util" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/exchange" + "github.com/prebid/prebid-server/metrics" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/stored_requests" + "github.com/prebid/prebid-server/usersync" + "github.com/prebid/prebid-server/util/iputil" + "github.com/prebid/prebid-server/util/uuidutil" +) + +//CTV Specific Endpoint +type ctvEndpointDeps struct { + endpointDeps + request *openrtb2.BidRequest + reqExt *openrtb_ext.ExtRequestAdPod + impData []*types.ImpData + videoSeats []*openrtb2.SeatBid //stores pure video impression bids + impIndices map[string]int + isAdPodRequest bool + impsExt map[string]map[string]map[string]interface{} + impPartnerBlockedTagIDMap map[string]map[string][]string + + //Prebid Specific + ctx context.Context + labels metrics.Labels +} + +//NewCTVEndpoint new ctv endpoint object +func NewCTVEndpoint( + ex exchange.Exchange, + validator openrtb_ext.BidderParamValidator, + requestsByID stored_requests.Fetcher, + videoFetcher stored_requests.Fetcher, + accounts stored_requests.AccountFetcher, + //categories stored_requests.CategoryFetcher, + cfg *config.Configuration, + met metrics.MetricsEngine, + pbsAnalytics analytics.PBSAnalyticsModule, + disabledBidders map[string]string, + defReqJSON []byte, + bidderMap map[string]openrtb_ext.BidderName) (httprouter.Handle, error) { + + if ex == nil || validator == nil || requestsByID == nil || accounts == nil || cfg == nil || met == nil { + return nil, errors.New("NewCTVEndpoint requires non-nil arguments") + } + defRequest := len(defReqJSON) > 0 + + ipValidator := iputil.PublicNetworkIPValidator{ + IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed, + IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed, + } + + var uuidGenerator uuidutil.UUIDGenerator + return httprouter.Handle((&ctvEndpointDeps{ + endpointDeps: endpointDeps{ + uuidGenerator, + ex, + validator, + requestsByID, + videoFetcher, + accounts, + cfg, + met, + pbsAnalytics, + disabledBidders, + defRequest, + defReqJSON, + bidderMap, + nil, + nil, + ipValidator, + nil, + }, + }).CTVAuctionEndpoint), nil +} + +func (deps *ctvEndpointDeps) CTVAuctionEndpoint(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + defer util.TimeTrack(time.Now(), "CTVAuctionEndpoint") + + var reqWrapper *openrtb_ext.RequestWrapper + var request *openrtb2.BidRequest + var response *openrtb2.BidResponse + var err error + var errL []error + + ao := analytics.AuctionObject{ + Status: http.StatusOK, + Errors: make([]error, 0), + } + + // Prebid Server interprets request.tmax to be the maximum amount of time that a caller is willing + // to wait for bids. However, tmax may be defined in the Stored Request data. + // + // If so, then the trip to the backend might use a significant amount of this time. + // We can respect timeouts more accurately if we note the *real* start time, and use it + // to compute the auction timeout. + start := time.Now() + //Prebid Stats + deps.labels = metrics.Labels{ + Source: metrics.DemandUnknown, + RType: metrics.ReqTypeVideo, + PubID: metrics.PublisherUnknown, + CookieFlag: metrics.CookieFlagUnknown, + RequestStatus: metrics.RequestStatusOK, + } + defer func() { + deps.metricsEngine.RecordRequest(deps.labels) + deps.metricsEngine.RecordRequestTime(deps.labels, time.Since(start)) + deps.analytics.LogAuctionObject(&ao) + }() + + //Parse ORTB Request and do Standard Validation + reqWrapper, _, _, _, _, errL = deps.parseRequest(r) + if errortypes.ContainsFatalError(errL) && writeError(errL, w, &deps.labels) { + return + } + request = reqWrapper.BidRequest + + util.JLogf("Original BidRequest", request) //TODO: REMOVE LOG + + //init + deps.init(request) + + //Set Default Values + deps.setDefaultValues() + util.JLogf("Extensions Request Extension", deps.reqExt) + util.JLogf("Extensions ImpData", deps.impData) + + //Validate CTV BidRequest + if err := deps.validateBidRequest(); err != nil { + errL = append(errL, err...) + writeError(errL, w, &deps.labels) + return + } + + if deps.isAdPodRequest { + //Create New BidRequest + request = deps.createBidRequest(request) + util.JLogf("CTV BidRequest", request) //TODO: REMOVE LOG + } + + //Parsing Cookies and Set Stats + usersyncs := usersync.ParseCookieFromRequest(r, &(deps.cfg.HostCookie)) + if request.App != nil { + deps.labels.Source = metrics.DemandApp + deps.labels.RType = metrics.ReqTypeVideo + deps.labels.PubID = getAccountID(request.App.Publisher) + } else { //request.Site != nil + deps.labels.Source = metrics.DemandWeb + if !usersyncs.HasAnyLiveSyncs() { + deps.labels.CookieFlag = metrics.CookieFlagNo + } else { + deps.labels.CookieFlag = metrics.CookieFlagYes + } + deps.labels.PubID = getAccountID(request.Site.Publisher) + } + + deps.ctx = context.Background() + + // Look up account now that we have resolved the pubID value + account, acctIDErrs := accountService.GetAccount(deps.ctx, deps.cfg, deps.accounts, deps.labels.PubID) + if len(acctIDErrs) > 0 { + errL = append(errL, acctIDErrs...) + writeError(errL, w, &deps.labels) + return + } + + //Setting Timeout for Request + timeout := deps.cfg.AuctionTimeouts.LimitAuctionTimeout(time.Duration(request.TMax) * time.Millisecond) + if timeout > 0 { + var cancel context.CancelFunc + deps.ctx, cancel = context.WithDeadline(deps.ctx, start.Add(timeout)) + defer cancel() + } + + response, err = deps.holdAuction(request, usersyncs, account, start) + + ao.Request = request + ao.Response = response + if err != nil || nil == response { + deps.labels.RequestStatus = metrics.RequestStatusErr + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Critical error while running the auction: %v", err) + glog.Errorf("/openrtb2/video Critical error: %v", err) + ao.Status = http.StatusInternalServerError + ao.Errors = append(ao.Errors, err) + return + } + util.JLogf("BidResponse", response) //TODO: REMOVE LOG + + if deps.isAdPodRequest { + //Validate Bid Response + if err := deps.validateBidResponse(request, response); err != nil { + errL = append(errL, err) + writeError(errL, w, &deps.labels) + return + } + + //Create Impression Bids + deps.getBids(response) + + //Do AdPod Exclusions + bids := deps.doAdPodExclusions() + + //Create Bid Response + response = deps.createBidResponse(response, bids) + util.JLogf("CTV BidResponse", response) //TODO: REMOVE LOG + } + + // Response Generation + enc := json.NewEncoder(w) + enc.SetEscapeHTML(false) + + // Fixes #328 + w.Header().Set("Content-Type", "application/json") + + // If an error happens when encoding the response, there isn't much we can do. + // If we've sent _any_ bytes, then Go would have sent the 200 status code first. + // That status code can't be un-sent... so the best we can do is log the error. + if err := enc.Encode(response); err != nil { + deps.labels.RequestStatus = metrics.RequestStatusNetworkErr + ao.Errors = append(ao.Errors, fmt.Errorf("/openrtb2/video Failed to send response: %v", err)) + } +} + +func (deps *ctvEndpointDeps) holdAuction(request *openrtb2.BidRequest, usersyncs *usersync.Cookie, account *config.Account, startTime time.Time) (*openrtb2.BidResponse, error) { + defer util.TimeTrack(time.Now(), fmt.Sprintf("Tid:%v CTVHoldAuction", deps.request.ID)) + + //Hold OpenRTB Standard Auction + if len(request.Imp) == 0 { + //Dummy Response Object + return &openrtb2.BidResponse{ID: request.ID}, nil + } + + auctionRequest := exchange.AuctionRequest{ + BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: request}, + Account: *account, + UserSyncs: usersyncs, + RequestType: deps.labels.RType, + StartTime: startTime, + LegacyLabels: deps.labels, + PubID: deps.labels.PubID, + } + + return deps.ex.HoldAuction(deps.ctx, auctionRequest, nil) +} + +/********************* BidRequest Processing *********************/ + +func (deps *ctvEndpointDeps) init(req *openrtb2.BidRequest) { + deps.request = req + deps.impData = make([]*types.ImpData, len(req.Imp)) + deps.impIndices = make(map[string]int, len(req.Imp)) + + for i := range req.Imp { + deps.impIndices[req.Imp[i].ID] = i + deps.impData[i] = &types.ImpData{} + } +} + +func (deps *ctvEndpointDeps) readVideoAdPodExt() (err []error) { + for index, imp := range deps.request.Imp { + if nil != imp.Video { + vidExt := openrtb_ext.ExtVideoAdPod{} + if len(imp.Video.Ext) > 0 { + errL := json.Unmarshal(imp.Video.Ext, &vidExt) + if nil != err { + err = append(err, errL) + continue + } + + imp.Video.Ext = jsonparser.Delete(imp.Video.Ext, constant.CTVAdpod) + imp.Video.Ext = jsonparser.Delete(imp.Video.Ext, constant.CTVOffset) + if string(imp.Video.Ext) == `{}` { + imp.Video.Ext = nil + } + } + + if nil == vidExt.AdPod { + if nil == deps.reqExt { + continue + } + vidExt.AdPod = &openrtb_ext.VideoAdPod{} + } + + //Use Request Level Parameters + if nil != deps.reqExt { + vidExt.AdPod.Merge(&deps.reqExt.VideoAdPod) + } + + //Set Default Values + vidExt.SetDefaultValue() + vidExt.AdPod.SetDefaultAdDurations(imp.Video.MinDuration, imp.Video.MaxDuration) + + deps.impData[index].VideoExt = &vidExt + } + } + return err +} + +func (deps *ctvEndpointDeps) readRequestExtension() (err []error) { + if len(deps.request.Ext) > 0 { + + //TODO: use jsonparser library for get adpod and remove that key + extAdPod, jsonType, _, errL := jsonparser.Get(deps.request.Ext, constant.CTVAdpod) + + if nil != errL { + //parsing error + if jsonparser.NotExist != jsonType { + //assuming key not present + err = append(err, errL) + return + } + } else { + deps.reqExt = &openrtb_ext.ExtRequestAdPod{} + + if errL := json.Unmarshal(extAdPod, deps.reqExt); nil != errL { + err = append(err, errL) + return + } + + deps.reqExt.SetDefaultValue() + } + } + + return +} + +func (deps *ctvEndpointDeps) readExtensions() (err []error) { + if errL := deps.readRequestExtension(); nil != errL { + err = append(err, errL...) + } + + if errL := deps.readVideoAdPodExt(); nil != errL { + err = append(err, errL...) + } + return err +} + +func (deps *ctvEndpointDeps) setIsAdPodRequest() { + deps.isAdPodRequest = false + for _, data := range deps.impData { + if nil != data.VideoExt && nil != data.VideoExt.AdPod { + deps.isAdPodRequest = true + break + } + } +} + +//setDefaultValues will set adpod and other default values +func (deps *ctvEndpointDeps) setDefaultValues() { + //read and set extension values + deps.readExtensions() + + //set request is adpod request or normal request + deps.setIsAdPodRequest() + + if deps.isAdPodRequest { + deps.readImpExtensionsAndTags() + } +} + +//validateBidRequest will validate AdPod specific mandatory Parameters and returns error +func (deps *ctvEndpointDeps) validateBidRequest() (err []error) { + //validating video extension adpod configurations + if nil != deps.reqExt { + err = deps.reqExt.Validate() + } + + for index, imp := range deps.request.Imp { + if nil != imp.Video && nil != deps.impData[index].VideoExt { + ext := deps.impData[index].VideoExt + if errL := ext.Validate(); nil != errL { + err = append(err, errL...) + } + + if nil != ext.AdPod { + if errL := ext.AdPod.ValidateAdPodDurations(imp.Video.MinDuration, imp.Video.MaxDuration, imp.Video.MaxExtended); nil != errL { + err = append(err, errL...) + } + } + } + + } + return +} + +//readImpExtensionsAndTags will read the impression extensions +func (deps *ctvEndpointDeps) readImpExtensionsAndTags() (errs []error) { + deps.impsExt = make(map[string]map[string]map[string]interface{}) + deps.impPartnerBlockedTagIDMap = make(map[string]map[string][]string) //Initially this will have all tags, eligible tags will be filtered in filterImpsVastTagsByDuration + + for _, imp := range deps.request.Imp { + var impExt map[string]map[string]interface{} + if err := json.Unmarshal(imp.Ext, &impExt); err != nil { + errs = append(errs, err) + continue + } + + deps.impPartnerBlockedTagIDMap[imp.ID] = make(map[string][]string) + + for partnerName, partnerExt := range impExt { + impVastTags, ok := partnerExt["tags"].([]interface{}) + if !ok { + continue + } + + for _, tag := range impVastTags { + vastTag, ok := tag.(map[string]interface{}) + if !ok { + continue + } + + deps.impPartnerBlockedTagIDMap[imp.ID][partnerName] = append(deps.impPartnerBlockedTagIDMap[imp.ID][partnerName], vastTag["tagid"].(string)) + } + } + + deps.impsExt[imp.ID] = impExt + } + + return errs +} + +/********************* Creating CTV BidRequest *********************/ + +//createBidRequest will return new bid request with all things copy from bid request except impression objects +func (deps *ctvEndpointDeps) createBidRequest(req *openrtb2.BidRequest) *openrtb2.BidRequest { + ctvRequest := *req + + //get configurations for all impressions + deps.getAllAdPodImpsConfigs() + + //createImpressions + ctvRequest.Imp = deps.createImpressions() + + deps.filterImpsVastTagsByDuration(&ctvRequest) + + //TODO: remove adpod extension if not required to send further + return &ctvRequest +} + +//filterImpsVastTagsByDuration checks if a Vast tag should be called for a generated impression based on the duration of tag and impression +func (deps *ctvEndpointDeps) filterImpsVastTagsByDuration(bidReq *openrtb2.BidRequest) { + + for impCount, imp := range bidReq.Imp { + index := strings.LastIndex(imp.ID, "_") + if index == -1 { + continue + } + + originalImpID := imp.ID[:index] + + impExtMap := deps.impsExt[originalImpID] + newImpExtMap := make(map[string]map[string]interface{}) + for k, v := range impExtMap { + newImpExtMap[k] = v + } + + for partnerName, partnerExt := range newImpExtMap { + if partnerExt["tags"] != nil { + impVastTags, ok := partnerExt["tags"].([]interface{}) + if !ok { + continue + } + + var compatibleVasts []interface{} + for _, tag := range impVastTags { + vastTag, ok := tag.(map[string]interface{}) + if !ok { + continue + } + + tagDuration := int(vastTag["dur"].(float64)) + if int(imp.Video.MinDuration) <= tagDuration && tagDuration <= int(imp.Video.MaxDuration) { + compatibleVasts = append(compatibleVasts, tag) + + deps.impPartnerBlockedTagIDMap[originalImpID][partnerName] = remove(deps.impPartnerBlockedTagIDMap[originalImpID][partnerName], vastTag["tagid"].(string)) + if len(deps.impPartnerBlockedTagIDMap[originalImpID][partnerName]) == 0 { + delete(deps.impPartnerBlockedTagIDMap[originalImpID], partnerName) + } + } + } + + if len(compatibleVasts) < 1 { + delete(newImpExtMap, partnerName) + } else { + newImpExtMap[partnerName] = map[string]interface{}{ + "tags": compatibleVasts, + } + } + + bExt, err := json.Marshal(newImpExtMap) + if err != nil { + continue + } + imp.Ext = bExt + } + } + bidReq.Imp[impCount] = imp + } + + for impID, blockedTags := range deps.impPartnerBlockedTagIDMap { + for _, datum := range deps.impData { + if datum.ImpID == impID { + datum.BlockedVASTTags = blockedTags + break + } + } + } +} + +func remove(slice []string, item string) []string { + index := -1 + for i := range slice { + if slice[i] == item { + index = i + break + } + } + + if index == -1 { + return slice + } + + return append(slice[:index], slice[index+1:]...) +} + +//getAllAdPodImpsConfigs will return all impression adpod configurations +func (deps *ctvEndpointDeps) getAllAdPodImpsConfigs() { + for index, imp := range deps.request.Imp { + if nil == imp.Video || nil == deps.impData[index].VideoExt || nil == deps.impData[index].VideoExt.AdPod { + continue + } + deps.impData[index].ImpID = imp.ID + + config, err := deps.getAdPodImpsConfigs(&imp, deps.impData[index].VideoExt.AdPod) + if err != nil { + deps.impData[index].Error = util.ErrToBidderMessage(err) + continue + } + deps.impData[index].Config = config[:] + } +} + +//getAdPodImpsConfigs will return number of impressions configurations within adpod +func (deps *ctvEndpointDeps) getAdPodImpsConfigs(imp *openrtb2.Imp, adpod *openrtb_ext.VideoAdPod) ([]*types.ImpAdPodConfig, error) { + // monitor + start := time.Now() + selectedAlgorithm := impressions.SelectAlgorithm(deps.reqExt) + impGen := impressions.NewImpressions(imp.Video.MinDuration, imp.Video.MaxDuration, deps.reqExt, adpod, selectedAlgorithm) + impRanges := impGen.Get() + labels := metrics.PodLabels{AlgorithmName: impressions.MonitorKey[selectedAlgorithm], NoOfImpressions: new(int)} + + //log number of impressions in stats + *labels.NoOfImpressions = len(impRanges) + deps.metricsEngine.RecordPodImpGenTime(labels, start) + + // check if algorithm has generated impressions + if len(impRanges) == 0 { + return nil, util.UnableToGenerateImpressionsError + } + + config := make([]*types.ImpAdPodConfig, len(impRanges)) + for i, value := range impRanges { + config[i] = &types.ImpAdPodConfig{ + ImpID: util.GetCTVImpressionID(imp.ID, i+1), + MinDuration: value[0], + MaxDuration: value[1], + SequenceNumber: int8(i + 1), /* Must be starting with 1 */ + } + } + return config[:], nil +} + +//createImpressions will create multiple impressions based on adpod configurations +func (deps *ctvEndpointDeps) createImpressions() []openrtb2.Imp { + impCount := 0 + for _, imp := range deps.impData { + if nil == imp.Error { + if len(imp.Config) == 0 { + impCount = impCount + 1 + } else { + impCount = impCount + len(imp.Config) + } + } + } + + count := 0 + imps := make([]openrtb2.Imp, impCount) + for index, imp := range deps.request.Imp { + if nil == deps.impData[index].Error { + adPodConfig := deps.impData[index].Config + if len(adPodConfig) == 0 { + //non adpod request it will be normal video impression + imps[count] = imp + count++ + } else { + //for adpod request it will create new impression based on configurations + for _, config := range adPodConfig { + imps[count] = *(newImpression(&imp, config)) + count++ + } + } + } + } + return imps[:] +} + +//newImpression will clone existing impression object and create video object with ImpAdPodConfig. +func newImpression(imp *openrtb2.Imp, config *types.ImpAdPodConfig) *openrtb2.Imp { + video := *imp.Video + video.MinDuration = config.MinDuration + video.MaxDuration = config.MaxDuration + video.Sequence = config.SequenceNumber + video.MaxExtended = 0 + //TODO: remove video adpod extension if not required + + newImp := *imp + newImp.ID = config.ImpID + //newImp.BidFloor = 0 + newImp.Video = &video + return &newImp +} + +/********************* Prebid BidResponse Processing *********************/ + +//validateBidResponse +func (deps *ctvEndpointDeps) validateBidResponse(req *openrtb2.BidRequest, resp *openrtb2.BidResponse) error { + //remove bids withoug cat and adomain + + return nil +} + +//getBids reads bids from bidresponse object +func (deps *ctvEndpointDeps) getBids(resp *openrtb2.BidResponse) { + var vseat *openrtb2.SeatBid + result := make(map[string]*types.AdPodBid) + + for i := range resp.SeatBid { + seat := resp.SeatBid[i] + vseat = nil + + for j := range seat.Bid { + bid := &seat.Bid[j] + + if len(bid.ID) == 0 { + bidID, err := uuid.NewV4() + if nil != err { + continue + } + bid.ID = bidID.String() + } + + if bid.Price == 0 { + //filter invalid bids + continue + } + + originalImpID, sequenceNumber := deps.getImpressionID(bid.ImpID) + if sequenceNumber < 0 { + continue + } + + value, err := util.GetTargeting(openrtb_ext.HbCategoryDurationKey, openrtb_ext.BidderName(seat.Seat), *bid) + if nil == err { + // ignore error + addTargetingKey(bid, openrtb_ext.HbCategoryDurationKey, value) + } + + value, err = util.GetTargeting(openrtb_ext.HbpbConstantKey, openrtb_ext.BidderName(seat.Seat), *bid) + if nil == err { + // ignore error + addTargetingKey(bid, openrtb_ext.HbpbConstantKey, value) + } + + index := deps.impIndices[originalImpID] + if len(deps.impData[index].Config) == 0 { + //adding pure video bids + if vseat == nil { + vseat = &openrtb2.SeatBid{ + Seat: seat.Seat, + Group: seat.Group, + Ext: seat.Ext, + } + deps.videoSeats = append(deps.videoSeats, vseat) + } + vseat.Bid = append(vseat.Bid, *bid) + } else { + //reading extension, ingorning parsing error + ext := openrtb_ext.ExtBid{} + if nil != bid.Ext { + json.Unmarshal(bid.Ext, &ext) + } + + //Adding adpod bids + impBids, ok := result[originalImpID] + if !ok { + impBids = &types.AdPodBid{ + OriginalImpID: originalImpID, + SeatName: string(openrtb_ext.BidderOWPrebidCTV), + } + result[originalImpID] = impBids + } + + if deps.cfg.GenerateBidID == false { + //making unique bid.id's per impression + bid.ID = util.GetUniqueBidID(bid.ID, len(impBids.Bids)+1) + } + + //get duration of creative + duration, status := getBidDuration(bid, deps.reqExt, deps.impData[index].Config, + deps.impData[index].Config[sequenceNumber-1].MaxDuration) + + impBids.Bids = append(impBids.Bids, &types.Bid{ + Bid: bid, + ExtBid: ext, + Status: status, + Duration: int(duration), + DealTierSatisfied: util.GetDealTierSatisfied(&ext), + }) + } + } + } + + //Sort Bids by Price + for index, imp := range deps.request.Imp { + impBids, ok := result[imp.ID] + if ok { + //sort bids + sort.Slice(impBids.Bids[:], func(i, j int) bool { return impBids.Bids[i].Price > impBids.Bids[j].Price }) + deps.impData[index].Bid = impBids + } + } +} + +//getImpressionID will return impression id and sequence number +func (deps *ctvEndpointDeps) getImpressionID(id string) (string, int) { + //get original impression id and sequence number + originalImpID, sequenceNumber := util.DecodeImpressionID(id) + + //check originalImpID present in request or not + index, ok := deps.impIndices[originalImpID] + if !ok { + //if not present check impression id present in request or not + _, ok = deps.impIndices[id] + if !ok { + return id, -1 + } + return originalImpID, 0 + } + + if sequenceNumber < 0 || sequenceNumber > len(deps.impData[index].Config) { + return id, -1 + } + + return originalImpID, sequenceNumber +} + +//doAdPodExclusions +func (deps *ctvEndpointDeps) doAdPodExclusions() types.AdPodBids { + defer util.TimeTrack(time.Now(), fmt.Sprintf("Tid:%v doAdPodExclusions", deps.request.ID)) + + result := types.AdPodBids{} + for index := 0; index < len(deps.request.Imp); index++ { + bid := deps.impData[index].Bid + if nil != bid && len(bid.Bids) > 0 { + //TODO: MULTI ADPOD IMPRESSIONS + //duration wise buckets sorted + buckets := util.GetDurationWiseBidsBucket(bid.Bids[:]) + + if len(buckets) == 0 { + deps.impData[index].Error = util.DurationMismatchWarning + continue + } + + //combination generator + comb := combination.NewCombination( + buckets, + uint64(deps.request.Imp[index].Video.MinDuration), + uint64(deps.request.Imp[index].Video.MaxDuration), + deps.impData[index].VideoExt.AdPod) + + //adpod generator + adpodGenerator := response.NewAdPodGenerator(deps.request, index, buckets, comb, deps.impData[index].VideoExt.AdPod, deps.metricsEngine) + + adpodBids := adpodGenerator.GetAdPodBids() + if adpodBids == nil { + deps.impData[index].Error = util.UnableToGenerateAdPodWarning + continue + } + + adpodBids.OriginalImpID = bid.OriginalImpID + adpodBids.SeatName = bid.SeatName + result = append(result, adpodBids) + } + } + return result +} + +/********************* Creating CTV BidResponse *********************/ + +//createBidResponse +func (deps *ctvEndpointDeps) createBidResponse(resp *openrtb2.BidResponse, adpods types.AdPodBids) *openrtb2.BidResponse { + defer util.TimeTrack(time.Now(), fmt.Sprintf("Tid:%v createBidResponse", deps.request.ID)) + + bidResp := &openrtb2.BidResponse{ + ID: resp.ID, + Cur: resp.Cur, + CustomData: resp.CustomData, + SeatBid: deps.getBidResponseSeatBids(adpods), + } + + //NOTE: this should be called at last + bidResp.Ext = deps.getBidResponseExt(resp) + return bidResp +} + +func (deps *ctvEndpointDeps) getBidResponseSeatBids(adpods types.AdPodBids) []openrtb2.SeatBid { + seats := []openrtb2.SeatBid{} + + //append pure video request seats + for _, seat := range deps.videoSeats { + seats = append(seats, *seat) + } + + var adpodSeat *openrtb2.SeatBid + for _, adpod := range adpods { + if len(adpod.Bids) == 0 { + continue + } + + bid := deps.getAdPodBid(adpod) + if bid != nil { + if nil == adpodSeat { + adpodSeat = &openrtb2.SeatBid{ + Seat: adpod.SeatName, + } + } + adpodSeat.Bid = append(adpodSeat.Bid, *bid.Bid) + } + } + if nil != adpodSeat { + seats = append(seats, *adpodSeat) + } + return seats[:] +} + +//getBidResponseExt will return extension object +func (deps *ctvEndpointDeps) getBidResponseExt(resp *openrtb2.BidResponse) (data json.RawMessage) { + var err error + + adpodExt := types.BidResponseAdPodExt{ + Response: *resp, + Config: make(map[string]*types.ImpData, len(deps.impData)), + } + + for index, imp := range deps.impData { + if nil != imp.VideoExt && nil != imp.VideoExt.AdPod { + adpodExt.Config[deps.request.Imp[index].ID] = imp + } + + if nil != imp.Bid && len(imp.Bid.Bids) > 0 { + for _, bid := range imp.Bid.Bids { + //update adm + //bid.AdM = constant.VASTDefaultTag + + //add duration value + raw, err := jsonparser.Set(bid.Ext, []byte(strconv.Itoa(int(bid.Duration))), "prebid", "video", "duration") + if nil == err { + bid.Ext = raw + } + + //add bid filter reason value + raw, err = jsonparser.Set(bid.Ext, []byte(strconv.Itoa(bid.Status)), "adpod", "aprc") + if nil == err { + bid.Ext = raw + } + } + } + } + + //Remove extension parameter + adpodExt.Response.Ext = nil + + if nil == resp.Ext { + bidResponseExt := &types.ExtCTVBidResponse{ + AdPod: &adpodExt, + } + + data, err = json.Marshal(bidResponseExt) + if err != nil { + glog.Errorf("JSON Marshal Error: %v", err.Error()) + return nil + } + } else { + data, err = json.Marshal(adpodExt) + if err != nil { + glog.Errorf("JSON Marshal Error: %v", err.Error()) + return nil + } + + data, err = jsonparser.Set(resp.Ext, data, constant.CTVAdpod) + if err != nil { + glog.Errorf("JSONParser Set Error: %v", err.Error()) + return nil + } + } + + return data[:] +} + +//getAdPodBid +func (deps *ctvEndpointDeps) getAdPodBid(adpod *types.AdPodBid) *types.Bid { + bid := types.Bid{ + Bid: &openrtb2.Bid{}, + } + + //TODO: Write single for loop to get all details + bidID, err := uuid.NewV4() + if nil == err { + bid.ID = bidID.String() + } else { + bid.ID = adpod.Bids[0].ID + } + + bid.ImpID = adpod.OriginalImpID + bid.Price = adpod.Price + bid.ADomain = adpod.ADomain[:] + bid.Cat = adpod.Cat[:] + bid.AdM = *getAdPodBidCreative(deps.request.Imp[deps.impIndices[adpod.OriginalImpID]].Video, adpod, deps.cfg.GenerateBidID) + bid.Ext = getAdPodBidExtension(adpod) + return &bid +} + +//getAdPodBidCreative get commulative adpod bid details +func getAdPodBidCreative(video *openrtb2.Video, adpod *types.AdPodBid, generatedBidID bool) *string { + doc := etree.NewDocument() + vast := doc.CreateElement(constant.VASTElement) + sequenceNumber := 1 + var version float64 = 2.0 + + for _, bid := range adpod.Bids { + var newAd *etree.Element + + if strings.HasPrefix(bid.AdM, constant.HTTPPrefix) { + newAd = etree.NewElement(constant.VASTAdElement) + wrapper := newAd.CreateElement(constant.VASTWrapperElement) + vastAdTagURI := wrapper.CreateElement(constant.VASTAdTagURIElement) + vastAdTagURI.CreateCharData(bid.AdM) + } else { + adDoc := etree.NewDocument() + if err := adDoc.ReadFromString(bid.AdM); err != nil { + continue + } + + if generatedBidID == false { + // adjust bidid in video event trackers and update + adjustBidIDInVideoEventTrackers(adDoc, bid.Bid) + adm, err := adDoc.WriteToString() + if nil != err { + util.JLogf("ERROR, %v", err.Error()) + } else { + bid.AdM = adm + } + } + + vastTag := adDoc.SelectElement(constant.VASTElement) + + //Get Actual VAST Version + bidVASTVersion, _ := strconv.ParseFloat(vastTag.SelectAttrValue(constant.VASTVersionAttribute, constant.VASTDefaultVersionStr), 64) + version = math.Max(version, bidVASTVersion) + + ads := vastTag.SelectElements(constant.VASTAdElement) + if len(ads) > 0 { + newAd = ads[0].Copy() + } + } + + if nil != newAd { + //creative.AdId attribute needs to be updated + newAd.CreateAttr(constant.VASTSequenceAttribute, fmt.Sprint(sequenceNumber)) + vast.AddChild(newAd) + sequenceNumber++ + } + } + + if int(version) > len(constant.VASTVersionsStr) { + version = constant.VASTMaxVersion + } + + vast.CreateAttr(constant.VASTVersionAttribute, constant.VASTVersionsStr[int(version)]) + bidAdM, err := doc.WriteToString() + if nil != err { + fmt.Printf("ERROR, %v", err.Error()) + return nil + } + return &bidAdM +} + +//getAdPodBidExtension get commulative adpod bid details +func getAdPodBidExtension(adpod *types.AdPodBid) json.RawMessage { + bidExt := &openrtb_ext.ExtOWBid{ + ExtBid: openrtb_ext.ExtBid{ + Prebid: &openrtb_ext.ExtBidPrebid{ + Type: openrtb_ext.BidTypeVideo, + Video: &openrtb_ext.ExtBidPrebidVideo{}, + }, + }, + AdPod: &openrtb_ext.BidAdPodExt{ + RefBids: make([]string, len(adpod.Bids)), + }, + } + + for i, bid := range adpod.Bids { + //get unique bid id + bidID := bid.ID + if bid.ExtBid.Prebid != nil && bid.ExtBid.Prebid.BidId != "" { + bidID = bid.ExtBid.Prebid.BidId + } + + //adding bid id in adpod.refbids + bidExt.AdPod.RefBids[i] = bidID + + //updating exact duration of adpod creative + bidExt.Prebid.Video.Duration += int(bid.Duration) + + //setting bid status as winning bid + bid.Status = constant.StatusWinningBid + } + rawExt, _ := json.Marshal(bidExt) + return rawExt +} + +//getDurationBasedOnDurationMatchingPolicy will return duration based on durationmatching policy +func getDurationBasedOnDurationMatchingPolicy(duration int64, policy openrtb_ext.OWVideoLengthMatchingPolicy, config []*types.ImpAdPodConfig) (int64, constant.BidStatus) { + switch policy { + case openrtb_ext.OWExactVideoLengthsMatching: + tmp := util.GetNearestDuration(duration, config) + if tmp != duration { + return duration, constant.StatusDurationMismatch + } + //its and valid duration return it with StatusOK + + case openrtb_ext.OWRoundupVideoLengthMatching: + tmp := util.GetNearestDuration(duration, config) + if tmp == -1 { + return duration, constant.StatusDurationMismatch + } + //update duration with nearest one duration + duration = tmp + //its and valid duration return it with StatusOK + } + + return duration, constant.StatusOK +} + +/* +getBidDuration determines the duration of video ad from given bid. +it will try to get the actual ad duration returned by the bidder using prebid.video.duration +if prebid.video.duration not present then uses defaultDuration passed as an argument +if video lengths matching policy is present for request then it will validate and update duration based on policy +*/ +func getBidDuration(bid *openrtb2.Bid, reqExt *openrtb_ext.ExtRequestAdPod, config []*types.ImpAdPodConfig, defaultDuration int64) (int64, constant.BidStatus) { + + // C1: Read it from bid.ext.prebid.video.duration field + duration, err := jsonparser.GetInt(bid.Ext, "prebid", "video", "duration") + if nil != err || duration <= 0 { + // incase if duration is not present use impression duration directly as it is + return defaultDuration, constant.StatusOK + } + + // C2: Based on video lengths matching policy validate and return duration + if nil != reqExt && len(reqExt.VideoLengthMatching) > 0 { + return getDurationBasedOnDurationMatchingPolicy(duration, reqExt.VideoLengthMatching, config) + } + + //default return duration which is present in bid.ext.prebid.vide.duration field + return duration, constant.StatusOK +} + +func addTargetingKey(bid *openrtb2.Bid, key openrtb_ext.TargetingKey, value string) error { + if nil == bid { + return errors.New("Invalid bid") + } + + raw, err := jsonparser.Set(bid.Ext, []byte(strconv.Quote(value)), "prebid", "targeting", string(key)) + if nil == err { + bid.Ext = raw + } + return err +} + +func adjustBidIDInVideoEventTrackers(doc *etree.Document, bid *openrtb2.Bid) { + // adjusment: update bid.id with ctv module generated bid.id + creatives := events.FindCreatives(doc) + for _, creative := range creatives { + trackingEvents := creative.FindElements("TrackingEvents/Tracking") + if nil != trackingEvents { + // update bidid= value with ctv generated bid id for this bid + for _, trackingEvent := range trackingEvents { + u, e := url.Parse(trackingEvent.Text()) + if nil == e { + values, e := url.ParseQuery(u.RawQuery) + // only do replacment if operId=8 + if nil == e && nil != values["bidid"] && nil != values["operId"] && values["operId"][0] == "8" { + values.Set("bidid", bid.ID) + } else { + continue + } + + //OTT-183: Fix + if nil != values["operId"] && values["operId"][0] == "8" { + operID := values.Get("operId") + values.Del("operId") + values.Add("_operId", operID) // _ (underscore) will keep it as first key + } + + u.RawQuery = values.Encode() // encode sorts query params by key. _ must be first (assuing no other query param with _) + // replace _operId with operId + u.RawQuery = strings.ReplaceAll(u.RawQuery, "_operId", "operId") + trackingEvent.SetText(u.String()) + } + } + } + } +} diff --git a/endpoints/openrtb2/ctv_auction_test.go b/endpoints/openrtb2/ctv_auction_test.go new file mode 100644 index 00000000000..bf463ed3818 --- /dev/null +++ b/endpoints/openrtb2/ctv_auction_test.go @@ -0,0 +1,569 @@ +package openrtb2 + +import ( + "encoding/json" + "testing" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/constant" + "github.com/prebid/prebid-server/endpoints/openrtb2/ctv/types" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestAddTargetingKeys(t *testing.T) { + var tests = []struct { + scenario string // Testcase scenario + key string + value string + bidExt string + expect map[string]string + }{ + {scenario: "key_not_exists", key: "hb_pb_cat_dur", value: "some_value", bidExt: `{"prebid":{"targeting":{}}}`, expect: map[string]string{"hb_pb_cat_dur": "some_value"}}, + {scenario: "key_already_exists", key: "hb_pb_cat_dur", value: "new_value", bidExt: `{"prebid":{"targeting":{"hb_pb_cat_dur":"old_value"}}}`, expect: map[string]string{"hb_pb_cat_dur": "new_value"}}, + } + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + bid := new(openrtb2.Bid) + bid.Ext = []byte(test.bidExt) + key := openrtb_ext.TargetingKey(test.key) + assert.Nil(t, addTargetingKey(bid, key, test.value)) + extBid := openrtb_ext.ExtBid{} + json.Unmarshal(bid.Ext, &extBid) + assert.Equal(t, test.expect, extBid.Prebid.Targeting) + }) + } + assert.Equal(t, "Invalid bid", addTargetingKey(nil, openrtb_ext.HbCategoryDurationKey, "some value").Error()) +} + +func TestFilterImpsVastTagsByDuration(t *testing.T) { + type inputParams struct { + request *openrtb2.BidRequest + generatedRequest *openrtb2.BidRequest + impData []*types.ImpData + } + + type output struct { + reqs openrtb2.BidRequest + blockedTags []map[string][]string + } + + tt := []struct { + testName string + input inputParams + expectedOutput output + }{ + { + testName: "test_single_impression_single_vast_partner_with_no_excluded_tags", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 35}}, + }, + }, + impData: []*types.ImpData{}, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 35}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{}, + }, + }, + { + testName: "test_single_impression_single_vast_partner_with_excluded_tags", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + }, + }, + impData: []*types.ImpData{ + {ImpID: "imp1"}, + }, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{ + {"openx_vast_bidder": []string{"openx_35"}}, + }, + }, + }, + { + testName: "test_single_impression_multiple_vast_partners_no_exclusions", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":15,"tagid":"spotx_15"},{"dur":25,"tagid":"spotx_25"},{"dur":30,"tagid":"spotx_30"}]},"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + }, + }, + impData: []*types.ImpData{}, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]},"spotx_vast_bidder":{"tags":[{"dur":15,"tagid":"spotx_15"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]},"spotx_vast_bidder":{"tags":[{"dur":25,"tagid":"spotx_25"},{"dur":30,"tagid":"spotx_30"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{}, + }, + }, + { + testName: "test_single_impression_multiple_vast_partners_with_exclusions", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":15,"tagid":"spotx_15"},{"dur":25,"tagid":"spotx_25"},{"dur":35,"tagid":"spotx_35"}]},"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":40,"tagid":"openx_40"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + }, + }, + impData: []*types.ImpData{ + {ImpID: "imp1"}, + }, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":15,"tagid":"spotx_15"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]},"spotx_vast_bidder":{"tags":[{"dur":25,"tagid":"spotx_25"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{ + {"openx_vast_bidder": []string{"openx_35", "openx_40"}, "spotx_vast_bidder": []string{"spotx_35"}}, + }, + }, + }, + { + testName: "test_multi_impression_multi_partner_no_exclusions", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp2", Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":30,"tagid":"spotx_30"},{"dur":40,"tagid":"spotx_40"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + {ID: "imp2_1", Video: &openrtb2.Video{MinDuration: 5, MaxDuration: 30}}, + }, + }, + impData: nil, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]}}`)}, + {ID: "imp2_1", Video: &openrtb2.Video{MinDuration: 5, MaxDuration: 30}, Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":30,"tagid":"spotx_30"}]}}`)}, + }, + }, + blockedTags: nil, + }, + }, + { + testName: "test_multi_impression_multi_partner_with_exclusions", + input: inputParams{ + request: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1", Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":35,"tagid":"openx_35"},{"dur":25,"tagid":"openx_25"},{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp2", Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":30,"tagid":"spotx_30"},{"dur":40,"tagid":"spotx_40"}]}}`)}, + }, + }, + generatedRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}}, + {ID: "imp2_1", Video: &openrtb2.Video{MinDuration: 5, MaxDuration: 30}}, + }, + }, + impData: []*types.ImpData{ + {ImpID: "imp1"}, + {ImpID: "imp2"}, + }, + }, + expectedOutput: output{ + reqs: openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + {ID: "imp1_1", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 10}, Ext: []byte(`{}`)}, + {ID: "imp1_2", Video: &openrtb2.Video{MinDuration: 10, MaxDuration: 20}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":20,"tagid":"openx_20"}]}}`)}, + {ID: "imp1_3", Video: &openrtb2.Video{MinDuration: 25, MaxDuration: 30}, Ext: []byte(`{"openx_vast_bidder":{"tags":[{"dur":25,"tagid":"openx_25"}]}}`)}, + {ID: "imp2_1", Video: &openrtb2.Video{MinDuration: 5, MaxDuration: 30}, Ext: []byte(`{"spotx_vast_bidder":{"tags":[{"dur":30,"tagid":"spotx_30"}]}}`)}, + }, + }, + blockedTags: []map[string][]string{ + {"openx_vast_bidder": []string{"openx_35"}}, + {"spotx_vast_bidder": []string{"spotx_40"}}, + }, + }, + }, + } + + for _, tc := range tt { + tc := tc + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + deps := ctvEndpointDeps{request: tc.input.request, impData: tc.input.impData} + deps.readImpExtensionsAndTags() + + outputBids := tc.input.generatedRequest + deps.filterImpsVastTagsByDuration(outputBids) + + assert.Equal(t, tc.expectedOutput.reqs, *outputBids, "Expected length of impressions array was %d but actual was %d", tc.expectedOutput.reqs, outputBids) + + for i, datum := range deps.impData { + assert.Equal(t, tc.expectedOutput.blockedTags[i], datum.BlockedVASTTags, "Expected and actual impData was different") + } + }) + } +} + +func TestGetBidDuration(t *testing.T) { + type args struct { + bid *openrtb2.Bid + reqExt *openrtb_ext.ExtRequestAdPod + config []*types.ImpAdPodConfig + defaultDuration int64 + } + type want struct { + duration int64 + status constant.BidStatus + } + var tests = []struct { + name string + args args + want want + expect int + }{ + { + name: "nil_bid_ext", + args: args{ + bid: &openrtb2.Bid{}, + reqExt: nil, + config: nil, + defaultDuration: 100, + }, + want: want{ + duration: 100, + status: constant.StatusOK, + }, + }, + { + name: "use_default_duration", + args: args{ + bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"tmp":123}`), + }, + reqExt: nil, + config: nil, + defaultDuration: 100, + }, + want: want{ + duration: 100, + status: constant.StatusOK, + }, + }, + { + name: "invalid_duration_in_bid_ext", + args: args{ + bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"prebid":{"video":{"duration":"invalid"}}}`), + }, + reqExt: nil, + config: nil, + defaultDuration: 100, + }, + want: want{ + duration: 100, + status: constant.StatusOK, + }, + }, + { + name: "0sec_duration_in_bid_ext", + args: args{ + bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"prebid":{"video":{"duration":0}}}`), + }, + reqExt: nil, + config: nil, + defaultDuration: 100, + }, + want: want{ + duration: 100, + status: constant.StatusOK, + }, + }, + { + name: "negative_duration_in_bid_ext", + args: args{ + bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"prebid":{"video":{"duration":-30}}}`), + }, + reqExt: nil, + config: nil, + defaultDuration: 100, + }, + want: want{ + duration: 100, + status: constant.StatusOK, + }, + }, + { + name: "30sec_duration_in_bid_ext", + args: args{ + bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"prebid":{"video":{"duration":30}}}`), + }, + reqExt: nil, + config: nil, + defaultDuration: 100, + }, + want: want{ + duration: 30, + status: constant.StatusOK, + }, + }, + { + name: "duration_matching_empty", + args: args{ + bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"prebid":{"video":{"duration":30}}}`), + }, + reqExt: &openrtb_ext.ExtRequestAdPod{ + VideoLengthMatching: "", + }, + config: nil, + defaultDuration: 100, + }, + want: want{ + duration: 30, + status: constant.StatusOK, + }, + }, + { + name: "duration_matching_exact", + args: args{ + bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"prebid":{"video":{"duration":30}}}`), + }, + reqExt: &openrtb_ext.ExtRequestAdPod{ + VideoLengthMatching: openrtb_ext.OWExactVideoLengthsMatching, + }, + config: []*types.ImpAdPodConfig{ + {MaxDuration: 10}, + {MaxDuration: 20}, + {MaxDuration: 30}, + {MaxDuration: 40}, + }, + defaultDuration: 100, + }, + want: want{ + duration: 30, + status: constant.StatusOK, + }, + }, + { + name: "duration_matching_exact_not_present", + args: args{ + bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"prebid":{"video":{"duration":35}}}`), + }, + reqExt: &openrtb_ext.ExtRequestAdPod{ + VideoLengthMatching: openrtb_ext.OWExactVideoLengthsMatching, + }, + config: []*types.ImpAdPodConfig{ + {MaxDuration: 10}, + {MaxDuration: 20}, + {MaxDuration: 30}, + {MaxDuration: 40}, + }, + defaultDuration: 100, + }, + want: want{ + duration: 35, + status: constant.StatusDurationMismatch, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + duration, status := getBidDuration(tt.args.bid, tt.args.reqExt, tt.args.config, tt.args.defaultDuration) + assert.Equal(t, tt.want.duration, duration) + assert.Equal(t, tt.want.status, status) + }) + } +} + +func Test_getDurationBasedOnDurationMatchingPolicy(t *testing.T) { + type args struct { + duration int64 + policy openrtb_ext.OWVideoLengthMatchingPolicy + config []*types.ImpAdPodConfig + } + type want struct { + duration int64 + status constant.BidStatus + } + tests := []struct { + name string + args args + want want + }{ + { + name: "empty_duration_policy", + args: args{ + duration: 10, + policy: "", + config: []*types.ImpAdPodConfig{ + {MaxDuration: 10}, + {MaxDuration: 20}, + {MaxDuration: 30}, + {MaxDuration: 40}, + }, + }, + want: want{ + duration: 10, + status: constant.StatusOK, + }, + }, + { + name: "policy_exact", + args: args{ + duration: 10, + policy: openrtb_ext.OWExactVideoLengthsMatching, + config: []*types.ImpAdPodConfig{ + {MaxDuration: 10}, + {MaxDuration: 20}, + {MaxDuration: 30}, + {MaxDuration: 40}, + }, + }, + want: want{ + duration: 10, + status: constant.StatusOK, + }, + }, + { + name: "policy_exact_didnot_match", + args: args{ + duration: 15, + policy: openrtb_ext.OWExactVideoLengthsMatching, + config: []*types.ImpAdPodConfig{ + {MaxDuration: 10}, + {MaxDuration: 20}, + {MaxDuration: 30}, + {MaxDuration: 40}, + }, + }, + want: want{ + duration: 15, + status: constant.StatusDurationMismatch, + }, + }, + { + name: "policy_roundup_exact", + args: args{ + duration: 20, + policy: openrtb_ext.OWRoundupVideoLengthMatching, + config: []*types.ImpAdPodConfig{ + {MaxDuration: 10}, + {MaxDuration: 20}, + {MaxDuration: 30}, + {MaxDuration: 40}, + }, + }, + want: want{ + duration: 20, + status: constant.StatusOK, + }, + }, + { + name: "policy_roundup", + args: args{ + duration: 25, + policy: openrtb_ext.OWRoundupVideoLengthMatching, + config: []*types.ImpAdPodConfig{ + {MaxDuration: 10}, + {MaxDuration: 20}, + {MaxDuration: 30}, + {MaxDuration: 40}, + }, + }, + want: want{ + duration: 30, + status: constant.StatusOK, + }, + }, + { + name: "policy_roundup_didnot_match", + args: args{ + duration: 45, + policy: openrtb_ext.OWRoundupVideoLengthMatching, + config: []*types.ImpAdPodConfig{ + {MaxDuration: 10}, + {MaxDuration: 20}, + {MaxDuration: 30}, + {MaxDuration: 40}, + }, + }, + want: want{ + duration: 45, + status: constant.StatusDurationMismatch, + }, + }, + + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + duration, status := getDurationBasedOnDurationMatchingPolicy(tt.args.duration, tt.args.policy, tt.args.config) + assert.Equal(t, tt.want.duration, duration) + assert.Equal(t, tt.want.status, status) + }) + } +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/origbidcpmusd.json b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/origbidcpmusd.json new file mode 100644 index 00000000000..411d6db3120 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/origbidcpmusd.json @@ -0,0 +1,76 @@ +{ + "description": "bid.ext.origbidcpmusd with bid.ext.origbidcpm in USD for wrapper logger and wrapper tracker", + "config": { + "assertBidExt": true, + "currencyRates":{ + "USD": { + "MXN": 20.07 + }, + "INR": { + "MXN": 0.25 + } + }, + "mockBidders": [ + {"bidderName": "pubmatic", "currency": "MXN", "price": 5.00} + ] + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "pubmatic": { + "placementId": 12883451 + } + } + } + ], + "cur": ["INR"], + "ext": { + "prebid": { + "aliases": { + "unknown": "pubmatic" + } + } + } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "cur": "INR", + "nbr":0, + "seatbid": [ + { + "bid": [ + { + "id": "pubmatic-bid", + "impid": "my-imp-id", + "price": 20, + "ext": { + "origbidcpm": 5, + "origbidcur": "MXN", + "prebid": { + "meta": { + "adaptercode": "pubmatic" + }, + "type": "banner" + }, + "origbidcpmusd": 0.2491280518186348 + } + } + ], + "seat": "pubmatic" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go index c5410cbd740..f3a47da948a 100644 --- a/endpoints/openrtb2/test_utils.go +++ b/endpoints/openrtb2/test_utils.go @@ -83,6 +83,7 @@ type testConfigValues struct { CurrencyRates map[string]map[string]float64 `json:"currencyRates"` MockBidders []mockBidderHandler `json:"mockBidders"` RealParamsValidator bool `json:"realParamsValidator"` + AssertBidExt bool `json:"assertbidext"` } type brokenExchange struct{} diff --git a/errortypes/code.go b/errortypes/code.go index 3974e8bd99c..d0f87c5e4ad 100644 --- a/errortypes/code.go +++ b/errortypes/code.go @@ -12,6 +12,10 @@ const ( BlacklistedAcctErrorCode AcctRequiredErrorCode NoConversionRateErrorCode + NoBidPriceErrorCode + BidderFailedSchemaValidationErrorCode + AdpodPrefilteringErrorCode + BidRejectionFloorsErrorCode ) // Defines numeric codes for well-known warnings. @@ -22,6 +26,7 @@ const ( BidderLevelDebugDisabledWarningCode DisabledCurrencyConversionWarningCode AlternateBidderCodeWarningCode + AdpodPostFilteringWarningCode ) // Coder provides an error or warning code with severity. diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go index 1fed2d7da6e..9f08f61f851 100644 --- a/errortypes/errortypes.go +++ b/errortypes/errortypes.go @@ -182,3 +182,73 @@ func (err *Warning) Code() int { func (err *Warning) Severity() Severity { return SeverityWarning } + +// BidderFailedSchemaValidation is used at the request validation step, +// when the bidder parameters fail the schema validation, we want to +// continue processing the request and still return an error message. +type BidderFailedSchemaValidation struct { + Message string +} + +func (err *BidderFailedSchemaValidation) Error() string { + return err.Message +} + +func (err *BidderFailedSchemaValidation) Code() int { + return BidderFailedSchemaValidationErrorCode +} + +func (err *BidderFailedSchemaValidation) Severity() Severity { + return SeverityWarning +} + +// NoBidPrice should be used when vast response doesn't contain any price value +type NoBidPrice struct { + Message string +} + +func (err *NoBidPrice) Error() string { + return err.Message +} + +func (err *NoBidPrice) Code() int { + return NoBidPriceErrorCode +} + +func (err *NoBidPrice) Severity() Severity { + return SeverityWarning +} + +// AdpodPrefiltering should be used when ctv impression algorithm not able to generate impressions +type AdpodPrefiltering struct { + Message string +} + +func (err *AdpodPrefiltering) Error() string { + return err.Message +} + +func (err *AdpodPrefiltering) Code() int { + return AdpodPrefilteringErrorCode +} + +func (err *AdpodPrefiltering) Severity() Severity { + return SeverityFatal +} + +// AdpodPostFiltering should be used when vast response doesn't contain any price value +type AdpodPostFiltering struct { + Message string +} + +func (err *AdpodPostFiltering) Error() string { + return err.Message +} + +func (err *AdpodPostFiltering) Code() int { + return AdpodPostFilteringWarningCode +} + +func (err *AdpodPostFiltering) Severity() Severity { + return SeverityWarning +} diff --git a/errortypes/errortypes_test.go b/errortypes/errortypes_test.go new file mode 100644 index 00000000000..a17f4d0f6d0 --- /dev/null +++ b/errortypes/errortypes_test.go @@ -0,0 +1,188 @@ +package errortypes + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestErrors(t *testing.T) { + type args struct { + err error + } + type want struct { + errorMessage string + code int + severity Severity + } + tests := []struct { + name string + args args + want want + }{ + { + name: `normal_error`, + args: args{ + err: fmt.Errorf(`normal_error`), + }, + want: want{ + errorMessage: `normal_error`, + code: UnknownErrorCode, + severity: SeverityUnknown, + }, + }, + { + name: `Timeout`, + args: args{ + err: &Timeout{Message: `Timeout_ErrorMessage`}, + }, + want: want{ + errorMessage: `Timeout_ErrorMessage`, + code: TimeoutErrorCode, + severity: SeverityFatal, + }, + }, + { + name: `BadInput`, + args: args{ + err: &BadInput{Message: `BadInput_ErrorMessage`}, + }, + want: want{ + errorMessage: `BadInput_ErrorMessage`, + code: BadInputErrorCode, + severity: SeverityFatal, + }, + }, + { + name: `BlacklistedApp`, + args: args{ + err: &BlacklistedApp{Message: `BlacklistedApp_ErrorMessage`}, + }, + want: want{ + errorMessage: `BlacklistedApp_ErrorMessage`, + code: BlacklistedAppErrorCode, + severity: SeverityFatal, + }, + }, + { + name: `BlacklistedAcct`, + args: args{ + err: &BlacklistedAcct{Message: `BlacklistedAcct_ErrorMessage`}, + }, + want: want{ + errorMessage: `BlacklistedAcct_ErrorMessage`, + code: BlacklistedAcctErrorCode, + severity: SeverityFatal, + }, + }, + { + name: `AcctRequired`, + args: args{ + err: &AcctRequired{Message: `AcctRequired_ErrorMessage`}, + }, + want: want{ + errorMessage: `AcctRequired_ErrorMessage`, + code: AcctRequiredErrorCode, + severity: SeverityFatal, + }, + }, + { + name: `BadServerResponse`, + args: args{ + err: &BadServerResponse{Message: `BadServerResponse_ErrorMessage`}, + }, + want: want{ + errorMessage: `BadServerResponse_ErrorMessage`, + code: BadServerResponseErrorCode, + severity: SeverityFatal, + }, + }, + { + name: `FailedToRequestBids`, + args: args{ + err: &FailedToRequestBids{Message: `FailedToRequestBids_ErrorMessage`}, + }, + want: want{ + errorMessage: `FailedToRequestBids_ErrorMessage`, + code: FailedToRequestBidsErrorCode, + severity: SeverityFatal, + }, + }, + { + name: `BidderTemporarilyDisabled`, + args: args{ + err: &BidderTemporarilyDisabled{Message: `BidderTemporarilyDisabled_ErrorMessage`}, + }, + want: want{ + errorMessage: `BidderTemporarilyDisabled_ErrorMessage`, + code: BidderTemporarilyDisabledErrorCode, + severity: SeverityWarning, + }, + }, + { + name: `Warning`, + args: args{ + err: &Warning{Message: `Warning_ErrorMessage`, WarningCode: UnknownWarningCode}, + }, + want: want{ + errorMessage: `Warning_ErrorMessage`, + code: UnknownWarningCode, + severity: SeverityWarning, + }, + }, + { + name: `BidderFailedSchemaValidation`, + args: args{ + err: &BidderFailedSchemaValidation{Message: `BidderFailedSchemaValidation_ErrorMessage`}, + }, + want: want{ + errorMessage: `BidderFailedSchemaValidation_ErrorMessage`, + code: BidderFailedSchemaValidationErrorCode, + severity: SeverityWarning, + }, + }, + { + name: `NoBidPrice`, + args: args{ + err: &NoBidPrice{Message: `NoBidPrice_ErrorMessage`}, + }, + want: want{ + errorMessage: `NoBidPrice_ErrorMessage`, + code: NoBidPriceErrorCode, + severity: SeverityWarning, + }, + }, + { + name: `AdpodPrefiltering`, + args: args{ + err: &AdpodPrefiltering{Message: `AdpodPrefiltering_ErrorMessage`}, + }, + want: want{ + errorMessage: `AdpodPrefiltering_ErrorMessage`, + code: AdpodPrefilteringErrorCode, + severity: SeverityFatal, + }, + }, + { + name: `AdpodPostFiltering`, + args: args{ + err: &AdpodPostFiltering{Message: `AdpodPostFiltering_ErrorMessage`}, + }, + want: want{ + errorMessage: `AdpodPostFiltering_ErrorMessage`, + code: AdpodPostFilteringWarningCode, + severity: SeverityWarning, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want.errorMessage, tt.args.err.Error()) + if code, ok := tt.args.err.(Coder); ok { + assert.Equal(t, tt.want.code, code.Code()) + assert.Equal(t, tt.want.severity, code.Severity()) + } + }) + } +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go old mode 100755 new mode 100644 index 978b634c20b..7f59fb862ff --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -6,6 +6,7 @@ import ( "github.com/prebid/prebid-server/adapters/aax" "github.com/prebid/prebid-server/adapters/aceex" "github.com/prebid/prebid-server/adapters/acuityads" + "github.com/prebid/prebid-server/adapters/adbuttler" "github.com/prebid/prebid-server/adapters/adf" "github.com/prebid/prebid-server/adapters/adgeneration" "github.com/prebid/prebid-server/adapters/adhese" @@ -58,6 +59,7 @@ import ( "github.com/prebid/prebid-server/adapters/conversant" "github.com/prebid/prebid-server/adapters/cpmstar" "github.com/prebid/prebid-server/adapters/criteo" + "github.com/prebid/prebid-server/adapters/criteoretail" "github.com/prebid/prebid-server/adapters/datablocks" "github.com/prebid/prebid-server/adapters/decenterads" "github.com/prebid/prebid-server/adapters/deepintent" @@ -85,6 +87,7 @@ import ( "github.com/prebid/prebid-server/adapters/kargo" "github.com/prebid/prebid-server/adapters/kayzen" "github.com/prebid/prebid-server/adapters/kidoz" + "github.com/prebid/prebid-server/adapters/koddi" "github.com/prebid/prebid-server/adapters/krushmedia" "github.com/prebid/prebid-server/adapters/kubient" "github.com/prebid/prebid-server/adapters/lockerdome" @@ -127,6 +130,7 @@ import ( "github.com/prebid/prebid-server/adapters/smilewanted" "github.com/prebid/prebid-server/adapters/sonobi" "github.com/prebid/prebid-server/adapters/sovrn" + "github.com/prebid/prebid-server/adapters/spotx" "github.com/prebid/prebid-server/adapters/sspBC" "github.com/prebid/prebid-server/adapters/stroeerCore" "github.com/prebid/prebid-server/adapters/synacormedia" @@ -138,6 +142,7 @@ import ( "github.com/prebid/prebid-server/adapters/ucfunnel" "github.com/prebid/prebid-server/adapters/unicorn" "github.com/prebid/prebid-server/adapters/unruly" + "github.com/prebid/prebid-server/adapters/vastbidder" "github.com/prebid/prebid-server/adapters/videobyte" "github.com/prebid/prebid-server/adapters/vidoomy" "github.com/prebid/prebid-server/adapters/visx" @@ -289,6 +294,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderSonobi: sonobi.Builder, openrtb_ext.BidderSovrn: sovrn.Builder, openrtb_ext.BidderSspBC: sspBC.Builder, + openrtb_ext.BidderSpotX: spotx.Builder, openrtb_ext.BidderStreamkey: adtelligent.Builder, openrtb_ext.BidderStroeerCore: stroeerCore.Builder, openrtb_ext.BidderSynacormedia: synacormedia.Builder, @@ -301,6 +307,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderUcfunnel: ucfunnel.Builder, openrtb_ext.BidderUnicorn: unicorn.Builder, openrtb_ext.BidderUnruly: unruly.Builder, + openrtb_ext.BidderVASTBidder: vastbidder.Builder, openrtb_ext.BidderValueImpression: apacdex.Builder, openrtb_ext.BidderVerizonMedia: yahoossp.Builder, openrtb_ext.BidderVideoByte: videobyte.Builder, @@ -315,5 +322,10 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderYieldone: yieldone.Builder, openrtb_ext.BidderYSSP: yahoossp.Builder, openrtb_ext.BidderZeroClickFraud: zeroclickfraud.Builder, + openrtb_ext.BidderKoddi: koddi.Builder, + openrtb_ext.BidderAdButtler: adbuttler.Builder, + openrtb_ext.BidderCriteoRetail: criteoretail.Builder, + } } + diff --git a/exchange/auction.go b/exchange/auction.go index 0719ae959f8..0abb3b14ce1 100644 --- a/exchange/auction.go +++ b/exchange/auction.go @@ -294,12 +294,18 @@ func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, // makeVAST returns some VAST XML for the given bid. If AdM is defined, // it takes precedence. Otherwise the Nurl will be wrapped in a redirect tag. func makeVAST(bid *openrtb2.Bid) string { + wrapperVASTTemplate := `` + + `prebid.org wrapper` + + `` + + `` + + `` + if bid.AdM == "" { - return `` + - `prebid.org wrapper` + - `` + - `` + - `` + return fmt.Sprintf(wrapperVASTTemplate, bid.NURL) // set nurl as VASTAdTagURI + } + + if strings.HasPrefix(bid.AdM, "http") { // check if it contains URL + return fmt.Sprintf(wrapperVASTTemplate, bid.AdM) // set adm as VASTAdTagURI } return bid.AdM } diff --git a/exchange/auction_test.go b/exchange/auction_test.go index 6c6d41ab35a..7a21fe0750b 100644 --- a/exchange/auction_test.go +++ b/exchange/auction_test.go @@ -42,6 +42,20 @@ func TestMakeVASTNurl(t *testing.T) { assert.Equal(t, expect, vast) } +func TestMakeVASTAdmContainsURI(t *testing.T) { + const url = "http://myvast.com/1.xml" + const expect = `` + + `prebid.org wrapper` + + `` + + `` + + `` + bid := &openrtb2.Bid{ + AdM: url, + } + vast := makeVAST(bid) + assert.Equal(t, expect, vast) +} + func TestBuildCacheString(t *testing.T) { testCases := []struct { description string diff --git a/exchange/bidder.go b/exchange/bidder.go index 479c0256841..798a6201eb2 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -78,6 +78,7 @@ const ImpIdReqBody = "Stored bid response for impression id: " // pbsOrtbBid.dealPriority is optionally provided by adapters and used internally by the exchange to support deal targeted campaigns. // pbsOrtbBid.dealTierSatisfied is set to true by exchange.updateHbPbCatDur if deal tier satisfied otherwise it will be set to false // pbsOrtbBid.generatedBidID is unique bid id generated by prebid server if generate bid id option is enabled in config +// pbsOrtbBid.originalBidCPMUSD is USD rate of the bid for WL and WTK as they only accepts USD type pbsOrtbBid struct { bid *openrtb2.Bid bidMeta *openrtb_ext.ExtBidPrebidMeta @@ -90,6 +91,7 @@ type pbsOrtbBid struct { generatedBidID string originalBidCPM float64 originalBidCur string + originalBidCPMUSD float64 } // pbsOrtbSeatBid is a SeatBid returned by an AdaptedBidder. @@ -106,6 +108,8 @@ type pbsOrtbSeatBid struct { httpCalls []*openrtb_ext.ExtHttpCall // seat defines whom these extra bids belong to. seat string + // bidderCoreName represents the core bidder id. + bidderCoreName openrtb_ext.BidderName } // Possible values of compression types Prebid Server can support for bidder compression @@ -262,6 +266,7 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde } if httpInfo.err == nil { + httpInfo.request.BidderName = bidderRequest.BidderName bidResponse, moreErrs := bidder.Bidder.MakeBids(bidderRequest.BidRequest, httpInfo.request, httpInfo.response) errs = append(errs, moreErrs...) @@ -274,6 +279,13 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde bidderRequest.BidRequest.Cur = []string{defaultCurrency} } + // WL and WTK only accepts USD so we would need to convert prices to USD before sending data to them. But, + // PBS-Core's getAuctionCurrencyRates() is not exposed and would be too much work to do so. Also, would be a repeated work for SSHB to convert each bid's price + // Hence, we would send a USD conversion rate to SSHB for each bid beside prebid's origbidcpm and origbidcur + // Ex. req.cur=INR and resp.cur=JYP. Hence, we cannot use origbidcpm and origbidcur and would need a dedicated field for USD conversion rates + var conversionRateUSD float64 + selectedCur := "USD" + // Try to get a conversion rate // Try to get the first currency from request.cur having a match in the rate converter, // and use it as currency @@ -282,10 +294,21 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde for _, bidReqCur := range bidderRequest.BidRequest.Cur { if conversionRate, err = conversions.GetRate(bidResponse.Currency, bidReqCur); err == nil { seatBidMap[bidderRequest.BidderName].currency = bidReqCur + selectedCur = bidReqCur break } } + // no need of conversionRateUSD if + // - bids with conversionRate = 0 would be a dropped + // - response would be in USD + if conversionRate != float64(0) && selectedCur != "USD" { + conversionRateUSD, err = conversions.GetRate(bidResponse.Currency, "USD") + if err != nil { + errs = append(errs, fmt.Errorf("failed to get USD conversion rate for WL and WTK %v", err)) + } + } + // Only do this for request from mobile app if bidderRequest.BidRequest.App != nil { for i := 0; i < len(bidResponse.Bids); i++ { @@ -353,9 +376,11 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde } originalBidCpm := 0.0 + originalBidCPMUSD := 0.0 if bidResponse.Bids[i].Bid != nil { originalBidCpm = bidResponse.Bids[i].Bid.Price bidResponse.Bids[i].Bid.Price = bidResponse.Bids[i].Bid.Price * adjustmentFactor * conversionRate + originalBidCPMUSD = originalBidCpm * adjustmentFactor * conversionRateUSD } if _, ok := seatBidMap[bidderName]; !ok { @@ -377,6 +402,9 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde dealPriority: bidResponse.Bids[i].DealPriority, originalBidCPM: originalBidCpm, originalBidCur: bidResponse.Currency, + bidTargets: bidResponse.Bids[i].BidTargets, + + originalBidCPMUSD: originalBidCPMUSD, }) } } else { @@ -501,6 +529,12 @@ func makeExt(httpInfo *httpCallInfo) *openrtb_ext.ExtHttpCall { ext.ResponseBody = string(httpInfo.response.Body) ext.Status = httpInfo.response.StatusCode } + + if nil != httpInfo.request.Params { + ext.Params = make(map[string]int) + ext.Params["ImpIndex"] = httpInfo.request.Params.ImpIndex + ext.Params["VASTTagIndex"] = httpInfo.request.Params.VASTTagIndex + } } return ext @@ -669,7 +703,7 @@ func (bidder *bidderAdapter) addClientTrace(ctx context.Context) context.Context TLSHandshakeDone: func(tls.ConnectionState, error) { tlsHandshakeTime := time.Now().Sub(tlsStart) - bidder.me.RecordTLSHandshakeTime(tlsHandshakeTime) + bidder.me.RecordTLSHandshakeTime(bidder.BidderName, tlsHandshakeTime) }, } return httptrace.WithClientTrace(ctx, trace) diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index 2da7c5a21c0..2884101c8e9 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -1068,7 +1068,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) { bidRequestCurrencies: []string{"EUR", "USD", "JPY"}, bidResponsesCurrency: "EUR", expectedPickedCurrency: "EUR", - expectedError: false, + expectedError: true, //conversionRateUSD fails as currency conversion in this test is default. rates: currency.Rates{ Conversions: map[string]map[string]float64{ "JPY": { @@ -1088,7 +1088,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) { bidRequestCurrencies: []string{"JPY"}, bidResponsesCurrency: "JPY", expectedPickedCurrency: "JPY", - expectedError: false, + expectedError: true, //conversionRateUSD fails as currency conversion in this test is default. rates: currency.Rates{ Conversions: map[string]map[string]float64{ "JPY": { @@ -1972,7 +1972,7 @@ func TestCallRecordDNSTime(t *testing.T) { func TestCallRecordTLSHandshakeTime(t *testing.T) { // setup a mock metrics engine and its expectation metricsMock := &metrics.MetricsEngineMock{} - metricsMock.Mock.On("RecordTLSHandshakeTime", mock.Anything).Return() + metricsMock.Mock.On("RecordTLSHandshakeTime", mock.Anything, mock.Anything).Return() // Instantiate the bidder that will send the request. We'll make sure to use an // http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{}) diff --git a/exchange/events.go b/exchange/events.go index 02fe10a95cc..15161b6b74b 100644 --- a/exchange/events.go +++ b/exchange/events.go @@ -4,12 +4,12 @@ import ( "encoding/json" "time" - jsonpatch "gopkg.in/evanphx/json-patch.v4" - + "github.com/mxmCherry/openrtb/v16/openrtb2" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/endpoints/events" "github.com/prebid/prebid-server/openrtb_ext" + jsonpatch "gopkg.in/evanphx/json-patch.v4" ) // eventTracking has configuration fields needed for adding event tracking to an auction response @@ -37,13 +37,10 @@ func getEventTracking(requestExtPrebid *openrtb_ext.ExtRequestPrebid, ts time.Ti } // modifyBidsForEvents adds bidEvents and modifies VAST AdM if necessary. -func (ev *eventTracking) modifyBidsForEvents(seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid) map[openrtb_ext.BidderName]*pbsOrtbSeatBid { +func (ev *eventTracking) modifyBidsForEvents(seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, req *openrtb2.BidRequest, trackerURL string) map[openrtb_ext.BidderName]*pbsOrtbSeatBid { for bidderName, seatBid := range seatBids { - modifyingVastXMLAllowed := ev.isModifyingVASTXMLAllowed(bidderName.String()) for _, pbsBid := range seatBid.bids { - if modifyingVastXMLAllowed { - ev.modifyBidVAST(pbsBid, bidderName) - } + ev.modifyBidVAST(pbsBid, bidderName, seatBid.bidderCoreName, req, trackerURL) pbsBid.bidEvents = ev.makeBidExtEvents(pbsBid, bidderName) } } @@ -56,7 +53,7 @@ func (ev *eventTracking) isModifyingVASTXMLAllowed(bidderName string) bool { } // modifyBidVAST injects event Impression url if needed, otherwise returns original VAST string -func (ev *eventTracking) modifyBidVAST(pbsBid *pbsOrtbBid, bidderName openrtb_ext.BidderName) { +func (ev *eventTracking) modifyBidVAST(pbsBid *pbsOrtbBid, bidderName openrtb_ext.BidderName, bidderCoreName openrtb_ext.BidderName, req *openrtb2.BidRequest, trackerURL string) { bid := pbsBid.bid if pbsBid.bidType != openrtb_ext.BidTypeVideo || len(bid.AdM) == 0 && len(bid.NURL) == 0 { return @@ -66,8 +63,16 @@ func (ev *eventTracking) modifyBidVAST(pbsBid *pbsOrtbBid, bidderName openrtb_ex if len(pbsBid.generatedBidID) > 0 { bidID = pbsBid.generatedBidID } - if newVastXML, ok := events.ModifyVastXmlString(ev.externalURL, vastXML, bidID, bidderName.String(), ev.accountID, ev.auctionTimestampMs, ev.integrationType); ok { - bid.AdM = newVastXML + + if ev.isModifyingVASTXMLAllowed(bidderName.String()) { // condition added for ow fork + if newVastXML, ok := events.ModifyVastXmlString(ev.externalURL, vastXML, bidID, bidderName.String(), ev.accountID, ev.auctionTimestampMs, ev.integrationType); ok { + bid.AdM = newVastXML + } + } + + // always inject event trackers without checkign isModifyingVASTXMLAllowed + if newVastXML, injected, _ := events.InjectVideoEventTrackers(trackerURL, vastXML, bid, bidID, bidderName.String(), bidderCoreName.String(), ev.accountID, ev.auctionTimestampMs, req); injected { + bid.AdM = string(newVastXML) } } diff --git a/exchange/events_test.go b/exchange/events_test.go index 15aac1cd18a..7194d70ba58 100644 --- a/exchange/events_test.go +++ b/exchange/events_test.go @@ -1,9 +1,12 @@ package exchange import ( + "strings" "testing" "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" ) @@ -152,3 +155,108 @@ func Test_eventsData_modifyBidJSON(t *testing.T) { }) } } + +func TestModifyBidVAST(t *testing.T) { + type args struct { + bidReq *openrtb2.BidRequest + bid *openrtb2.Bid + } + type want struct { + tags []string + } + tests := []struct { + name string + args args + want want + }{ + { + name: "empty_adm", // expect adm contain vast tag with tracking events and VASTAdTagURI nurl contents + args: args{ + bidReq: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{ID: "123", Video: &openrtb2.Video{}}}, + }, + bid: &openrtb2.Bid{ + AdM: "", + NURL: "nurl_contents", + ImpID: "123", + }, + }, + want: want{ + tags: []string{ + // ``, + // ``, + // ``, + // ``, + // "", + // "", + // "", + ``, + ``, + ``, + ``, + "", + "", + "", + }, + }, + }, + { + name: "adm_containing_url", // expect adm contain vast tag with tracking events and VASTAdTagURI adm url (previous value) contents + args: args{ + bidReq: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{ID: "123", Video: &openrtb2.Video{}}}, + }, + bid: &openrtb2.Bid{ + AdM: "http://vast_tag_inline.xml", + NURL: "nurl_contents", + ImpID: "123", + }, + }, + want: want{ + tags: []string{ + // ``, + // ``, + // ``, + // ``, + // "", + // "", + // "", + ``, + ``, + ``, + ``, + "", + "", + "", + }, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ev := eventTracking{ + bidderInfos: config.BidderInfos{ + "somebidder": config.BidderInfo{ + ModifyingVastXmlAllowed: false, + }, + }, + } + ev.modifyBidVAST(&pbsOrtbBid{ + bid: tc.args.bid, + bidType: openrtb_ext.BidTypeVideo, + }, "somebidder", "coreBidder", tc.args.bidReq, "http://company.tracker.com?e=[EVENT_ID]") + validator(t, tc.args.bid, tc.want.tags) + }) + } +} + +func validator(t *testing.T, b *openrtb2.Bid, expectedTags []string) { + adm := b.AdM + assert.NotNil(t, adm) + assert.NotEmpty(t, adm) + // check tags are present + + for _, tag := range expectedTags { + assert.True(t, strings.Contains(adm, tag), "expected '"+tag+"' tag in Adm") + } +} diff --git a/exchange/exchange.go b/exchange/exchange.go index f1a9a7b8396..ab1c1ab8f52 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -20,6 +20,7 @@ import ( "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/experiment/adscert" "github.com/prebid/prebid-server/firstpartydata" + "github.com/prebid/prebid-server/floors" "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/metrics" "github.com/prebid/prebid-server/openrtb_ext" @@ -69,6 +70,9 @@ type exchange struct { bidIDGenerator BidIDGenerator hostSChainNode *openrtb2.SupplyChainNode adsCertSigner adscert.Signer + + floor config.PriceFloors + trakerURL string } // Container to pass out response ext data from the GetAllBids goroutines back into the main thread @@ -147,6 +151,9 @@ func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid bidIDGenerator: &bidIDGenerator{cfg.GenerateBidID}, hostSChainNode: cfg.HostSChainNode, adsCertSigner: adsCertSigner, + + floor: cfg.PriceFloors, + trakerURL: cfg.TrackerURL, } } @@ -161,6 +168,7 @@ type ImpExtInfo struct { type AuctionRequest struct { BidRequestWrapper *openrtb_ext.RequestWrapper ResolvedBidRequest json.RawMessage + UpdatedBidRequest json.RawMessage Account config.Account UserSyncs IdFetcher RequestType metrics.RequestType @@ -247,6 +255,13 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * bidAdjustmentFactors := getExtBidAdjustmentFactors(requestExt) + // Get currency rates conversions for the auction + conversions := e.getAuctionCurrencyRates(requestExt.Prebid.CurrencyConversions) + + // If floors feature is enabled at server and request level, Update floors values in impression object + floorErrs := selectFloorsAndModifyImp(&r, e.floor, conversions, responseDebugAllow) + errs = append(errs, floorErrs...) + recordImpMetrics(r.BidRequestWrapper.BidRequest, e.me) // Make our best guess if GDPR applies @@ -266,9 +281,6 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * auctionCtx, cancel := e.makeAuctionContext(ctx, cacheInstructions.cacheBids) defer cancel() - // Get currency rates conversions for the auction - conversions := e.getAuctionCurrencyRates(requestExt.Prebid.CurrencyConversions) - var adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid var adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra var anyBidsReturned bool @@ -294,11 +306,35 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * var bidResponseExt *openrtb_ext.ExtBidResponse if anyBidsReturned { + //If floor enforcement config enabled then filter bids + adapterBids, enforceErrs, rejectedBids := enforceFloors(&r, adapterBids, e.floor, conversions, responseDebugAllow) + errs = append(errs, enforceErrs...) + + if floors.RequestHasFloors(r.BidRequestWrapper.BidRequest) { + // Record request count with non-zero imp.bidfloor value + e.me.RecordFloorsRequestForAccount(r.PubID) + + if e.floor.Enabled && len(rejectedBids) > 0 { + // Record rejected bid count at account level + e.me.RecordRejectedBidsForAccount(r.PubID) + // Record rejected bid count at adaptor/bidder level + for _, rejectedBid := range rejectedBids { + e.me.RecordRejectedBidsForBidder(openrtb_ext.BidderName(rejectedBid.BidderName)) + } + } + } + + adapterBids, rejections := applyAdvertiserBlocking(r.BidRequestWrapper.BidRequest, adapterBids) + + // add advertiser blocking specific errors + for _, message := range rejections { + errs = append(errs, errors.New(message)) + } var bidCategory map[string]string //If includebrandcategory is present in ext then CE feature is on. if requestExt.Prebid.Targeting != nil && requestExt.Prebid.Targeting.IncludeBrandCategory != nil { var rejections []string - bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, requestExt, adapterBids, e.categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, r.BidRequestWrapper.BidRequest, requestExt, adapterBids, e.categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) if err != nil { return nil, fmt.Errorf("Error in category mapping : %s", err.Error()) } @@ -311,6 +347,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * for _, seatBid := range adapterBids { for _, pbsBid := range seatBid.bids { pbsBid.generatedBidID, err = e.bidIDGenerator.New() + glog.Infof("Original BidID = %s Generated BidID = %s", pbsBid.bid.ID, pbsBid.generatedBidID) if err != nil { errs = append(errs, errors.New("Error generating bid.ext.prebid.bidid")) } @@ -319,7 +356,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * } evTracking := getEventTracking(&requestExt.Prebid, r.StartTime, &r.Account, e.bidderInfo, e.externalURL) - adapterBids = evTracking.modifyBidsForEvents(adapterBids) + adapterBids = evTracking.modifyBidsForEvents(adapterBids, r.BidRequestWrapper.BidRequest, e.trakerURL) if targData != nil { // A non-nil auction is only needed if targeting is active. (It is used below this block to extract cache keys) @@ -503,6 +540,7 @@ func (e *exchange) getAllBids( adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, len(bidderRequests)) chBids := make(chan *bidResponseWrapper, len(bidderRequests)) bidsFound := false + bidIDsCollision := false for _, bidder := range bidderRequests { // Here we actually call the adapters and collect the bids. @@ -556,7 +594,12 @@ func (e *exchange) getAllBids( var cpm = float64(bid.bid.Price * 1000) e.me.RecordAdapterPrice(bidderRequest.BidderLabels, cpm) e.me.RecordAdapterBidReceived(bidderRequest.BidderLabels, bid.bidType, bid.bid.AdM != "") + if bid.bidType == openrtb_ext.BidTypeVideo && bid.bidVideo != nil && bid.bidVideo.Duration > 0 { + e.me.RecordAdapterVideoBidDuration(bidderRequest.BidderLabels, bid.bidVideo.Duration) + } } + // Setting bidderCoreName in SeatBid + seatBid.bidderCoreName = bidderRequest.BidderCoreName } } chBids <- brw @@ -582,9 +625,13 @@ func (e *exchange) getAllBids( if !bidsFound && adapterBids[brw.bidder] != nil && len(adapterBids[brw.bidder].bids) > 0 { bidsFound = true + bidIDsCollision = recordAdaptorDuplicateBidIDs(e.me, adapterBids) } } - + if bidIDsCollision { + // record this request count this request if bid collision is detected + e.me.RecordRequestHavingDuplicateBidID() + } return adapterBids, adapterExtra, bidsFound } @@ -605,9 +652,19 @@ func (e *exchange) recoverSafely(bidderRequests []BidderRequest, allBidders = sb.String()[:sb.Len()-1] } + bidderRequestStr := "" + if nil != bidderRequest.BidRequest { + value, err := json.Marshal(bidderRequest.BidRequest) + if nil == err { + bidderRequestStr = string(value) + } else { + bidderRequestStr = err.Error() + } + } + glog.Errorf("OpenRTB auction recovered panic from Bidder %s: %v. "+ - "Account id: %s, All Bidders: %s, Stack trace is: %v", - bidderRequest.BidderCoreName, r, bidderRequest.BidderLabels.PubID, allBidders, string(debug.Stack())) + "Account id: %s, All Bidders: %s, BidRequest: %s, Stack trace is: %v", + bidderRequest.BidderCoreName, r, bidderRequest.BidderLabels.PubID, allBidders, bidderRequestStr, string(debug.Stack())) e.me.RecordAdapterPanic(bidderRequest.BidderLabels) // Let the master request know that there is no data here brw := new(bidResponseWrapper) @@ -702,7 +759,6 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ } bidResponse.SeatBid = seatBids - bidResponse.Ext, err = encodeBidResponseExt(bidResponseExt) return bidResponse, err @@ -718,7 +774,7 @@ func encodeBidResponseExt(bidResponseExt *openrtb_ext.ExtBidResponse) ([]byte, e return buffer.Bytes(), err } -func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData, booleanGenerator deduplicateChanceGenerator) (map[string]string, map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []string, error) { +func applyCategoryMapping(ctx context.Context, bidRequest *openrtb2.BidRequest, requestExt *openrtb_ext.ExtRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData, booleanGenerator deduplicateChanceGenerator) (map[string]string, map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []string, error) { res := make(map[string]string) type bidDedupe struct { @@ -730,6 +786,8 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques dedupe := make(map[string]bidDedupe) + impMap := make(map[string]*openrtb2.Imp) + // applyCategoryMapping doesn't get called unless // requestExt.Prebid.Targeting != nil && requestExt.Prebid.Targeting.IncludeBrandCategory != nil brandCatExt := requestExt.Prebid.Targeting.IncludeBrandCategory @@ -744,6 +802,11 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques var rejections []string var translateCategories = true + //Maintaining BidRequest Impression Map + for i := range bidRequest.Imp { + impMap[bidRequest.Imp[i].ID] = &bidRequest.Imp[i] + } + if includeBrandCategory && brandCatExt.WithCategory { if brandCatExt.TranslateCategories != nil { translateCategories = *brandCatExt.TranslateCategories @@ -820,6 +883,12 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques break } } + } else if targData.priceGranularity.Test || newDur == 0 { + if imp, ok := impMap[bid.bid.ImpID]; ok { + if nil != imp.Video && imp.Video.MaxDuration > 0 { + newDur = int(imp.Video.MaxDuration) + } + } } var categoryDuration string @@ -836,50 +905,52 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques categoryDuration = fmt.Sprintf("%s_%s", categoryDuration, bidderName.String()) } - if dupe, ok := dedupe[dupeKey]; ok { + if !brandCatExt.SkipDedup { + if dupe, ok := dedupe[dupeKey]; ok { - dupeBidPrice, err := strconv.ParseFloat(dupe.bidPrice, 64) - if err != nil { - dupeBidPrice = 0 - } - currBidPrice, err := strconv.ParseFloat(pb, 64) - if err != nil { - currBidPrice = 0 - } - if dupeBidPrice == currBidPrice { - if booleanGenerator.Generate() { - dupeBidPrice = -1 - } else { - currBidPrice = -1 + dupeBidPrice, err := strconv.ParseFloat(dupe.bidPrice, 64) + if err != nil { + dupeBidPrice = 0 + } + currBidPrice, err := strconv.ParseFloat(pb, 64) + if err != nil { + currBidPrice = 0 + } + if dupeBidPrice == currBidPrice { + if booleanGenerator.Generate() { + dupeBidPrice = -1 + } else { + currBidPrice = -1 + } } - } - if dupeBidPrice < currBidPrice { - if dupe.bidderName == bidderName { - // An older bid from the current bidder - bidsToRemove = append(bidsToRemove, dupe.bidIndex) - rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated") - } else { - // An older bid from a different seatBid we've already finished with - oldSeatBid := (seatBids)[dupe.bidderName] - rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated") - if len(oldSeatBid.bids) == 1 { - seatBidsToRemove = append(seatBidsToRemove, dupe.bidderName) + if dupeBidPrice < currBidPrice { + if dupe.bidderName == bidderName { + // An older bid from the current bidder + bidsToRemove = append(bidsToRemove, dupe.bidIndex) + rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated") } else { - // This is a very rare, but still possible case where bid needs to be removed from already processed bidder - // This happens when current processing bidder has a bid that has same deduplication key as a bid from already processed bidder - // and already processed bid was selected to be removed - // See example of input data in unit test `TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice` - // Need to remove bid by name, not index in this case - removeBidById(oldSeatBid, dupe.bidID) + // An older bid from a different seatBid we've already finished with + oldSeatBid := (seatBids)[dupe.bidderName] + rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated") + if len(oldSeatBid.bids) == 1 { + seatBidsToRemove = append(seatBidsToRemove, dupe.bidderName) + } else { + // This is a very rare, but still possible case where bid needs to be removed from already processed bidder + // This happens when current processing bidder has a bid that has same deduplication key as a bid from already processed bidder + // and already processed bid was selected to be removed + // See example of input data in unit test `TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice` + // Need to remove bid by name, not index in this case + removeBidById(oldSeatBid, dupe.bidID) + } } + delete(res, dupe.bidID) + } else { + // Remove this bid + bidsToRemove = append(bidsToRemove, bidInd) + rejections = updateRejections(rejections, bidID, "Bid was deduplicated") + continue } - delete(res, dupe.bidID) - } else { - // Remove this bid - bidsToRemove = append(bidsToRemove, bidInd) - rejections = updateRejections(rejections, bidID, "Bid was deduplicated") - continue } } res[bidID] = categoryDuration @@ -955,6 +1026,7 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pb bidResponseExt.Debug = &openrtb_ext.ExtResponseDebug{ HttpCalls: make(map[openrtb_ext.BidderName][]*openrtb_ext.ExtHttpCall), ResolvedRequest: r.ResolvedBidRequest, + UpdatedRequest: r.UpdatedBidRequest, } } if !r.StartTime.IsZero() { @@ -1032,7 +1104,7 @@ func (e *exchange) makeBid(bids []*pbsOrtbBid, auc *auction, returnCreative bool } } - if bidExtJSON, err := makeBidExtJSON(bid.bid.Ext, bidExtPrebid, impExtInfoMap, bid.bid.ImpID, bid.originalBidCPM, bid.originalBidCur); err != nil { + if bidExtJSON, err := makeBidExtJSON(bid.bid.Ext, bidExtPrebid, impExtInfoMap, bid.bid.ImpID, bid.originalBidCPM, bid.originalBidCur, bid.originalBidCPMUSD); err != nil { errs = append(errs, err) } else { result = append(result, *bid.bid) @@ -1046,7 +1118,7 @@ func (e *exchange) makeBid(bids []*pbsOrtbBid, auc *auction, returnCreative bool return result, errs } -func makeBidExtJSON(ext json.RawMessage, prebid *openrtb_ext.ExtBidPrebid, impExtInfoMap map[string]ImpExtInfo, impId string, originalBidCpm float64, originalBidCur string) (json.RawMessage, error) { +func makeBidExtJSON(ext json.RawMessage, prebid *openrtb_ext.ExtBidPrebid, impExtInfoMap map[string]ImpExtInfo, impId string, originalBidCpm float64, originalBidCur string, originalBidCpmUSD float64) (json.RawMessage, error) { var extMap map[string]interface{} if len(ext) != 0 { @@ -1067,6 +1139,11 @@ func makeBidExtJSON(ext json.RawMessage, prebid *openrtb_ext.ExtBidPrebid, impEx extMap[openrtb_ext.OriginalBidCurKey] = originalBidCur } + //ext.origbidcpmusd + if originalBidCpmUSD > float64(0) { + extMap[openrtb_ext.OriginalBidCpmUsdKey] = originalBidCpmUSD + } + // ext.prebid if prebid.Meta == nil && maputil.HasElement(extMap, "prebid", "meta") { metaContainer := struct { @@ -1213,7 +1290,7 @@ func buildStoredAuctionResponse(storedAuctionResponses map[string]json.RawMessag } else { //create new seat bid and add it to live adapters liveAdapters = append(liveAdapters, bidderName) - newSeatBid := pbsOrtbSeatBid{bidsToAdd, "", nil, ""} + newSeatBid := pbsOrtbSeatBid{bidsToAdd, "", nil, "", bidderName} adapterBids[bidderName] = &newSeatBid } diff --git a/exchange/exchange_ow.go b/exchange/exchange_ow.go new file mode 100644 index 00000000000..323381f2686 --- /dev/null +++ b/exchange/exchange_ow.go @@ -0,0 +1,112 @@ +package exchange + +import ( + "errors" + "fmt" + "net/url" + "strings" + + "github.com/golang/glog" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/metrics" + "github.com/prebid/prebid-server/openrtb_ext" + "golang.org/x/net/publicsuffix" +) + +// recordAdaptorDuplicateBidIDs finds the bid.id collisions for each bidder and records them with metrics engine +// it returns true if collosion(s) is/are detected in any of the bidder's bids +func recordAdaptorDuplicateBidIDs(metricsEngine metrics.MetricsEngine, adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid) bool { + bidIDCollisionFound := false + if nil == adapterBids { + return false + } + for bidder, bid := range adapterBids { + bidIDColisionMap := make(map[string]int, len(adapterBids[bidder].bids)) + for _, thisBid := range bid.bids { + if collisions, ok := bidIDColisionMap[thisBid.bid.ID]; ok { + bidIDCollisionFound = true + bidIDColisionMap[thisBid.bid.ID]++ + glog.Warningf("Bid.id %v :: %v collision(s) [imp.id = %v] for bidder '%v'", thisBid.bid.ID, collisions, thisBid.bid.ImpID, string(bidder)) + metricsEngine.RecordAdapterDuplicateBidID(string(bidder), 1) + } else { + bidIDColisionMap[thisBid.bid.ID] = 1 + } + } + } + return bidIDCollisionFound +} + +//normalizeDomain validates, normalizes and returns valid domain or error if failed to validate +//checks if domain starts with http by lowercasing entire domain +//if not it prepends it before domain. This is required for obtaining the url +//using url.parse method. on successfull url parsing, it will replace first occurance of www. +//from the domain +func normalizeDomain(domain string) (string, error) { + domain = strings.Trim(strings.ToLower(domain), " ") + // not checking if it belongs to icann + suffix, _ := publicsuffix.PublicSuffix(domain) + if domain != "" && suffix == domain { // input is publicsuffix + return "", errors.New("domain [" + domain + "] is public suffix") + } + if !strings.HasPrefix(domain, "http") { + domain = fmt.Sprintf("http://%s", domain) + } + url, err := url.Parse(domain) + if nil == err && url.Host != "" { + return strings.Replace(url.Host, "www.", "", 1), nil + } + return "", err +} + +//applyAdvertiserBlocking rejects the bids of blocked advertisers mentioned in req.badv +//the rejection is currently only applicable to vast tag bidders. i.e. not for ortb bidders +//it returns seatbids containing valid bids and rejections containing rejected bid.id with reason +func applyAdvertiserBlocking(bidRequest *openrtb2.BidRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid) (map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []string) { + rejections := []string{} + nBadvs := []string{} + if nil != bidRequest.BAdv { + for _, domain := range bidRequest.BAdv { + nDomain, err := normalizeDomain(domain) + if nil == err && nDomain != "" { // skip empty and domains with errors + nBadvs = append(nBadvs, nDomain) + } + } + } + + for bidderName, seatBid := range seatBids { + if seatBid.bidderCoreName == openrtb_ext.BidderVASTBidder && len(nBadvs) > 0 { + for bidIndex := len(seatBid.bids) - 1; bidIndex >= 0; bidIndex-- { + bid := seatBid.bids[bidIndex] + for _, bAdv := range nBadvs { + aDomains := bid.bid.ADomain + rejectBid := false + if nil == aDomains { + // provision to enable rejecting of bids when req.badv is set + rejectBid = true + } else { + for _, d := range aDomains { + if aDomain, err := normalizeDomain(d); nil == err { + // compare and reject bid if + // 1. aDomain == bAdv + // 2. .bAdv is suffix of aDomain + // 3. aDomain not present but request has list of block advertisers + if aDomain == bAdv || strings.HasSuffix(aDomain, "."+bAdv) || (len(aDomain) == 0 && len(bAdv) > 0) { + // aDomain must be subdomain of bAdv + rejectBid = true + break + } + } + } + } + if rejectBid { + // reject the bid. bid belongs to blocked advertisers list + seatBid.bids = append(seatBid.bids[:bidIndex], seatBid.bids[bidIndex+1:]...) + rejections = updateRejections(rejections, bid.bid.ID, fmt.Sprintf("Bid (From '%s') belongs to blocked advertiser '%s'", bidderName, bAdv)) + break // bid is rejected due to advertiser blocked. No need to check further domains + } + } + } + } + } + return seatBids, rejections +} diff --git a/exchange/exchange_ow_test.go b/exchange/exchange_ow_test.go new file mode 100644 index 00000000000..73a425f097d --- /dev/null +++ b/exchange/exchange_ow_test.go @@ -0,0 +1,667 @@ +package exchange + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" + "testing" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/adapters/vastbidder" + "github.com/prebid/prebid-server/config" + metricsConf "github.com/prebid/prebid-server/metrics/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +//TestApplyAdvertiserBlocking verifies advertiser blocking +//Currently it is expected to work only with TagBidders and not woth +// normal bidders +func TestApplyAdvertiserBlocking(t *testing.T) { + type args struct { + advBlockReq *openrtb2.BidRequest + adaptorSeatBids map[*bidderAdapter]*pbsOrtbSeatBid // bidder adaptor and its dummy seat bids map + } + type want struct { + rejectedBidIds []string + validBidCountPerSeat map[string]int + } + tests := []struct { + name string + args args + want want + }{ + { + name: "reject_bid_of_blocked_adv_from_tag_bidder", + args: args{ + advBlockReq: &openrtb2.BidRequest{ + BAdv: []string{"a.com"}, // block bids returned by a.com + }, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("vast_tag_bidder"): { // tag bidder returning 1 bid from blocked advertiser + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "a.com_bid", + ADomain: []string{"a.com"}, + }, + }, + { + bid: &openrtb2.Bid{ + ID: "b.com_bid", + ADomain: []string{"b.com"}, + }, + }, + { + bid: &openrtb2.Bid{ + ID: "keep_ba.com", + ADomain: []string{"ba.com"}, + }, + }, + { + bid: &openrtb2.Bid{ + ID: "keep_ba.com", + ADomain: []string{"b.a.com.shri.com"}, + }, + }, + { + bid: &openrtb2.Bid{ + ID: "reject_b.a.com.a.com.b.c.d.a.com", + ADomain: []string{"b.a.com.a.com.b.c.d.a.com"}, + }, + }, + }, + bidderCoreName: openrtb_ext.BidderVASTBidder, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"a.com_bid", "reject_b.a.com.a.com.b.c.d.a.com"}, + validBidCountPerSeat: map[string]int{ + "vast_tag_bidder": 3, + }, + }, + }, + { + name: "Badv_is_not_present", // expect no advertiser blocking + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: nil}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tab_bidder_1"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_1_adapter_1", ADomain: []string{"a.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_1"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, // no bid rejection expected + validBidCountPerSeat: map[string]int{ + "tab_bidder_1": 2, + }, + }, + }, + { + name: "adomain_is_not_present_but_Badv_is_set", // reject bids without adomain as badv is set + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"advertiser_1.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder_1"): { + bids: []*pbsOrtbBid{ // expect all bids are rejected + {bid: &openrtb2.Bid{ID: "bid_1_adapter_1_without_adomain"}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_1_with_empty_adomain", ADomain: []string{"", " "}}}, + }, + }, + newTestRtbAdapter("rtb_bidder_1"): { + bids: []*pbsOrtbBid{ // all bids should be present. It belongs to RTB adapator + {bid: &openrtb2.Bid{ID: "bid_1_adapter_2_without_adomain"}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_2_with_empty_adomain", ADomain: []string{"", " "}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_adapter_1_without_adomain", "bid_2_adapter_1_with_empty_adomain"}, + validBidCountPerSeat: map[string]int{ + "tag_bidder_1": 0, // expect 0 bids. i.e. all bids are rejected + "rtb_bidder_1": 2, // no bid must be rejected + }, + }, + }, + { + name: "adomain_and_badv_is_not_present", // expect no advertiser blocking + args: args{ + advBlockReq: &openrtb2.BidRequest{}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adaptor_1"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_without_adomain"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, // no rejection expected as badv not present + validBidCountPerSeat: map[string]int{ + "tag_adaptor_1": 1, + }, + }, + }, + { + name: "empty_badv", // expect no advertiser blocking + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder_1"): { + bids: []*pbsOrtbBid{ // expect all bids are rejected + {bid: &openrtb2.Bid{ID: "bid_1_adapter_1", ADomain: []string{"a.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_1"}}, + }, + }, + newTestRtbAdapter("rtb_bidder_1"): { + bids: []*pbsOrtbBid{ // all bids should be present. It belongs to RTB adapator + {bid: &openrtb2.Bid{ID: "bid_1_adapter_2_without_adomain"}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_2_with_empty_adomain", ADomain: []string{"", " "}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, // no rejections expect as there is not badv set + validBidCountPerSeat: map[string]int{ + "tag_bidder_1": 2, + "rtb_bidder_1": 2, + }, + }, + }, + { + name: "nil_badv", // expect no advertiser blocking + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: nil}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder_1"): { + bids: []*pbsOrtbBid{ // expect all bids are rejected + {bid: &openrtb2.Bid{ID: "bid_1_adapter_1", ADomain: []string{"a.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_1"}}, + }, + }, + newTestRtbAdapter("rtb_bidder_1"): { + bids: []*pbsOrtbBid{ // all bids should be present. It belongs to RTB adapator + {bid: &openrtb2.Bid{ID: "bid_1_adapter_2_without_adomain"}}, + {bid: &openrtb2.Bid{ID: "bid_2_adapter_2_with_empty_adomain", ADomain: []string{"", " "}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, // no rejections expect as there is not badv set + validBidCountPerSeat: map[string]int{ + "tag_bidder_1": 2, + "rtb_bidder_1": 2, + }, + }, + }, + { + name: "ad_domains_normalized_and_checked", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"a.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("my_adapter"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_1_of_blocked_adv", ADomain: []string{"www.a.com"}}}, + // expect a.com is extracted from page url + {bid: &openrtb2.Bid{ID: "bid_2_of_blocked_adv", ADomain: []string{"http://a.com/my/page?k1=v1&k2=v2"}}}, + // invalid adomain - will be skipped and the bid will be not be rejected + {bid: &openrtb2.Bid{ID: "bid_3_with_domain_abcd1234", ADomain: []string{"abcd1234"}}}, + }, + }}, + }, + want: want{ + rejectedBidIds: []string{"bid_1_of_blocked_adv", "bid_2_of_blocked_adv"}, + validBidCountPerSeat: map[string]int{"my_adapter": 1}, + }, + }, { + name: "multiple_badv", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"advertiser_1.com", "advertiser_2.com", "www.advertiser_3.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adapter_1"): { + bids: []*pbsOrtbBid{ + // adomain without www prefix + {bid: &openrtb2.Bid{ID: "bid_1_tag_adapter_1", ADomain: []string{"advertiser_3.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_tag_adapter_1", ADomain: []string{"advertiser_2.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_3_tag_adapter_1", ADomain: []string{"advertiser_4.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_4_tag_adapter_1", ADomain: []string{"advertiser_100.com"}}}, + }, + }, + newTestTagAdapter("tag_adapter_2"): { + bids: []*pbsOrtbBid{ + // adomain has www prefix + {bid: &openrtb2.Bid{ID: "bid_1_tag_adapter_2", ADomain: []string{"www.advertiser_1.com"}}}, + }, + }, + newTestRtbAdapter("rtb_adapter_1"): { + bids: []*pbsOrtbBid{ + // should not reject following bid though its advertiser is blocked + // because this bid belongs to RTB Adaptor + {bid: &openrtb2.Bid{ID: "bid_1_rtb_adapter_2", ADomain: []string{"advertiser_1.com"}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_tag_adapter_1", "bid_2_tag_adapter_1", "bid_1_tag_adapter_2"}, + validBidCountPerSeat: map[string]int{ + "tag_adapter_1": 2, + "tag_adapter_2": 0, + "rtb_adapter_1": 1, + }, + }, + }, { + name: "multiple_adomain", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"www.advertiser_3.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adapter_1"): { + bids: []*pbsOrtbBid{ + // adomain without www prefix + {bid: &openrtb2.Bid{ID: "bid_1_tag_adapter_1", ADomain: []string{"a.com", "b.com", "advertiser_3.com", "d.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_tag_adapter_1", ADomain: []string{"a.com", "https://advertiser_3.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_3_tag_adapter_1", ADomain: []string{"advertiser_4.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_4_tag_adapter_1", ADomain: []string{"advertiser_100.com"}}}, + }, + }, + newTestTagAdapter("tag_adapter_2"): { + bids: []*pbsOrtbBid{ + // adomain has www prefix + {bid: &openrtb2.Bid{ID: "bid_1_tag_adapter_2", ADomain: []string{"a.com", "b.com", "www.advertiser_3.com"}}}, + }, + }, + newTestRtbAdapter("rtb_adapter_1"): { + bids: []*pbsOrtbBid{ + // should not reject following bid though its advertiser is blocked + // because this bid belongs to RTB Adaptor + {bid: &openrtb2.Bid{ID: "bid_1_rtb_adapter_2", ADomain: []string{"a.com", "b.com", "advertiser_3.com"}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_tag_adapter_1", "bid_2_tag_adapter_1", "bid_1_tag_adapter_2"}, + validBidCountPerSeat: map[string]int{ + "tag_adapter_1": 2, + "tag_adapter_2": 0, + "rtb_adapter_1": 1, + }, + }, + }, { + name: "case_insensitive_badv", // case of domain not matters + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"ADVERTISER_1.COM"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adapter_1"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_1_rtb_adapter_1", ADomain: []string{"advertiser_1.com"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_rtb_adapter_1", ADomain: []string{"www.advertiser_1.com"}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_rtb_adapter_1", "bid_2_rtb_adapter_1"}, + validBidCountPerSeat: map[string]int{ + "tag_adapter_1": 0, // expect all bids are rejected as belongs to blocked advertiser + }, + }, + }, + { + name: "case_insensitive_adomain", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"advertiser_1.com"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_adapter_1"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ID: "bid_1_rtb_adapter_1", ADomain: []string{"advertiser_1.COM"}}}, + {bid: &openrtb2.Bid{ID: "bid_2_rtb_adapter_1", ADomain: []string{"wWw.ADVERTISER_1.com"}}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"bid_1_rtb_adapter_1", "bid_2_rtb_adapter_1"}, + validBidCountPerSeat: map[string]int{ + "tag_adapter_1": 0, // expect all bids are rejected as belongs to blocked advertiser + }, + }, + }, + { + name: "various_tld_combinations", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"http://blockme.shri"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("block_bidder"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ADomain: []string{"www.blockme.shri"}, ID: "reject_www.blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"http://www.blockme.shri"}, ID: "rejecthttp://www.blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"https://blockme.shri"}, ID: "reject_https://blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"https://www.blockme.shri"}, ID: "reject_https://www.blockme.shri"}}, + }, + }, + newTestRtbAdapter("rtb_non_block_bidder"): { + bids: []*pbsOrtbBid{ // all below bids are eligible and should not be rejected + {bid: &openrtb2.Bid{ADomain: []string{"www.blockme.shri"}, ID: "accept_bid_www.blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"http://www.blockme.shri"}, ID: "accept_bid__http://www.blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"https://blockme.shri"}, ID: "accept_bid__https://blockme.shri"}}, + {bid: &openrtb2.Bid{ADomain: []string{"https://www.blockme.shri"}, ID: "accept_bid__https://www.blockme.shri"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"reject_www.blockme.shri", "reject_http://www.blockme.shri", "reject_https://blockme.shri", "reject_https://www.blockme.shri"}, + validBidCountPerSeat: map[string]int{ + "block_bidder": 0, + "rtb_non_block_bidder": 4, + }, + }, + }, + { + name: "subdomain_tests", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"10th.college.puneunv.edu"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("block_bidder"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ADomain: []string{"shri.10th.college.puneunv.edu"}, ID: "reject_shri.10th.college.puneunv.edu"}}, + {bid: &openrtb2.Bid{ADomain: []string{"puneunv.edu"}, ID: "allow_puneunv.edu"}}, + {bid: &openrtb2.Bid{ADomain: []string{"http://WWW.123.456.10th.college.PUNEUNV.edu"}, ID: "reject_123.456.10th.college.puneunv.edu"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{"reject_shri.10th.college.puneunv.edu", "reject_123.456.10th.college.puneunv.edu"}, + validBidCountPerSeat: map[string]int{ + "block_bidder": 1, + }, + }, + }, { + name: "only_domain_test", // do not expect bid rejection. edu is valid domain + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"edu"}}, + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ADomain: []string{"school.edu"}, ID: "keep_bid_school.edu"}}, + {bid: &openrtb2.Bid{ADomain: []string{"edu"}, ID: "keep_bid_edu"}}, + {bid: &openrtb2.Bid{ADomain: []string{"..edu"}, ID: "keep_bid_..edu"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, + validBidCountPerSeat: map[string]int{ + "tag_bidder": 3, + }, + }, + }, + { + name: "public_suffix_in_badv", + args: args{ + advBlockReq: &openrtb2.BidRequest{BAdv: []string{"co.in"}}, // co.in is valid public suffix + adaptorSeatBids: map[*bidderAdapter]*pbsOrtbSeatBid{ + newTestTagAdapter("tag_bidder"): { + bids: []*pbsOrtbBid{ + {bid: &openrtb2.Bid{ADomain: []string{"a.co.in"}, ID: "allow_a.co.in"}}, + {bid: &openrtb2.Bid{ADomain: []string{"b.com"}, ID: "allow_b.com"}}, + }, + }, + }, + }, + want: want{ + rejectedBidIds: []string{}, + validBidCountPerSeat: map[string]int{ + "tag_bidder": 2, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.name != "reject_bid_of_blocked_adv_from_tag_bidder" { + return + } + seatBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + tagBidders := make(map[openrtb_ext.BidderName]adapters.Bidder) + adapterMap := make(map[openrtb_ext.BidderName]AdaptedBidder, 0) + for adaptor, sbids := range tt.args.adaptorSeatBids { + adapterMap[adaptor.BidderName] = adaptor + if tagBidder, ok := adaptor.Bidder.(*vastbidder.TagBidder); ok { + tagBidders[adaptor.BidderName] = tagBidder + } + seatBids[adaptor.BidderName] = sbids + } + + // applyAdvertiserBlocking internally uses tagBidders from (adapter_map.go) + // not testing alias here + seatBids, rejections := applyAdvertiserBlocking(tt.args.advBlockReq, seatBids) + + re := regexp.MustCompile("bid rejected \\[bid ID:(.*?)\\] reason") + for bidder, sBid := range seatBids { + // verify only eligible bids are returned + assert.Equal(t, tt.want.validBidCountPerSeat[string(bidder)], len(sBid.bids), "Expected eligible bids are %d, but found [%d] ", tt.want.validBidCountPerSeat[string(bidder)], len(sBid.bids)) + // verify rejections + assert.Equal(t, len(tt.want.rejectedBidIds), len(rejections), "Expected bid rejections are %d, but found [%d]", len(tt.want.rejectedBidIds), len(rejections)) + // verify rejected bid ids + present := false + for _, expectRejectedBidID := range tt.want.rejectedBidIds { + for _, rejection := range rejections { + match := re.FindStringSubmatch(rejection) + rejectedBidID := strings.Trim(match[1], " ") + if expectRejectedBidID == rejectedBidID { + present = true + break + } + } + if present { + break + } + } + if len(tt.want.rejectedBidIds) > 0 && !present { + assert.Fail(t, "Expected Bid ID [%s] as rejected. But bid is not rejected", re) + } + + if sBid.bidderCoreName != openrtb_ext.BidderVASTBidder { + continue // advertiser blocking is currently enabled only for tag bidders + } + // verify eligible bids not belongs to blocked advertisers + for _, bid := range sBid.bids { + if nil != bid.bid.ADomain { + for _, adomain := range bid.bid.ADomain { + for _, blockDomain := range tt.args.advBlockReq.BAdv { + nDomain, _ := normalizeDomain(adomain) + if nDomain == blockDomain { + assert.Fail(t, "bid %s with ad domain %s is not blocked", bid.bid.ID, adomain) + } + } + } + } + + // verify this bid not belongs to rejected list + for _, rejectedBidID := range tt.want.rejectedBidIds { + if rejectedBidID == bid.bid.ID { + assert.Fail(t, "Bid ID [%s] is not expected in list of rejected bids", bid.bid.ID) + } + } + } + } + }) + } +} + +func TestNormalizeDomain(t *testing.T) { + type args struct { + domain string + } + type want struct { + domain string + err error + } + tests := []struct { + name string + args args + want want + }{ + {name: "a.com", args: args{domain: "a.com"}, want: want{domain: "a.com"}}, + {name: "http://a.com", args: args{domain: "http://a.com"}, want: want{domain: "a.com"}}, + {name: "https://a.com", args: args{domain: "https://a.com"}, want: want{domain: "a.com"}}, + {name: "https://www.a.com", args: args{domain: "https://www.a.com"}, want: want{domain: "a.com"}}, + {name: "https://www.a.com/my/page?k=1", args: args{domain: "https://www.a.com/my/page?k=1"}, want: want{domain: "a.com"}}, + {name: "empty_domain", args: args{domain: ""}, want: want{domain: ""}}, + {name: "trim_domain", args: args{domain: " trim.me?k=v "}, want: want{domain: "trim.me"}}, + {name: "trim_domain_with_http_in_it", args: args{domain: " http://trim.me?k=v "}, want: want{domain: "trim.me"}}, + {name: "https://www.something.a.com/my/page?k=1", args: args{domain: "https://www.something.a.com/my/page?k=1"}, want: want{domain: "something.a.com"}}, + {name: "wWW.something.a.com", args: args{domain: "wWW.something.a.com"}, want: want{domain: "something.a.com"}}, + {name: "2_times_www", args: args{domain: "www.something.www.a.com"}, want: want{domain: "something.www.a.com"}}, + {name: "consecutive_www", args: args{domain: "www.www.something.a.com"}, want: want{domain: "www.something.a.com"}}, + {name: "abchttp.com", args: args{domain: "abchttp.com"}, want: want{domain: "abchttp.com"}}, + {name: "HTTP://CAPS.com", args: args{domain: "HTTP://CAPS.com"}, want: want{domain: "caps.com"}}, + + // publicsuffix + {name: "co.in", args: args{domain: "co.in"}, want: want{domain: "", err: fmt.Errorf("domain [co.in] is public suffix")}}, + {name: ".co.in", args: args{domain: ".co.in"}, want: want{domain: ".co.in"}}, + {name: "amazon.co.in", args: args{domain: "amazon.co.in"}, want: want{domain: "amazon.co.in"}}, + // we wont check if shriprasad belongs to icann + {name: "shriprasad", args: args{domain: "shriprasad"}, want: want{domain: "", err: fmt.Errorf("domain [shriprasad] is public suffix")}}, + {name: ".shriprasad", args: args{domain: ".shriprasad"}, want: want{domain: ".shriprasad"}}, + {name: "abc.shriprasad", args: args{domain: "abc.shriprasad"}, want: want{domain: "abc.shriprasad"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adjustedDomain, err := normalizeDomain(tt.args.domain) + actualErr := "nil" + expectedErr := "nil" + if nil != err { + actualErr = err.Error() + } + if nil != tt.want.err { + actualErr = tt.want.err.Error() + } + assert.Equal(t, tt.want.err, err, "Expected error is %s, but found [%s]", expectedErr, actualErr) + assert.Equal(t, tt.want.domain, adjustedDomain, "Expected domain is %s, but found [%s]", tt.want.domain, adjustedDomain) + }) + } +} + +func newTestTagAdapter(name string) *bidderAdapter { + return &bidderAdapter{ + Bidder: vastbidder.NewTagBidder(openrtb_ext.BidderName(name), config.Adapter{}), + BidderName: openrtb_ext.BidderName(name), + } +} + +func newTestRtbAdapter(name string) *bidderAdapter { + return &bidderAdapter{ + Bidder: &goodSingleBidder{}, + BidderName: openrtb_ext.BidderName(name), + } +} + +func TestRecordAdaptorDuplicateBidIDs(t *testing.T) { + type bidderCollisions = map[string]int + testCases := []struct { + scenario string + bidderCollisions *bidderCollisions // represents no of collisions detected for bid.id at bidder level for given request + hasCollision bool + }{ + {scenario: "invalid collision value", bidderCollisions: &map[string]int{"bidder-1": -1}, hasCollision: false}, + {scenario: "no collision", bidderCollisions: &map[string]int{"bidder-1": 0}, hasCollision: false}, + {scenario: "one collision", bidderCollisions: &map[string]int{"bidder-1": 1}, hasCollision: false}, + {scenario: "multiple collisions", bidderCollisions: &map[string]int{"bidder-1": 2}, hasCollision: true}, // when 2 collisions it counter will be 1 + {scenario: "multiple bidders", bidderCollisions: &map[string]int{"bidder-1": 2, "bidder-2": 4}, hasCollision: true}, + {scenario: "multiple bidders with bidder-1 no collision", bidderCollisions: &map[string]int{"bidder-1": 1, "bidder-2": 4}, hasCollision: true}, + {scenario: "no bidders", bidderCollisions: nil, hasCollision: false}, + } + testEngine := metricsConf.NewMetricsEngine(&config.Configuration{}, nil, nil) + + for _, testcase := range testCases { + var adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid + if nil == testcase.bidderCollisions { + break + } + adapterBids = make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + for bidder, collisions := range *testcase.bidderCollisions { + bids := make([]*pbsOrtbBid, 0) + testBidID := "bid_id_for_bidder_" + bidder + // add bids as per collisions value + bidCount := 0 + for ; bidCount < collisions; bidCount++ { + bids = append(bids, &pbsOrtbBid{ + bid: &openrtb2.Bid{ + ID: testBidID, + }, + }) + } + if nil == adapterBids[openrtb_ext.BidderName(bidder)] { + adapterBids[openrtb_ext.BidderName(bidder)] = new(pbsOrtbSeatBid) + } + adapterBids[openrtb_ext.BidderName(bidder)].bids = bids + } + assert.Equal(t, testcase.hasCollision, recordAdaptorDuplicateBidIDs(testEngine, adapterBids)) + } +} + +func TestMakeBidExtJSONOW(t *testing.T) { + + type aTest struct { + description string + ext json.RawMessage + extBidPrebid openrtb_ext.ExtBidPrebid + impExtInfo map[string]ImpExtInfo + origbidcpm float64 + origbidcur string + origbidcpmusd float64 + expectedBidExt string + expectedErrMessage string + } + + testCases := []aTest{ + { + description: "Valid extension with origbidcpmusd = 0", + ext: json.RawMessage(`{"video":{"h":100}}`), + extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video"), Meta: &openrtb_ext.ExtBidPrebidMeta{BrandName: "foo"}, Passthrough: nil}, + impExtInfo: map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`), json.RawMessage(`{"imp_passthrough_val": 1}`)}}, + origbidcpm: 10.0000, + origbidcur: "USD", + expectedBidExt: `{"prebid":{"meta": {"brandName": "foo"}, "passthrough":{"imp_passthrough_val":1}, "type":"video"}, "storedrequestattributes":{"h":480,"mimes":["video/mp4"]},"video":{"h":100}, "origbidcpm": 10, "origbidcur": "USD"}`, + expectedErrMessage: "", + }, + { + description: "Valid extension with origbidcpmusd > 0", + ext: json.RawMessage(`{"video":{"h":100}}`), + extBidPrebid: openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video"), Meta: &openrtb_ext.ExtBidPrebidMeta{BrandName: "foo"}, Passthrough: nil}, + impExtInfo: map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`), json.RawMessage(`{"imp_passthrough_val": 1}`)}}, + origbidcpm: 10.0000, + origbidcur: "USD", + origbidcpmusd: 10.0000, + expectedBidExt: `{"prebid":{"meta": {"brandName": "foo"}, "passthrough":{"imp_passthrough_val":1}, "type":"video"}, "storedrequestattributes":{"h":480,"mimes":["video/mp4"]},"video":{"h":100}, "origbidcpm": 10, "origbidcur": "USD", "origbidcpmusd": 10}`, + expectedErrMessage: "", + }, + } + + for _, test := range testCases { + result, err := makeBidExtJSON(test.ext, &test.extBidPrebid, test.impExtInfo, "test_imp_id", test.origbidcpm, test.origbidcur, test.origbidcpmusd) + + if test.expectedErrMessage == "" { + assert.JSONEq(t, test.expectedBidExt, string(result), "Incorrect result") + assert.NoError(t, err, "Error should not be returned") + } else { + assert.Contains(t, err.Error(), test.expectedErrMessage, "incorrect error message") + } + } +} diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 8ef79e30f29..2431929099d 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -2515,6 +2515,7 @@ func TestCategoryMapping(t *testing.T) { t.Errorf("Failed to create a category Fetcher: %v", error) } + bidRequest := openrtb2.BidRequest{} requestExt := newExtRequest() targData := &targetData{ @@ -2535,10 +2536,10 @@ func TestCategoryMapping(t *testing.T) { bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1} - bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} - bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD"} - bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, "", 30.0000, "USD"} - bid1_4 := pbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 40.0000, "USD"} + bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} + bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", 20.0000} + bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, "", 30.0000, "USD", 30.0000} + bid1_4 := pbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 40.0000, "USD", 40.0000} innerBids := []*pbsOrtbBid{ &bid1_1, @@ -2552,7 +2553,7 @@ func TestCategoryMapping(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &bidRequest, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") @@ -2571,6 +2572,7 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { t.Errorf("Failed to create a category Fetcher: %v", error) } + bidRequest := openrtb2.BidRequest{} requestExt := newExtRequestNoBrandCat() targData := &targetData{ @@ -2590,10 +2592,10 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1} - bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} - bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD"} - bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, "", 30.0000, "USD"} - bid1_4 := pbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, "", 40.0000, "USD"} + bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} + bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", 20.0000} + bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, "", 30.0000, "USD", 30.0000} + bid1_4 := pbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, "", 40.0000, "USD", 40.0000} innerBids := []*pbsOrtbBid{ &bid1_1, @@ -2607,7 +2609,7 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &bidRequest, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be no bid rejection messages") @@ -2626,6 +2628,7 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { t.Errorf("Failed to create a category Fetcher: %v", error) } + bidRequest := openrtb2.BidRequest{} requestExt := newExtRequestTranslateCategories(nil) targData := &targetData{ @@ -2644,9 +2647,9 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} - bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} - bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD"} - bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 30.0000, "USD"} + bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} + bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", 20.0000} + bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 30.0000, "USD", 30.0000} innerBids := []*pbsOrtbBid{ &bid1_1, @@ -2659,7 +2662,7 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &bidRequest, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") @@ -2708,6 +2711,7 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { } translateCategories := false + bidRequest := openrtb2.BidRequest{} requestExt := newExtRequestTranslateCategories(&translateCategories) targData := &targetData{ @@ -2726,9 +2730,9 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} - bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} - bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD"} - bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 30.0000, "USD"} + bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} + bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", 20.0000} + bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 30.0000, "USD", 30.0000} innerBids := []*pbsOrtbBid{ &bid1_1, @@ -2741,7 +2745,7 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &bidRequest, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be no bid rejection messages") @@ -2759,6 +2763,7 @@ func TestCategoryDedupe(t *testing.T) { t.Errorf("Failed to create a category Fetcher: %v", error) } + bidRequest := openrtb2.BidRequest{} requestExt := newExtRequest() targData := &targetData{ @@ -2778,11 +2783,11 @@ func TestCategoryDedupe(t *testing.T) { bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1} bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 20.0000, Cat: cats1, W: 1, H: 1} - bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} - bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, "", 15.0000, "USD"} - bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"} - bid1_4 := pbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"} - bid1_5 := pbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"} + bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} + bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, "", 15.0000, "USD", 15.0000} + bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", 20.0000} + bid1_4 := pbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", 20.0000} + bid1_5 := pbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", 20.0000} selectedBids := make(map[string]int) expectedCategories := map[string]string{ @@ -2811,7 +2816,7 @@ func TestCategoryDedupe(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &bidRequest, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 3, len(rejections), "There should be 2 bid rejection messages") @@ -2839,6 +2844,7 @@ func TestNoCategoryDedupe(t *testing.T) { t.Errorf("Failed to create a category Fetcher: %v", error) } + bidRequest := openrtb2.BidRequest{} requestExt := newExtRequestNoBrandCat() targData := &targetData{ @@ -2857,11 +2863,11 @@ func TestNoCategoryDedupe(t *testing.T) { bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1} bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 10.0000, Cat: cats1, W: 1, H: 1} - bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 14.0000, "USD"} - bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 14.0000, "USD"} - bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"} - bid1_4 := pbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"} - bid1_5 := pbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} + bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 14.0000, "USD", 14.0000} + bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 14.0000, "USD", 14.0000} + bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", 20.0000} + bid1_4 := pbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", 20.0000} + bid1_5 := pbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} selectedBids := make(map[string]int) expectedCategories := map[string]string{ @@ -2891,7 +2897,7 @@ func TestNoCategoryDedupe(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &bidRequest, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 2, len(rejections), "There should be 2 bid rejection messages") @@ -2928,6 +2934,7 @@ func TestCategoryMappingBidderName(t *testing.T) { includeWinners: true, } + bidRequest := openrtb2.BidRequest{} requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30} adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) @@ -2937,8 +2944,8 @@ func TestCategoryMappingBidderName(t *testing.T) { bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 10.0000, Cat: cats2, W: 1, H: 1} - bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} - bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} + bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} + bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} innerBids1 := []*pbsOrtbBid{ &bid1_1, @@ -2956,7 +2963,7 @@ func TestCategoryMappingBidderName(t *testing.T) { adapterBids[bidderName1] = &seatBid1 adapterBids[bidderName2] = &seatBid2 - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &bidRequest, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be 0 bid rejection messages") @@ -2982,6 +2989,7 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) { includeWinners: true, } + bidRequest := openrtb2.BidRequest{} requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30} adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) @@ -2991,8 +2999,8 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) { bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 12.0000, Cat: cats2, W: 1, H: 1} - bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} - bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 12.0000, "USD"} + bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} + bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 12.0000, "USD", 12.0000} innerBids1 := []*pbsOrtbBid{ &bid1_1, @@ -3010,7 +3018,7 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) { adapterBids[bidderName1] = &seatBid1 adapterBids[bidderName2] = &seatBid2 - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &bidRequest, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be 0 bid rejection messages") @@ -3103,15 +3111,16 @@ func TestBidRejectionErrors(t *testing.T) { innerBids := []*pbsOrtbBid{} for _, bid := range test.bids { currentBid := pbsOrtbBid{ - bid, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, nil, 0, false, "", 10.0000, "USD"} + bid, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, nil, 0, false, "", 10.0000, "USD", 10.0000} innerBids = append(innerBids, ¤tBid) } seatBid := pbsOrtbSeatBid{bids: innerBids, currency: "USD"} adapterBids[bidderName] = &seatBid + bidRequest := openrtb2.BidRequest{} - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &test.reqExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &bidRequest, &test.reqExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) if len(test.expectedCatDur) > 0 { // Bid deduplication case @@ -3135,6 +3144,7 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { t.Errorf("Failed to create a category Fetcher: %v", error) } + bidRequest := openrtb2.BidRequest{} requestExt := newExtRequestTranslateCategories(nil) targData := &targetData{ @@ -3151,8 +3161,8 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { bidApn1 := openrtb2.Bid{ID: "bid_idApn1", ImpID: "imp_idApn1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bidApn2 := openrtb2.Bid{ID: "bid_idApn2", ImpID: "imp_idApn2", Price: 10.0000, Cat: cats2, W: 1, H: 1} - bid1_Apn1 := pbsOrtbBid{&bidApn1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} - bid1_Apn2 := pbsOrtbBid{&bidApn2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} + bid1_Apn1 := pbsOrtbBid{&bidApn1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} + bid1_Apn2 := pbsOrtbBid{&bidApn2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} innerBidsApn1 := []*pbsOrtbBid{ &bid1_Apn1, @@ -3174,7 +3184,7 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { adapterBids[bidderNameApn1] = &seatBidApn1 adapterBids[bidderNameApn2] = &seatBidApn2 - bidCategory, _, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &bidRequest, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Len(t, rejections, 1, "There should be 1 bid rejection message") @@ -3212,6 +3222,7 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) t.Errorf("Failed to create a category Fetcher: %v", error) } + bidRequest := openrtb2.BidRequest{} requestExt := newExtRequestTranslateCategories(nil) targData := &targetData{ @@ -3231,11 +3242,11 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) bidApn2_1 := openrtb2.Bid{ID: "bid_idApn2_1", ImpID: "imp_idApn2_1", Price: 10.0000, Cat: cats2, W: 1, H: 1} bidApn2_2 := openrtb2.Bid{ID: "bid_idApn2_2", ImpID: "imp_idApn2_2", Price: 20.0000, Cat: cats2, W: 1, H: 1} - bid1_Apn1_1 := pbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} - bid1_Apn1_2 := pbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"} + bid1_Apn1_1 := pbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} + bid1_Apn1_2 := pbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", 20.0000} - bid1_Apn2_1 := pbsOrtbBid{&bidApn2_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} - bid1_Apn2_2 := pbsOrtbBid{&bidApn2_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"} + bid1_Apn2_1 := pbsOrtbBid{&bidApn2_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} + bid1_Apn2_2 := pbsOrtbBid{&bidApn2_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", 20.0000} innerBidsApn1 := []*pbsOrtbBid{ &bid1_Apn1_1, @@ -3258,7 +3269,7 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) adapterBids[bidderNameApn1] = &seatBidApn1 adapterBids[bidderNameApn2] = &seatBidApn2 - _, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &fakeRandomDeduplicateBidBooleanGenerator{true}) + _, adapterBids, rejections, err := applyCategoryMapping(nil, &bidRequest, &requestExt, adapterBids, categoriesFetcher, targData, &fakeRandomDeduplicateBidBooleanGenerator{true}) assert.NoError(t, err, "Category mapping error should be empty") @@ -3313,9 +3324,9 @@ func TestRemoveBidById(t *testing.T) { bidApn1_2 := openrtb2.Bid{ID: "bid_idApn1_2", ImpID: "imp_idApn1_2", Price: 20.0000, Cat: cats1, W: 1, H: 1} bidApn1_3 := openrtb2.Bid{ID: "bid_idApn1_3", ImpID: "imp_idApn1_3", Price: 10.0000, Cat: cats1, W: 1, H: 1} - bid1_Apn1_1 := pbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} - bid1_Apn1_2 := pbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"} - bid1_Apn1_3 := pbsOrtbBid{&bidApn1_3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"} + bid1_Apn1_1 := pbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} + bid1_Apn1_2 := pbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", 20.0000} + bid1_Apn1_3 := pbsOrtbBid{&bidApn1_3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", 10.0000} type aTest struct { desc string @@ -3440,7 +3451,7 @@ func TestApplyDealSupport(t *testing.T) { }, } - bid := pbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, "", 0, "USD"} + bid := pbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, "", 0, "USD", 0} bidCategory := map[string]string{ bid.bid.ID: test.targ["hb_pb_cat_dur"], } @@ -3607,7 +3618,7 @@ func TestUpdateHbPbCatDur(t *testing.T) { } for _, test := range testCases { - bid := pbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, "", 0, "USD"} + bid := pbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, "", 0, "USD", 0} bidCategory := map[string]string{ bid.bid.ID: test.targ["hb_pb_cat_dur"], } @@ -3628,6 +3639,7 @@ func TestMakeBidExtJSON(t *testing.T) { impExtInfo map[string]ImpExtInfo origbidcpm float64 origbidcur string + origbidcpmusd float64 expectedBidExt string expectedErrMessage string } @@ -3810,7 +3822,7 @@ func TestMakeBidExtJSON(t *testing.T) { } for _, test := range testCases { - result, err := makeBidExtJSON(test.ext, &test.extBidPrebid, test.impExtInfo, "test_imp_id", test.origbidcpm, test.origbidcur) + result, err := makeBidExtJSON(test.ext, &test.extBidPrebid, test.impExtInfo, "test_imp_id", test.origbidcpm, test.origbidcur, test.origbidcpmusd) if test.expectedErrMessage == "" { assert.JSONEq(t, test.expectedBidExt, string(result), "Incorrect result") diff --git a/exchange/floors.go b/exchange/floors.go new file mode 100644 index 00000000000..526df4278c2 --- /dev/null +++ b/exchange/floors.go @@ -0,0 +1,190 @@ +package exchange + +import ( + "encoding/json" + "fmt" + "math/rand" + + "github.com/golang/glog" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/currency" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/floors" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// RejectedBid defines the contract for bid rejection errors due to floors enforcement +type RejectedBid struct { + Bid *openrtb2.Bid `json:"bid,omitempty"` + RejectionReason int `json:"rejectreason,omitempty"` + BidderName string `json:"biddername,omitempty"` +} + +// Check for Floors enforcement for deals, +// In case bid wit DealID present and enforceDealFloors = false then bid floor enforcement should be skipped +func checkDealsForEnforcement(bid *pbsOrtbBid, enforceDealFloors bool) *pbsOrtbBid { + if bid != nil && bid.bid != nil && bid.bid.DealID != "" && !enforceDealFloors { + return bid + } + return nil +} + +// Get conversion rate in case floor currency and seatBid currency are not same +func getCurrencyConversionRate(seatBidCur, reqImpCur string, conversions currency.Conversions) (float64, error) { + rate := 1.0 + if seatBidCur != reqImpCur { + return conversions.GetRate(seatBidCur, reqImpCur) + } else { + return rate, nil + } +} + +// enforceFloorToBids function does floors enforcement for each bid. +// The bids returned by each partner below bid floor price are rejected and remaining eligible bids are considered for further processing +func enforceFloorToBids(bidRequest *openrtb2.BidRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, conversions currency.Conversions, enforceDealFloors bool) (map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []error, []RejectedBid) { + errs := []error{} + rejectedBids := []RejectedBid{} + impMap := make(map[string]openrtb2.Imp, len(bidRequest.Imp)) + + //Maintaining BidRequest Impression Map + for i := range bidRequest.Imp { + impMap[bidRequest.Imp[i].ID] = bidRequest.Imp[i] + } + + for bidderName, seatBid := range seatBids { + eligibleBids := make([]*pbsOrtbBid, 0, len(seatBid.bids)) + for _, bid := range seatBid.bids { + retBid := checkDealsForEnforcement(bid, enforceDealFloors) + if retBid != nil { + eligibleBids = append(eligibleBids, retBid) + continue + } + + reqImp, ok := impMap[bid.bid.ImpID] + if ok { + reqImpCur := reqImp.BidFloorCur + if reqImpCur == "" { + if bidRequest.Cur != nil { + reqImpCur = bidRequest.Cur[0] + } else { + reqImpCur = "USD" + } + } + rate, err := getCurrencyConversionRate(seatBid.currency, reqImpCur, conversions) + if err == nil { + bidPrice := rate * bid.bid.Price + if reqImp.BidFloor > bidPrice { + rejectedBid := RejectedBid{ + Bid: bid.bid, + BidderName: string(bidderName), + RejectionReason: errortypes.BidRejectionFloorsErrorCode, + } + rejectedBids = append(rejectedBids, rejectedBid) + errs = append(errs, fmt.Errorf("bid rejected [bid ID: %s] reason: bid price value %.4f %s is less than bidFloor value %.4f %s for impression id %s bidder %s", bid.bid.ID, bidPrice, reqImpCur, reqImp.BidFloor, reqImpCur, bid.bid.ImpID, bidderName)) + } else { + eligibleBids = append(eligibleBids, bid) + } + } else { + errMsg := fmt.Errorf("Error in rate conversion from = %s to %s with bidder %s for impression id %s and bid id %s", seatBid.currency, reqImpCur, bidderName, bid.bid.ImpID, bid.bid.ID) + glog.Errorf(errMsg.Error()) + errs = append(errs, errMsg) + + } + } + } + seatBids[bidderName].bids = eligibleBids + } + return seatBids, errs, rejectedBids +} + +// selectFloorsAndModifyImp function does singanlling of floors, +// Internally validation of floors parameters and validation of rules is done, +// Based on number of modelGroups and modelWeight, one model is selected and imp.bidfloor and imp.bidfloorcur is updated +func selectFloorsAndModifyImp(r *AuctionRequest, floor config.PriceFloors, conversions currency.Conversions, responseDebugAllow bool) []error { + var errs []error + if r == nil || r.BidRequestWrapper == nil { + return errs + } + + requestExt, err := r.BidRequestWrapper.GetRequestExt() + if err != nil { + errs = append(errs, err) + return errs + } + prebidExt := requestExt.GetPrebid() + if floor.Enabled && prebidExt != nil && prebidExt.Floors != nil && prebidExt.Floors.GetEnabled() { + errs = floors.ModifyImpsWithFloors(prebidExt.Floors, r.BidRequestWrapper.BidRequest, conversions) + requestExt.SetPrebid(prebidExt) + err := r.BidRequestWrapper.RebuildRequest() + if err != nil { + errs = append(errs, err) + } + + if responseDebugAllow { + updatedBidReq, _ := json.Marshal(r.BidRequestWrapper.BidRequest) + //save updated request after floors signalling + r.UpdatedBidRequest = updatedBidReq + } + } + return errs +} + +// getFloorsFlagFromReqExt returns floors enabled flag, +// if floors enabled flag is not provided in request extesion, by default treated as true +func getFloorsFlagFromReqExt(prebidExt *openrtb_ext.ExtRequestPrebid) bool { + floorEnabled := true + if prebidExt == nil || prebidExt.Floors == nil || prebidExt.Floors.Enabled == nil { + return floorEnabled + } + return *prebidExt.Floors.Enabled +} + +func getEnforceDealsFlag(Floors *openrtb_ext.PriceFloorRules) bool { + return Floors != nil && Floors.Enforcement != nil && Floors.Enforcement.FloorDeals != nil && *Floors.Enforcement.FloorDeals +} + +// eneforceFloors function does floors enforcement +func enforceFloors(r *AuctionRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, floor config.PriceFloors, conversions currency.Conversions, responseDebugAllow bool) (map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []error, []RejectedBid) { + + rejectionsErrs := []error{} + rejecteBids := []RejectedBid{} + if r == nil || r.BidRequestWrapper == nil { + return seatBids, rejectionsErrs, rejecteBids + } + + requestExt, err := r.BidRequestWrapper.GetRequestExt() + if err != nil { + rejectionsErrs = append(rejectionsErrs, err) + return seatBids, rejectionsErrs, rejecteBids + } + prebidExt := requestExt.GetPrebid() + reqFloorEnable := getFloorsFlagFromReqExt(prebidExt) + if floor.Enabled && reqFloorEnable { + var enforceDealFloors bool + var floorsEnfocement bool + floorsEnfocement = floors.RequestHasFloors(r.BidRequestWrapper.BidRequest) + if prebidExt != nil && floorsEnfocement { + if floorsEnfocement = floors.ShouldEnforce(r.BidRequestWrapper.BidRequest, prebidExt.Floors, floor.EnforceFloorsRate, rand.Intn); floorsEnfocement { + enforceDealFloors = floor.EnforceDealFloors && getEnforceDealsFlag(prebidExt.Floors) + } + } + + if floorsEnfocement { + seatBids, rejectionsErrs, rejecteBids = enforceFloorToBids(r.BidRequestWrapper.BidRequest, seatBids, conversions, enforceDealFloors) + } + requestExt.SetPrebid(prebidExt) + err = r.BidRequestWrapper.RebuildRequest() + if err != nil { + rejectionsErrs = append(rejectionsErrs, err) + return seatBids, rejectionsErrs, rejecteBids + } + + if responseDebugAllow { + updatedBidReq, _ := json.Marshal(r.BidRequestWrapper.BidRequest) + //save updated request after floors enforcement + r.UpdatedBidRequest = updatedBidReq + } + } + return seatBids, rejectionsErrs, rejecteBids +} diff --git a/exchange/floors_test.go b/exchange/floors_test.go new file mode 100644 index 00000000000..16d1b02a098 --- /dev/null +++ b/exchange/floors_test.go @@ -0,0 +1,1913 @@ +package exchange + +import ( + "encoding/json" + "errors" + "reflect" + "sort" + "testing" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/currency" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +type convert struct { +} + +func (c convert) GetRate(from string, to string) (float64, error) { + + if from == to { + return 1, nil + } + + if from == "USD" && to == "INR" { + return 77.59, nil + } else if from == "INR" && to == "USD" { + return 0.013, nil + } + + return 0, errors.New("currency conversion not supported") + +} + +func (c convert) GetRates() *map[string]map[string]float64 { + return &map[string]map[string]float64{} +} + +func ErrToString(Err []error) []string { + var errString []string + for _, eachErr := range Err { + errString = append(errString, eachErr.Error()) + } + sort.Strings(errString) + return errString +} + +func TestEnforceFloorToBids(t *testing.T) { + + type args struct { + bidRequest *openrtb2.BidRequest + seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid + conversions currency.Conversions + enforceDealFloors bool + } + tests := []struct { + name string + args args + want map[openrtb_ext.BidderName]*pbsOrtbSeatBid + want1 []string + }{ + { + name: "Bids with same currency", + args: args{ + bidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{ + { + ID: "some-impression-id-1", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 1.01, + BidFloorCur: "USD", + }, + { + ID: "some-impression-id-2", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 400, H: 350}, {W: 200, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 2.01, + BidFloorCur: "USD", + }, + }, + Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + AT: 1, + TMax: 500, + }, + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-2", + Price: 1.5, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + }, + conversions: currency.Conversions(convert{}), + enforceDealFloors: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 0.5000 USD is less than bidFloor value 1.0100 USD for impression id some-impression-id-1 bidder appnexus", "bid rejected [bid ID: some-bid-2] reason: bid price value 1.5000 USD is less than bidFloor value 2.0100 USD for impression id some-impression-id-2 bidder pubmatic"}, + }, + { + name: "Bids with different currency", + args: args{ + bidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{ + { + ID: "some-impression-id-1", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 60, + BidFloorCur: "INR", + }, + { + ID: "some-impression-id-2", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 400, H: 350}, {W: 200, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 100, + BidFloorCur: "INR", + }, + }, + Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + AT: 1, + TMax: 500, + }, + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-2", + Price: 1.5, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + }, + conversions: currency.Conversions(convert{}), + enforceDealFloors: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-2", + Price: 1.5, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 38.7950 INR is less than bidFloor value 60.0000 INR for impression id some-impression-id-1 bidder appnexus"}, + }, + { + name: "Bids with different currency with enforceDealFloor false", + args: args{ + bidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{ + { + ID: "some-impression-id-1", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 60, + BidFloorCur: "INR", + }, + { + ID: "some-impression-id-2", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 400, H: 350}, {W: 200, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 100, + BidFloorCur: "INR", + }, + }, + Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + AT: 1, + TMax: 500, + }, + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-2", + Price: 1.5, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + }, + conversions: currency.Conversions(convert{}), + enforceDealFloors: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-2", + Price: 1.5, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 38.7950 INR is less than bidFloor value 60.0000 INR for impression id some-impression-id-1 bidder appnexus"}, + }, + { + name: "Dealid not empty, enforceDealFloors is true", + args: args{ + bidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{ + { + ID: "some-impression-id-1", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 60, + BidFloorCur: "INR", + }, + { + ID: "some-impression-id-2", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 400, H: 350}, {W: 200, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 100, + BidFloorCur: "INR", + }, + }, + Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + AT: 1, + TMax: 500, + }, + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-2", + Price: 1.5, + ImpID: "some-impression-id-2", + DealID: "2", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + DealID: "3", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + DealID: "4", + }, + }, + }, + currency: "USD", + }, + }, + conversions: currency.Conversions(convert{}), + enforceDealFloors: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-2", + Price: 1.5, + ImpID: "some-impression-id-2", + DealID: "2", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + DealID: "4", + }, + }, + }, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 38.7950 INR is less than bidFloor value 60.0000 INR for impression id some-impression-id-1 bidder appnexus"}, + }, + { + name: "Dealid not empty, enforceDealFloors is false", + args: args{ + bidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{ + { + ID: "some-impression-id-1", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 60, + BidFloorCur: "INR", + }, + { + ID: "some-impression-id-2", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 400, H: 350}, {W: 200, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 100, + BidFloorCur: "INR", + }, + }, + Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + AT: 1, + TMax: 500, + }, + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-2", + Price: 1.5, + ImpID: "some-impression-id-2", + DealID: "2", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + DealID: "3", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + DealID: "4", + }, + }, + }, + currency: "USD", + }, + }, + conversions: currency.Conversions(convert{}), + enforceDealFloors: false, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-2", + Price: 1.5, + ImpID: "some-impression-id-2", + DealID: "2", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + DealID: "3", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + DealID: "4", + }, + }, + }, + currency: "USD", + }, + }, + want1: nil, + }, + { + name: "Impression does not have currency defined", + args: args{ + bidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Cur: []string{"USD"}, + Imp: []openrtb2.Imp{ + { + ID: "some-impression-id-1", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 1.01, + }, + { + ID: "some-impression-id-2", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 400, H: 350}, {W: 200, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 2.01, + }, + }, + Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + AT: 1, + TMax: 500, + }, + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-2", + Price: 1.5, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + }, + conversions: currency.Conversions(convert{}), + enforceDealFloors: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 0.5000 USD is less than bidFloor value 1.0100 USD for impression id some-impression-id-1 bidder appnexus", "bid rejected [bid ID: some-bid-2] reason: bid price value 1.5000 USD is less than bidFloor value 2.0100 USD for impression id some-impression-id-2 bidder pubmatic"}, + }, + { + name: "Impression map does not have imp id", + args: args{ + bidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Cur: []string{"USD"}, + Imp: []openrtb2.Imp{ + { + ID: "some-impression-id-1", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 1.01, + }, + { + ID: "some-impression-id-2", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 400, H: 350}, {W: 200, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 2.01, + }, + }, + Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + AT: 1, + TMax: 500, + }, + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-2", + Price: 1.5, + ImpID: "some-impression-id-2", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-3", + Price: 1.4, + ImpID: "some-impression-id-3", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + }, + conversions: currency.Conversions(convert{}), + enforceDealFloors: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-12", + Price: 2.2, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 0.5000 USD is less than bidFloor value 1.0100 USD for impression id some-impression-id-1 bidder appnexus", "bid rejected [bid ID: some-bid-2] reason: bid price value 1.5000 USD is less than bidFloor value 2.0100 USD for impression id some-impression-id-2 bidder pubmatic"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + seatbids, errs, _ := enforceFloorToBids(tt.args.bidRequest, tt.args.seatBids, tt.args.conversions, tt.args.enforceDealFloors) + if !reflect.DeepEqual(seatbids, tt.want) { + t.Errorf("enforceFloorToBids() got = %v, want %v", seatbids, tt.want) + } + assert.Equal(t, tt.want1, ErrToString(errs)) + }) + } +} + +func TestEnforceFloorToBidsConversion(t *testing.T) { + + type args struct { + bidRequest *openrtb2.BidRequest + seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid + conversions currency.Conversions + enforceDealFloors bool + } + + tests := []struct { + name string + args args + want map[openrtb_ext.BidderName]*pbsOrtbSeatBid + want1 []string + }{ + { + name: "Error in currency conversion", + args: args{ + bidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Cur: []string{"USD"}, + Imp: []openrtb2.Imp{ + { + ID: "some-impression-id-1", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 1.01, + }, + { + ID: "some-impression-id-2", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 400, H: 350}, {W: 200, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + BidFloor: 2.01, + }, + }, + Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + AT: 1, + TMax: 500, + }, + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + { + bid: &openrtb2.Bid{ + ID: "some-bid-2", + Price: 1.5, + ImpID: "some-impression-id-2", + }, + }, + }, + currency: "EUR", + }, + }, + conversions: convert{}, + enforceDealFloors: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{}, + currency: "EUR", + }, + }, + want1: []string{"Error in rate conversion from = EUR to USD with bidder pubmatic for impression id some-impression-id-1 and bid id some-bid-1", "Error in rate conversion from = EUR to USD with bidder pubmatic for impression id some-impression-id-2 and bid id some-bid-2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, _ := enforceFloorToBids(tt.args.bidRequest, tt.args.seatBids, tt.args.conversions, tt.args.enforceDealFloors) + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.want1, ErrToString(got1)) + }) + } +} + +func TestSelectFloorsAndModifyImp(t *testing.T) { + type args struct { + r *AuctionRequest + floor config.PriceFloors + conversions currency.Conversions + responseDebugAllow bool + } + tests := []struct { + name string + args args + want []error + expBidFloor float64 + expBidFloorCur string + }{ + { + name: "Should Signal Floors", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"Some-imp-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/1234/DMDemo","bidfloor":100,"bidfloorcur":"USD","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/1234/DMDemo@300x250","publisherId":"123","wiid":"e643368f-06fe-4493-86a8-36ae2f13286a","wrapper":{"version":1,"profile":123}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://myurl.com","ver":"1.0","publisher":{"id":"123"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.1.1.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891995,"cur":["USD"],"ext":{"prebid":{"aliases":{"adg":"adgeneration","andbeyond":"adkernel","appnexus-1":"appnexus","appnexus-2":"appnexus","districtm":"appnexus","districtmDMX":"dmx","pubmatic2":"pubmatic"},"channel":{"name":"app","version":""},"debug":true,"bidderparams":{"pubmatic":{"wiid":"e643368f-06fe-4493-86a8-36ae2f13286a"}},"floors":{"floormin":1,"data":{"currency":"USD","skiprate":0,"modelgroups":[{"modelweight":40,"modelversion":"version1","skiprate":0,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":17.01,"*|*|www.website1.com":16.01,"*|300x250|*":11.01,"*|300x250|www.website1.com":100.01,"*|300x600|*":13.01,"*|300x600|www.website1.com":12.01,"*|728x90|*":15.01,"*|728x90|www.website1.com":14.01,"banner|*|*":90.01,"banner|*|www.website1.com":80.01,"banner|300x250|*":30.01,"banner|300x250|www.website1.com":20.01,"banner|300x600|*":50.01,"banner|300x600|www.website1.com":40.01,"banner|728x90|*":70.01,"banner|728x90|www.website1.com":60.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":true},"enabled":true}}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: nil, + expBidFloor: 20.01, + expBidFloorCur: "USD", + }, + { + name: "Should not Signal Floors as req.ext.prebid.floors.enabled = false", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"Some-imp-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/1234/DMDemo","bidfloor":100,"bidfloorcur":"USD","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/1234/DMDemo@300x250","publisherId":"123","wiid":"e643368f-06fe-4493-86a8-36ae2f13286a","wrapper":{"version":1,"profile":123}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://myurl.com","ver":"1.0","publisher":{"id":"123"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.1.1.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891995,"cur":["USD"],"ext":{"prebid":{"aliases":{"adg":"adgeneration","andbeyond":"adkernel","appnexus-1":"appnexus","appnexus-2":"appnexus","districtm":"appnexus","districtmDMX":"dmx","pubmatic2":"pubmatic"},"channel":{"name":"app","version":""},"debug":true,"bidderparams":{"pubmatic":{"wiid":"e643368f-06fe-4493-86a8-36ae2f13286a"}},"floors":{"floormin":1,"data":{"currency":"USD","skiprate":0,"modelgroups":[{"modelweight":40,"modelversion":"version1","skiprate":0,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":17.01,"*|*|www.website1.com":16.01,"*|300x250|*":11.01,"*|300x250|www.website1.com":100.01,"*|300x600|*":13.01,"*|300x600|www.website1.com":12.01,"*|728x90|*":15.01,"*|728x90|www.website1.com":14.01,"banner|*|*":90.01,"banner|*|www.website1.com":80.01,"banner|300x250|*":30.01,"banner|300x250|www.website1.com":20.01,"banner|300x600|*":50.01,"banner|300x600|www.website1.com":40.01,"banner|728x90|*":70.01,"banner|728x90|www.website1.com":60.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":true},"enabled":false}}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: nil, + expBidFloor: 100.00, + expBidFloorCur: "USD", + }, + { + name: "Should not Signal Floors as req.ext.prebid.floors not provided", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"Some-imp-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/1234/DMDemo","bidfloor":100,"bidfloorcur":"USD","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/1234/DMDemo@300x250","publisherId":"123","wiid":"e643368f-06fe-4493-86a8-36ae2f13286a","wrapper":{"version":1,"profile":123}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://myurl.com","ver":"1.0","publisher":{"id":"123"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.1.1.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891995,"cur":["USD"],"ext":{"prebid":{"aliases":{"adg":"adgeneration","andbeyond":"adkernel","appnexus-1":"appnexus","appnexus-2":"appnexus","districtm":"appnexus","districtmDMX":"dmx","pubmatic2":"pubmatic"},"channel":{"name":"app","version":""},"debug":true,"bidderparams":{"pubmatic":{"wiid":"e643368f-06fe-4493-86a8-36ae2f13286a"}}}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: nil, + expBidFloor: 100.00, + expBidFloorCur: "USD", + }, + { + name: "Should not Signal Floors as req.ext.prebid not provided", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"Some-imp-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/1234/DMDemo","bidfloor":100,"bidfloorcur":"USD","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/1234/DMDemo@300x250","publisherId":"123","wiid":"e643368f-06fe-4493-86a8-36ae2f13286a","wrapper":{"version":1,"profile":123}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://myurl.com","ver":"1.0","publisher":{"id":"123"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.1.1.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891995,"cur":["USD"],"ext":{}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: nil, + expBidFloor: 100.00, + expBidFloorCur: "USD", + }, + { + name: "Should not Signal Floors as req.ext not provided", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"Some-imp-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/1234/DMDemo","bidfloor":100,"bidfloorcur":"USD","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/1234/DMDemo@300x250","publisherId":"123","wiid":"e643368f-06fe-4493-86a8-36ae2f13286a","wrapper":{"version":1,"profile":123}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://myurl.com","ver":"1.0","publisher":{"id":"123"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.1.1.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891995,"cur":["USD"]}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: nil, + expBidFloor: 100.00, + expBidFloorCur: "USD", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := selectFloorsAndModifyImp(tt.args.r, tt.args.floor, tt.args.conversions, tt.args.responseDebugAllow); !reflect.DeepEqual(got, tt.want) { + t.Errorf("selectFloorsAndModifyImp() = %v, want %v", got, tt.want) + } + + if !reflect.DeepEqual(tt.args.r.BidRequestWrapper.Imp[0].BidFloor, tt.expBidFloor) { + t.Errorf("selectFloorsAndModifyImp() bidfloor value = %v, want %v", tt.args.r.BidRequestWrapper.Imp[0].BidFloor, tt.expBidFloor) + } + + if !reflect.DeepEqual(tt.args.r.BidRequestWrapper.Imp[0].BidFloorCur, tt.expBidFloorCur) { + t.Errorf("selectFloorsAndModifyImp() bidfloorcur value = %v, want %v", tt.args.r.BidRequestWrapper.Imp[0].BidFloorCur, tt.expBidFloorCur) + } + + }) + } +} + +func TestEnforceFloors(t *testing.T) { + type args struct { + r *AuctionRequest + seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid + floor config.PriceFloors + conversions currency.Conversions + responseDebugAllow bool + } + tests := []struct { + name string + args args + want map[openrtb_ext.BidderName]*pbsOrtbSeatBid + want1 []string + }{ + { + name: "Should enforce floors for deals, ext.prebid.floors.enforcement.floorDeals=true and floors enabled = true", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"some-impression-id-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/43743431/DMDemo","bidfloor":20.01,"bidfloorcur":"USD","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/43743431/DMDemo@300x250","publisherId":"5890","wiid":"42faaac0-9134-41c2-a283-77f1302d00ac","wrapper":{"version":1,"profile":7255}},"prebid":{"floors":{"floorRule":"banner|300x250|www.website1.com","floorRuleValue":20.01}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1","ver":"1.0","publisher":{"id":"5890"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.0.2.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891525,"cur":["USD"],"source":{"tid":"95d6643c-3da6-40a2-b9ca-12279393ffbf","ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"ext":{"prebid":{"aliases":{"adg":"adgeneration","andbeyond":"adkernel","appnexus-1":"appnexus","appnexus-2":"appnexus","districtm":"appnexus","districtmDMX":"dmx","pubmatic2":"pubmatic"},"channel":{"name":"app","version":""},"debug":true,"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":5,"increment":0.05},{"min":5,"max":10,"increment":0.1},{"min":10,"max":20,"increment":0.5}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false},"bidderparams":{"pubmatic":{"wiid":"42faaac0-9134-41c2-a283-77f1302d00ac"}},"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelweight":40,"debugweight":75,"modelversion":"version2","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":17.01,"*|*|www.website1.com":16.01,"*|300x250|*":11.01,"*|300x250|www.website1.com":100.01,"*|300x600|*":13.01,"*|300x600|www.website1.com":12.01,"*|728x90|*":15.01,"*|728x90|www.website1.com":14.01,"banner|*|*":90.01,"banner|*|www.website1.com":80.01,"banner|300x250|*":30.01,"banner|300x250|www.website1.com":20.01,"banner|300x600|*":50.01,"banner|300x600|www.website1.com":40.01,"banner|728x90|*":70.01,"banner|728x90|www.website1.com":60.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":true},"enabled":true,"skipped":false}}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + DealID: "3", + }, + }, + }, + currency: "USD", + }, + }, + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{}, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{}, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 0.5000 USD is less than bidFloor value 20.0100 USD for impression id some-impression-id-1 bidder appnexus", "bid rejected [bid ID: some-bid-1] reason: bid price value 1.2000 USD is less than bidFloor value 20.0100 USD for impression id some-impression-id-1 bidder pubmatic"}, + }, + { + name: "Should not enforce floors for deals, ext.prebid.floors.enforcement.floorDeals not provided", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"some-impression-id-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/43743431/DMDemo","bidfloor":20.01,"bidfloorcur":"USD","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/43743431/DMDemo@300x250","publisherId":"5890","wiid":"42faaac0-9134-41c2-a283-77f1302d00ac","wrapper":{"version":1,"profile":7255}},"prebid":{"floors":{"floorRule":"banner|300x250|www.website1.com","floorRuleValue":20.01}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1","ver":"1.0","publisher":{"id":"5890"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.0.2.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891525,"cur":["USD"],"source":{"tid":"95d6643c-3da6-40a2-b9ca-12279393ffbf","ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"ext":{"prebid":{"aliases":{"adg":"adgeneration","andbeyond":"adkernel","appnexus-1":"appnexus","appnexus-2":"appnexus","districtm":"appnexus","districtmDMX":"dmx","pubmatic2":"pubmatic"},"channel":{"name":"app","version":""},"debug":true,"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":5,"increment":0.05},{"min":5,"max":10,"increment":0.1},{"min":10,"max":20,"increment":0.5}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false},"bidderparams":{"pubmatic":{"wiid":"42faaac0-9134-41c2-a283-77f1302d00ac"}},"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelweight":40,"debugweight":75,"modelversion":"version2","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":17.01,"*|*|www.website1.com":16.01,"*|300x250|*":11.01,"*|300x250|www.website1.com":100.01,"*|300x600|*":13.01,"*|300x600|www.website1.com":12.01,"*|728x90|*":15.01,"*|728x90|www.website1.com":14.01,"banner|*|*":90.01,"banner|*|www.website1.com":80.01,"banner|300x250|*":30.01,"banner|300x250|www.website1.com":20.01,"banner|300x600|*":50.01,"banner|300x600|www.website1.com":40.01,"banner|728x90|*":70.01,"banner|728x90|www.website1.com":60.01},"default":21}]},"enforcement":{"enforcepbs":true},"enabled":true,"skipped":false}}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + }, + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{}, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 0.5000 USD is less than bidFloor value 20.0100 USD for impression id some-impression-id-1 bidder appnexus"}, + }, + { + name: "Should not enforce floors for deals, ext.prebid.floors.enforcement.floorDeals=false is set", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"some-impression-id-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/43743431/DMDemo","bidfloor":20.01,"bidfloorcur":"USD","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/43743431/DMDemo@300x250","publisherId":"5890","wiid":"42faaac0-9134-41c2-a283-77f1302d00ac","wrapper":{"version":1,"profile":7255}},"prebid":{"floors":{"floorRule":"banner|300x250|www.website1.com","floorRuleValue":20.01}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1","ver":"1.0","publisher":{"id":"5890"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.0.2.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891525,"cur":["USD"],"source":{"tid":"95d6643c-3da6-40a2-b9ca-12279393ffbf","ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"ext":{"prebid":{"aliases":{"adg":"adgeneration","andbeyond":"adkernel","appnexus-1":"appnexus","appnexus-2":"appnexus","districtm":"appnexus","districtmDMX":"dmx","pubmatic2":"pubmatic"},"channel":{"name":"app","version":""},"debug":true,"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":5,"increment":0.05},{"min":5,"max":10,"increment":0.1},{"min":10,"max":20,"increment":0.5}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false},"bidderparams":{"pubmatic":{"wiid":"42faaac0-9134-41c2-a283-77f1302d00ac"}},"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelweight":40,"debugweight":75,"modelversion":"version2","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":17.01,"*|*|www.website1.com":16.01,"*|300x250|*":11.01,"*|300x250|www.website1.com":100.01,"*|300x600|*":13.01,"*|300x600|www.website1.com":12.01,"*|728x90|*":15.01,"*|728x90|www.website1.com":14.01,"banner|*|*":90.01,"banner|*|www.website1.com":80.01,"banner|300x250|*":30.01,"banner|300x250|www.website1.com":20.01,"banner|300x600|*":50.01,"banner|300x600|www.website1.com":40.01,"banner|728x90|*":70.01,"banner|728x90|www.website1.com":60.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":false},"enabled":true,"skipped":false}}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + }, + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{}, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 0.5000 USD is less than bidFloor value 20.0100 USD for impression id some-impression-id-1 bidder appnexus"}, + }, + { + name: "Should not enforce floors for deals, ext.prebid.floors.enforcement.floorDeals=true and EnforceDealFloors = false from config", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"some-impression-id-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/43743431/DMDemo","bidfloor":20.01,"bidfloorcur":"USD","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/43743431/DMDemo@300x250","publisherId":"5890","wiid":"42faaac0-9134-41c2-a283-77f1302d00ac","wrapper":{"version":1,"profile":7255}},"prebid":{"floors":{"floorRule":"banner|300x250|www.website1.com","floorRuleValue":20.01}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1","ver":"1.0","publisher":{"id":"5890"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.0.2.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891525,"cur":["USD"],"source":{"tid":"95d6643c-3da6-40a2-b9ca-12279393ffbf","ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"ext":{"prebid":{"aliases":{"adg":"adgeneration","andbeyond":"adkernel","appnexus-1":"appnexus","appnexus-2":"appnexus","districtm":"appnexus","districtmDMX":"dmx","pubmatic2":"pubmatic"},"channel":{"name":"app","version":""},"debug":true,"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":5,"increment":0.05},{"min":5,"max":10,"increment":0.1},{"min":10,"max":20,"increment":0.5}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false},"bidderparams":{"pubmatic":{"wiid":"42faaac0-9134-41c2-a283-77f1302d00ac"}},"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelweight":40,"debugweight":75,"modelversion":"version2","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":17.01,"*|*|www.website1.com":16.01,"*|300x250|*":11.01,"*|300x250|www.website1.com":100.01,"*|300x600|*":13.01,"*|300x600|www.website1.com":12.01,"*|728x90|*":15.01,"*|728x90|www.website1.com":14.01,"banner|*|*":90.01,"banner|*|www.website1.com":80.01,"banner|300x250|*":30.01,"banner|300x250|www.website1.com":20.01,"banner|300x600|*":50.01,"banner|300x600|www.website1.com":40.01,"banner|728x90|*":70.01,"banner|728x90|www.website1.com":60.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":true},"enabled":true,"skipped":false}}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + }, + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: false, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{}, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 0.5000 USD is less than bidFloor value 20.0100 USD for impression id some-impression-id-1 bidder appnexus"}, + }, + { + name: "Should enforce floors when imp.bidfloor provided and req.ext.prebid not provided", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"some-impression-id-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/43743431/DMDemo","bidfloor":5.01,"bidfloorcur":"USD","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/43743431/DMDemo@300x250","publisherId":"5890","wiid":"42faaac0-9134-41c2-a283-77f1302d00ac","wrapper":{"version":1,"profile":7255}},"prebid":{"floors":{"floorRule":"banner|300x250|www.website1.com","floorRuleValue":20.01}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1","ver":"1.0","publisher":{"id":"5890"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.0.2.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891525,"cur":["USD"],"source":{"tid":"95d6643c-3da6-40a2-b9ca-12279393ffbf","ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"ext":{}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + }, + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{}, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 0.5000 USD is less than bidFloor value 5.0100 USD for impression id some-impression-id-1 bidder appnexus"}, + }, + { + name: "Should not enforce floors when imp.bidfloor not provided and req.ext.prebid not provided", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"some-impression-id-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/43743431/DMDemo","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/43743431/DMDemo@300x250","publisherId":"5890","wiid":"42faaac0-9134-41c2-a283-77f1302d00ac","wrapper":{"version":1,"profile":7255}},"prebid":{"floors":{"floorRule":"banner|300x250|www.website1.com","floorRuleValue":20.01}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1","ver":"1.0","publisher":{"id":"5890"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.0.2.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891525,"cur":["USD"],"source":{"tid":"95d6643c-3da6-40a2-b9ca-12279393ffbf","ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"ext":{}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + }, + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + }, + want1: nil, + }, + { + name: "Should not enforce floors when config flag Enabled = false", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"some-impression-id-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/43743431/DMDemo","bidfloor":20.01,"bidfloorcur":"USD","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/43743431/DMDemo@300x250","publisherId":"5890","wiid":"42faaac0-9134-41c2-a283-77f1302d00ac","wrapper":{"version":1,"profile":7255}},"prebid":{"floors":{"floorRule":"banner|300x250|www.website1.com","floorRuleValue":20.01}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1","ver":"1.0","publisher":{"id":"5890"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.0.2.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891525,"cur":["USD"],"source":{"tid":"95d6643c-3da6-40a2-b9ca-12279393ffbf","ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"ext":{"prebid":{"aliases":{"adg":"adgeneration","andbeyond":"adkernel","appnexus-1":"appnexus","appnexus-2":"appnexus","districtm":"appnexus","districtmDMX":"dmx","pubmatic2":"pubmatic"},"channel":{"name":"app","version":""},"debug":true,"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":5,"increment":0.05},{"min":5,"max":10,"increment":0.1},{"min":10,"max":20,"increment":0.5}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false},"bidderparams":{"pubmatic":{"wiid":"42faaac0-9134-41c2-a283-77f1302d00ac"}},"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelweight":40,"debugweight":75,"modelversion":"version2","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":17.01,"*|*|www.website1.com":16.01,"*|300x250|*":11.01,"*|300x250|www.website1.com":100.01,"*|300x600|*":13.01,"*|300x600|www.website1.com":12.01,"*|728x90|*":15.01,"*|728x90|www.website1.com":14.01,"banner|*|*":90.01,"banner|*|www.website1.com":80.01,"banner|300x250|*":30.01,"banner|300x250|www.website1.com":20.01,"banner|300x600|*":50.01,"banner|300x600|www.website1.com":40.01,"banner|728x90|*":70.01,"banner|728x90|www.website1.com":60.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":true},"enabled":true,"skipped":false}}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + DealID: "3", + }, + }, + }, + currency: "USD", + }, + }, + floor: config.PriceFloors{ + Enabled: false, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + DealID: "3", + }, + }, + }, + currency: "USD", + }, + }, + want1: nil, + }, + { + name: "Should not enforce floors when req.ext.prebid.floors.enabled = false ", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"some-impression-id-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/43743431/DMDemo","bidfloor":20.01,"bidfloorcur":"USD","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/43743431/DMDemo@300x250","publisherId":"5890","wiid":"42faaac0-9134-41c2-a283-77f1302d00ac","wrapper":{"version":1,"profile":7255}},"prebid":{"floors":{"floorRule":"banner|300x250|www.website1.com","floorRuleValue":20.01}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1","ver":"1.0","publisher":{"id":"5890"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.0.2.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891525,"cur":["USD"],"source":{"tid":"95d6643c-3da6-40a2-b9ca-12279393ffbf","ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"ext":{"prebid":{"aliases":{"adg":"adgeneration","andbeyond":"adkernel","appnexus-1":"appnexus","appnexus-2":"appnexus","districtm":"appnexus","districtmDMX":"dmx","pubmatic2":"pubmatic"},"channel":{"name":"app","version":""},"debug":true,"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":5,"increment":0.05},{"min":5,"max":10,"increment":0.1},{"min":10,"max":20,"increment":0.5}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false},"bidderparams":{"pubmatic":{"wiid":"42faaac0-9134-41c2-a283-77f1302d00ac"}},"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelweight":40,"debugweight":75,"modelversion":"version2","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":17.01,"*|*|www.website1.com":16.01,"*|300x250|*":11.01,"*|300x250|www.website1.com":100.01,"*|300x600|*":13.01,"*|300x600|www.website1.com":12.01,"*|728x90|*":15.01,"*|728x90|www.website1.com":14.01,"banner|*|*":90.01,"banner|*|www.website1.com":80.01,"banner|300x250|*":30.01,"banner|300x250|www.website1.com":20.01,"banner|300x600|*":50.01,"banner|300x600|www.website1.com":40.01,"banner|728x90|*":70.01,"banner|728x90|www.website1.com":60.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":true},"enabled":false,"skipped":false}}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + DealID: "3", + }, + }, + }, + currency: "USD", + }, + }, + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + DealID: "3", + }, + }, + }, + currency: "USD", + }, + }, + want1: nil, + }, + { + name: "Should not enforce floors when req.ext.prebid.floors.enforcement.enforcepbs = false ", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"some-impression-id-1","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/43743431/DMDemo","bidfloor":20.01,"bidfloorcur":"USD","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/43743431/DMDemo@300x250","publisherId":"5890","wiid":"42faaac0-9134-41c2-a283-77f1302d00ac","wrapper":{"version":1,"profile":7255}},"prebid":{"floors":{"floorRule":"banner|300x250|www.website1.com","floorRuleValue":20.01}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1","ver":"1.0","publisher":{"id":"5890"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.0.2.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891525,"cur":["USD"],"source":{"tid":"95d6643c-3da6-40a2-b9ca-12279393ffbf","ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"ext":{"prebid":{"aliases":{"adg":"adgeneration","andbeyond":"adkernel","appnexus-1":"appnexus","appnexus-2":"appnexus","districtm":"appnexus","districtmDMX":"dmx","pubmatic2":"pubmatic"},"channel":{"name":"app","version":""},"debug":true,"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":5,"increment":0.05},{"min":5,"max":10,"increment":0.1},{"min":10,"max":20,"increment":0.5}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false},"bidderparams":{"pubmatic":{"wiid":"42faaac0-9134-41c2-a283-77f1302d00ac"}},"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelweight":40,"debugweight":75,"modelversion":"version2","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":17.01,"*|*|www.website1.com":16.01,"*|300x250|*":11.01,"*|300x250|www.website1.com":100.01,"*|300x600|*":13.01,"*|300x600|www.website1.com":12.01,"*|728x90|*":15.01,"*|728x90|www.website1.com":14.01,"banner|*|*":90.01,"banner|*|www.website1.com":80.01,"banner|300x250|*":30.01,"banner|300x250|www.website1.com":20.01,"banner|300x600|*":50.01,"banner|300x600|www.website1.com":40.01,"banner|728x90|*":70.01,"banner|728x90|www.website1.com":60.01},"default":21}]},"enforcement":{"enforcepbs":false,"floordeals":false}}}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + DealID: "3", + }, + }, + }, + currency: "USD", + }, + }, + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + DealID: "1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + DealID: "3", + }, + }, + }, + currency: "USD", + }, + }, + want1: nil, + }, + { + name: "Should not enforce floors for deals as req.ext.prebid.floors not provided and imp.bidfloor provided", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"some-impression-id-1","bidfloor":20.01,"bidfloorcur":"USD","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/43743431/DMDemo","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/43743431/DMDemo@300x250","publisherId":"5890","wiid":"42faaac0-9134-41c2-a283-77f1302d00ac","wrapper":{"version":1,"profile":7255}},"prebid":{"floors":{"floorRule":"banner|300x250|www.website1.com","floorRuleValue":20.01}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1","ver":"1.0","publisher":{"id":"5890"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.0.2.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891525,"cur":["USD"],"source":{"tid":"95d6643c-3da6-40a2-b9ca-12279393ffbf","ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"ext":{"prebid":{"aliases":{"adg":"adgeneration","andbeyond":"adkernel","appnexus-1":"appnexus","appnexus-2":"appnexus","districtm":"appnexus","districtmDMX":"dmx","pubmatic2":"pubmatic"},"channel":{"name":"app","version":""},"debug":true,"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":5,"increment":0.05},{"min":5,"max":10,"increment":0.1},{"min":10,"max":20,"increment":0.5}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false},"bidderparams":{"pubmatic":{"wiid":"42faaac0-9134-41c2-a283-77f1302d00ac"}}}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + DealID: "2", + }, + }, + }, + currency: "USD", + }, + }, + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{}, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + DealID: "2", + }, + }, + }, + + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-1] reason: bid price value 1.2000 USD is less than bidFloor value 20.0100 USD for impression id some-impression-id-1 bidder pubmatic"}, + }, + { + name: "Should enforce floors as req.ext.prebid.floors not provided and imp.bidfloor provided", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"some-impression-id-1","bidfloor":20.01,"bidfloorcur":"USD","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/43743431/DMDemo","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/43743431/DMDemo@300x250","publisherId":"5890","wiid":"42faaac0-9134-41c2-a283-77f1302d00ac","wrapper":{"version":1,"profile":7255}},"prebid":{"floors":{"floorRule":"banner|300x250|www.website1.com","floorRuleValue":20.01}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1","ver":"1.0","publisher":{"id":"5890"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.0.2.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891525,"cur":["USD"],"source":{"tid":"95d6643c-3da6-40a2-b9ca-12279393ffbf","ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"ext":{"prebid":{"aliases":{"adg":"adgeneration","andbeyond":"adkernel","appnexus-1":"appnexus","appnexus-2":"appnexus","districtm":"appnexus","districtmDMX":"dmx","pubmatic2":"pubmatic"},"channel":{"name":"app","version":""},"debug":true,"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":5,"increment":0.05},{"min":5,"max":10,"increment":0.1},{"min":10,"max":20,"increment":0.5}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false},"bidderparams":{"pubmatic":{"wiid":"42faaac0-9134-41c2-a283-77f1302d00ac"}}}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + }, + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{}, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{}, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 0.5000 USD is less than bidFloor value 20.0100 USD for impression id some-impression-id-1 bidder appnexus", "bid rejected [bid ID: some-bid-1] reason: bid price value 1.2000 USD is less than bidFloor value 20.0100 USD for impression id some-impression-id-1 bidder pubmatic"}, + }, + { + name: "Should enforce floors as req.ext.prebid.floors not provided and imp.bidfloor provided", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"some-impression-id-1","bidfloor":20.01,"bidfloorcur":"USD","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/43743431/DMDemo","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/43743431/DMDemo@300x250","publisherId":"5890","wiid":"42faaac0-9134-41c2-a283-77f1302d00ac","wrapper":{"version":1,"profile":7255}},"prebid":{"floors":{"floorRule":"banner|300x250|www.website1.com","floorRuleValue":20.01}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1","ver":"1.0","publisher":{"id":"5890"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.0.2.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891525,"cur":["USD"],"source":{"tid":"95d6643c-3da6-40a2-b9ca-12279393ffbf","ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}},"ext":{"prebid":{"floors": {}}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + }, + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{}, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{}, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 0.5000 USD is less than bidFloor value 20.0100 USD for impression id some-impression-id-1 bidder appnexus", "bid rejected [bid ID: some-bid-1] reason: bid price value 1.2000 USD is less than bidFloor value 20.0100 USD for impression id some-impression-id-1 bidder pubmatic"}, + }, + { + name: "Should enforce floors as req.ext not provided and imp.bidfloor provided", + args: args{ + r: func() *AuctionRequest { + var wrapper openrtb_ext.RequestWrapper + strReq := `{"id":"95d6643c-3da6-40a2-b9ca-12279393ffbf","imp":[{"id":"some-impression-id-1","bidfloor":20.01,"bidfloorcur":"USD","banner":{"format":[{"w":300,"h":250}],"pos":7,"api":[5,6,7]},"displaymanager":"PubMatic_OpenBid_SDK","displaymanagerver":"1.4.0","instl":1,"tagid":"/43743431/DMDemo","secure":0,"ext":{"appnexus-1":{"placementId":234234},"appnexus-2":{"placementId":9880618},"pubmatic":{"adSlot":"/43743431/DMDemo@300x250","publisherId":"5890","wiid":"42faaac0-9134-41c2-a283-77f1302d00ac","wrapper":{"version":1,"profile":7255}},"prebid":{"floors":{"floorRule":"banner|300x250|www.website1.com","floorRuleValue":20.01}}}}],"app":{"name":"OpenWrapperSample","bundle":"com.pubmatic.openbid.app","domain":"www.website1.com","storeurl":"https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?appnexus_banner_fixedbid=1&fixedbid=1","ver":"1.0","publisher":{"id":"5890"}},"device":{"ua":"Mozilla/5.0 (Linux; Android 9; Android SDK built for x86 Build/PSR1.180720.075; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36","geo":{"lat":37.421998333333335,"lon":-122.08400000000002,"type":1},"lmt":0,"ip":"192.0.2.1","devicetype":4,"make":"Google","model":"Android SDK built for x86","os":"Android","osv":"9","h":1794,"w":1080,"pxratio":2.625,"js":1,"language":"en","carrier":"Android","mccmnc":"310-260","connectiontype":6,"ifa":"07c387f2-e030-428f-8336-42f682150759"},"user":{},"at":1,"tmax":1891525,"cur":["USD"],"source":{"tid":"95d6643c-3da6-40a2-b9ca-12279393ffbf","ext":{"omidpn":"PubMatic","omidpv":"1.2.11-Pubmatic"}}}` + _ = json.Unmarshal([]byte(strReq), &wrapper) + ar := AuctionRequest{BidRequestWrapper: &wrapper} + return &ar + }(), + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-1", + Price: 1.2, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{ + { + bid: &openrtb2.Bid{ + ID: "some-bid-11", + Price: 0.5, + ImpID: "some-impression-id-1", + }, + }, + }, + currency: "USD", + }, + }, + floor: config.PriceFloors{ + Enabled: true, + EnforceFloorsRate: 100, + EnforceDealFloors: true, + }, + conversions: convert{}, + responseDebugAllow: true, + }, + want: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "pubmatic": { + bids: []*pbsOrtbBid{}, + currency: "USD", + }, + "appnexus": { + bids: []*pbsOrtbBid{}, + currency: "USD", + }, + }, + want1: []string{"bid rejected [bid ID: some-bid-11] reason: bid price value 0.5000 USD is less than bidFloor value 20.0100 USD for impression id some-impression-id-1 bidder appnexus", "bid rejected [bid ID: some-bid-1] reason: bid price value 1.2000 USD is less than bidFloor value 20.0100 USD for impression id some-impression-id-1 bidder pubmatic"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + seatbid, errs, _ := enforceFloors(tt.args.r, tt.args.seatBids, tt.args.floor, tt.args.conversions, tt.args.responseDebugAllow) + for biderName, seat := range seatbid { + if len(seat.bids) != len(tt.want[biderName].bids) { + t.Errorf("enforceFloors() got = %v bids, want %v bids for BidderCode = %v ", len(seat.bids), len(tt.want[biderName].bids), biderName) + } + } + assert.Equal(t, tt.want1, ErrToString(errs)) + }) + } +} diff --git a/exchange/price_granularity.go b/exchange/price_granularity.go index 242d420f1fc..52ea3492838 100644 --- a/exchange/price_granularity.go +++ b/exchange/price_granularity.go @@ -24,7 +24,8 @@ func GetPriceBucket(cpm float64, config openrtb_ext.PriceGranularity) string { } } - if cpm > bucketMax { + //OTT-603: Adding Test Price Granularity + if config.Test || cpm > bucketMax { // We are over max, just return that cpmStr = strconv.FormatFloat(bucketMax, 'f', precision, 64) } else if increment > 0 { diff --git a/exchange/price_granularity_test.go b/exchange/price_granularity_test.go index 13840838ba7..7598ed310f4 100644 --- a/exchange/price_granularity_test.go +++ b/exchange/price_granularity_test.go @@ -14,6 +14,7 @@ func TestGetPriceBucketString(t *testing.T) { high := openrtb_ext.PriceGranularityFromString("high") auto := openrtb_ext.PriceGranularityFromString("auto") dense := openrtb_ext.PriceGranularityFromString("dense") + testPG := openrtb_ext.PriceGranularityFromString("testpg") custom1 := openrtb_ext.PriceGranularity{ Precision: 2, Ranges: []openrtb_ext.GranularityRange{ @@ -50,6 +51,7 @@ func TestGetPriceBucketString(t *testing.T) { {"high", high, "1.87"}, {"auto", auto, "1.85"}, {"dense", dense, "1.87"}, + {"testpg", testPG, "50.00"}, {"custom1", custom1, "1.86"}, }, }, @@ -62,6 +64,7 @@ func TestGetPriceBucketString(t *testing.T) { {"high", high, "5.72"}, {"auto", auto, "5.70"}, {"dense", dense, "5.70"}, + {"testpg", testPG, "50.00"}, {"custom1", custom1, "5.70"}, }, }, @@ -122,6 +125,13 @@ func TestGetPriceBucketString(t *testing.T) { cpm: math.MaxFloat64, testCases: []aTest{{"low", low, "5.00"}}, }, + { + groupDesc: "cpm above max test price granularity value", + cpm: 60, + testCases: []aTest{ + {"testpg", testPG, "50.00"}, + }, + }, } for _, testGroup := range testGroups { diff --git a/exchange/targeting.go b/exchange/targeting.go index 67cb534d12e..04c43f7baa7 100644 --- a/exchange/targeting.go +++ b/exchange/targeting.go @@ -76,7 +76,7 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMappi if len(categoryMapping) > 0 { targData.addKeys(targets, openrtb_ext.HbCategoryDurationKey, categoryMapping[topBidPerBidder.bid.ID], bidderName, isOverallWinner, truncateTargetAttr) } - + targData.addBidderKeys(targets, topBidPerBidder.bidTargets) topBidPerBidder.bidTargets = targets } } @@ -106,3 +106,11 @@ func makeHbSize(bid *openrtb2.Bid) string { } return "" } + +func (targData *targetData) addBidderKeys(keys map[string]string, bidderKeys map[string]string) { + if targData.includeBidderKeys { + for index, element := range bidderKeys { + keys[index] = element + } + } +} diff --git a/exchange/utils.go b/exchange/utils.go index 96e5e2a9741..6ac36e53fbb 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -73,11 +73,13 @@ func cleanOpenRTBRequests(ctx context.Context, var allBidderRequests []BidderRequest allBidderRequests, errs = getAuctionBidderRequests(auctionReq, requestExt, bidderToSyncerKey, impsByBidder, aliases, hostSChainNode) - + bidderNameToBidderReq := buildBidResponseRequest(req.BidRequest, bidderImpWithBidResp, aliases, auctionReq.BidderImpReplaceImpID) //this function should be executed after getAuctionBidderRequests allBidderRequests = mergeBidderRequests(allBidderRequests, bidderNameToBidderReq) + updateContentObjectForBidder(allBidderRequests, requestExt) + gdprSignal, err := extractGDPR(req.BidRequest) if err != nil { errs = append(errs, err) @@ -441,6 +443,10 @@ func createSanitizedImpExt(impExt, impExtPrebid map[string]json.RawMessage) (map sanitizedImpExt[openrtb_ext.TIDKey] = v } + if v, exists := impExt[string(openrtb_ext.CommerceParamKey)]; exists { + sanitizedImpExt[openrtb_ext.CommerceParamKey] = v + } + return sanitizedImpExt, nil } @@ -472,7 +478,8 @@ func isSpecialField(bidder string) bool { bidder == openrtb_ext.SKAdNExtKey || bidder == openrtb_ext.GPIDKey || bidder == openrtb_ext.PrebidExtKey || - bidder == openrtb_ext.TIDKey + bidder == openrtb_ext.TIDKey || + bidder == openrtb_ext.CommerceParamKey } // prepareUser changes req.User so that it's ready for the given bidder. diff --git a/exchange/utils_ow.go b/exchange/utils_ow.go new file mode 100644 index 00000000000..f79ba4a25e3 --- /dev/null +++ b/exchange/utils_ow.go @@ -0,0 +1,259 @@ +package exchange + +import ( + "encoding/json" + + "github.com/golang/glog" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func JLogf(msg string, obj interface{}) { + if glog.V(3) { + data, _ := json.Marshal(obj) + glog.Infof("[OPENWRAP] %v:%v", msg, string(data)) + } +} + +// updateContentObjectForBidder updates the content object for each bidder based on content transparency rules +func updateContentObjectForBidder(allBidderRequests []BidderRequest, requestExt *openrtb_ext.ExtRequest) { + if requestExt == nil || requestExt.Prebid.Transparency == nil || requestExt.Prebid.Transparency.Content == nil { + return + } + + rules := requestExt.Prebid.Transparency.Content + + if len(rules) == 0 { + return + } + + var contentObject *openrtb2.Content + isApp := false + bidderRequest := allBidderRequests[0] + if bidderRequest.BidRequest.App != nil && bidderRequest.BidRequest.App.Content != nil { + contentObject = bidderRequest.BidRequest.App.Content + isApp = true + } else if bidderRequest.BidRequest.Site != nil && bidderRequest.BidRequest.Site.Content != nil { + contentObject = bidderRequest.BidRequest.Site.Content + } else { + return + } + + // Dont send content object if no rule and default is not present + var defaultRule = openrtb_ext.TransparencyRule{} + if rule, ok := rules["default"]; ok { + defaultRule = rule + } + + for _, bidderRequest := range allBidderRequests { + var newContentObject *openrtb2.Content + + rule, ok := rules[string(bidderRequest.BidderName)] + if !ok { + rule = defaultRule + } + + if len(rule.Keys) != 0 { + newContentObject = createNewContentObject(contentObject, rule.Include, rule.Keys) + } else if rule.Include { + newContentObject = contentObject + } + deepCopyContentObj(bidderRequest.BidRequest, newContentObject, isApp) + } +} + +func deepCopyContentObj(request *openrtb2.BidRequest, contentObject *openrtb2.Content, isApp bool) { + if isApp { + app := *request.App + app.Content = contentObject + request.App = &app + } else { + site := *request.Site + site.Content = contentObject + request.Site = &site + } +} + +// func createNewContentObject(contentObject *openrtb2.Content, include bool, keys []string) *openrtb2.Content { +// if include { +// return includeKeys(contentObject, keys) +// } +// return excludeKeys(contentObject, keys) + +// } + +// func excludeKeys(contentObject *openrtb2.Content, keys []string) *openrtb2.Content { +// newContentObject := *contentObject + +// keyMap := make(map[string]struct{}, 1) +// for _, key := range keys { +// keyMap[key] = struct{}{} +// } + +// rt := reflect.TypeOf(newContentObject) +// for i := 0; i < rt.NumField(); i++ { +// key := strings.Split(rt.Field(i).Tag.Get("json"), ",")[0] // remove omitempty, etc +// if _, ok := keyMap[key]; ok { +// reflect.ValueOf(&newContentObject).Elem().FieldByName(rt.Field(i).Name).Set(reflect.Zero(rt.Field(i).Type)) +// } +// } + +// return &newContentObject +// } + +// func includeKeys(contentObject *openrtb2.Content, keys []string) *openrtb2.Content { +// newContentObject := openrtb2.Content{} +// v := reflect.ValueOf(contentObject).Elem() +// keyMap := make(map[string]struct{}, 1) +// for _, key := range keys { +// keyMap[key] = struct{}{} +// } + +// rt := reflect.TypeOf(newContentObject) +// rvElem := reflect.ValueOf(&newContentObject).Elem() +// for i := 0; i < rt.NumField(); i++ { +// field := rt.Field(i) +// key := strings.Split(field.Tag.Get("json"), ",")[0] // remove omitempty, etc +// if _, ok := keyMap[key]; ok { +// rvElem.FieldByName(field.Name).Set(v.FieldByName(field.Name)) +// } +// } + +// return &newContentObject +// } + +func createNewContentObject(contentObject *openrtb2.Content, include bool, keys []string) *openrtb2.Content { + newContentObject := &openrtb2.Content{} + if !include { + *newContentObject = *contentObject + for _, key := range keys { + + switch key { + case "id": + newContentObject.ID = "" + case "episode": + newContentObject.Episode = 0 + case "title": + newContentObject.Title = "" + case "series": + newContentObject.Series = "" + case "season": + newContentObject.Season = "" + case "artist": + newContentObject.Artist = "" + case "genre": + newContentObject.Genre = "" + case "album": + newContentObject.Album = "" + case "isrc": + newContentObject.ISRC = "" + case "producer": + newContentObject.Producer = nil + case "url": + newContentObject.URL = "" + case "cat": + newContentObject.Cat = nil + case "prodq": + newContentObject.ProdQ = nil + case "videoquality": + newContentObject.VideoQuality = nil + case "context": + newContentObject.Context = 0 + case "contentrating": + newContentObject.ContentRating = "" + case "userrating": + newContentObject.UserRating = "" + case "qagmediarating": + newContentObject.QAGMediaRating = 0 + case "keywords": + newContentObject.Keywords = "" + case "livestream": + newContentObject.LiveStream = 0 + case "sourcerelationship": + newContentObject.SourceRelationship = 0 + case "len": + newContentObject.Len = 0 + case "language": + newContentObject.Language = "" + case "embeddable": + newContentObject.Embeddable = 0 + case "data": + newContentObject.Data = nil + case "ext": + newContentObject.Ext = nil + } + + } + return newContentObject + } + + for _, key := range keys { + switch key { + case "id": + newContentObject.ID = contentObject.ID + case "episode": + newContentObject.Episode = contentObject.Episode + case "title": + newContentObject.Title = contentObject.Title + case "series": + newContentObject.Series = contentObject.Series + case "season": + newContentObject.Season = contentObject.Season + case "artist": + newContentObject.Artist = contentObject.Artist + case "genre": + newContentObject.Genre = contentObject.Genre + case "album": + newContentObject.Album = contentObject.Album + case "isrc": + newContentObject.ISRC = contentObject.ISRC + case "producer": + if contentObject.Producer != nil { + producer := *contentObject.Producer + newContentObject.Producer = &producer + } + case "url": + newContentObject.URL = contentObject.URL + case "cat": + newContentObject.Cat = contentObject.Cat + case "prodq": + if contentObject.ProdQ != nil { + prodQ := *contentObject.ProdQ + newContentObject.ProdQ = &prodQ + } + case "videoquality": + if contentObject.VideoQuality != nil { + videoQuality := *contentObject.VideoQuality + newContentObject.VideoQuality = &videoQuality + } + case "context": + newContentObject.Context = contentObject.Context + case "contentrating": + newContentObject.ContentRating = contentObject.ContentRating + case "userrating": + newContentObject.UserRating = contentObject.UserRating + case "qagmediarating": + newContentObject.QAGMediaRating = contentObject.QAGMediaRating + case "keywords": + newContentObject.Keywords = contentObject.Keywords + case "livestream": + newContentObject.LiveStream = contentObject.LiveStream + case "sourcerelationship": + newContentObject.SourceRelationship = contentObject.SourceRelationship + case "len": + newContentObject.Len = contentObject.Len + case "language": + newContentObject.Language = contentObject.Language + case "embeddable": + newContentObject.Embeddable = contentObject.Embeddable + case "data": + if contentObject.Data != nil { + newContentObject.Data = contentObject.Data + } + case "ext": + newContentObject.Ext = contentObject.Ext + } + } + + return newContentObject +} diff --git a/exchange/utils_ow_test.go b/exchange/utils_ow_test.go new file mode 100644 index 00000000000..dff7b369792 --- /dev/null +++ b/exchange/utils_ow_test.go @@ -0,0 +1,1049 @@ +package exchange + +import ( + "testing" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func Test_updateContentObjectForBidder(t *testing.T) { + + createBidderRequest := func(BidRequest *openrtb2.BidRequest) []BidderRequest { + newReq := *BidRequest + newReq.ID = "2" + return []BidderRequest{{ + BidderName: "pubmatic", + BidRequest: BidRequest, + }, + { + BidderName: "appnexus", + BidRequest: &newReq, + }, + } + } + + type args struct { + BidRequest *openrtb2.BidRequest + requestExt *openrtb_ext.ExtRequest + } + tests := []struct { + name string + args args + wantedAllBidderRequests []BidderRequest + }{ + { + name: "No Transparency Object", + args: args{ + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + }, + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + }, + }, + }, + }, + }, + }, + { + name: "No Content Object in App/Site", + args: args{ + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + }, + Site: &openrtb2.Site{ + ID: "1", + Name: "Site1", + }, + }, + + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{ + "pubmatic": { + Include: true, + }, + }, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + }, + Site: &openrtb2.Site{ + ID: "1", + Name: "Site1", + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + }, + Site: &openrtb2.Site{ + ID: "1", + Name: "Site1", + }, + }, + }, + }, + }, + { + name: "No partner/ default rules in tranpsarency", + args: args{ + BidRequest: &openrtb2.BidRequest{ + ID: "1", + Site: &openrtb2.Site{ + ID: "1", + Name: "Test", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{}, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + Site: &openrtb2.Site{ + ID: "1", + Name: "Test", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + Site: &openrtb2.Site{ + ID: "1", + Name: "Test", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + }, + }, + }, + { + name: "Include All keys for bidder", + args: args{ + + BidRequest: &openrtb2.BidRequest{ + ID: "1", + Site: &openrtb2.Site{ + ID: "1", + Name: "Test", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{ + "pubmatic": { + Include: true, + Keys: []string{}, + }, + "appnexus": { + Include: false, + Keys: []string{}, + }, + }, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + Site: &openrtb2.Site{ + ID: "1", + Name: "Test", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + Site: &openrtb2.Site{ + ID: "1", + Name: "Test", + }, + }, + }, + }, + }, + { + name: "Exclude All keys for pubmatic bidder", + args: args{ + + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{ + "pubmatic": { + Include: false, + Keys: []string{}, + }, + "appnexus": { + Include: true, + Keys: []string{}, + }, + }, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + }, + }, + }, + { + name: "Include title field for pubmatic bidder", + args: args{ + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{ + "pubmatic": { + Include: true, + Keys: []string{"title"}, + }, + "appnexus": { + Include: false, + Keys: []string{"genre"}, + }, + }, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + }, + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + }, + }, + }, + }, + }, + }, + { + name: "Exclude title field for pubmatic bidder", + args: args{ + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{ + "pubmatic": { + Include: false, + Keys: []string{"title"}, + }, + "appnexus": { + Include: true, + Keys: []string{"genre"}, + }, + }, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Genre: "Genre1", + }, + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Genre: "Genre1", + }, + }, + }, + }, + }, + }, + { + name: "Use default rule for pubmatic bidder", + args: args{ + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + Series: "Series1", + Season: "Season1", + Artist: "Artist1", + Album: "Album1", + ISRC: "isrc1", + Producer: &openrtb2.Producer{}, + }, + }, + }, + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{ + "default": { + Include: true, + Keys: []string{ + "id", "episode", "series", "season", "artist", "genre", "album", "isrc", "producer", "url", "cat", "prodq", "videoquality", "context", "contentrating", "userrating", "qagmediarating", "livestream", "sourcerelationship", "len", "language", "embeddable", "data", "ext"}, + }, + "pubmatic": { + Include: true, + Keys: []string{"title", "genre"}, + }, + }, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Genre: "Genre1", + Series: "Series1", + Season: "Season1", + Artist: "Artist1", + Album: "Album1", + ISRC: "isrc1", + Producer: &openrtb2.Producer{}, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + allBidderRequests := createBidderRequest(tt.args.BidRequest) + updateContentObjectForBidder(allBidderRequests, tt.args.requestExt) + assert.Equal(t, tt.wantedAllBidderRequests, allBidderRequests, tt.name) + }) + } +} + +func Benchmark_updateContentObjectForBidder(b *testing.B) { + + createBidderRequest := func(BidRequest *openrtb2.BidRequest) []BidderRequest { + newReq := *BidRequest + newReq.ID = "2" + return []BidderRequest{{ + BidderName: "pubmatic", + BidRequest: BidRequest, + }, + { + BidderName: "appnexus", + BidRequest: &newReq, + }, + } + } + + type args struct { + BidRequest *openrtb2.BidRequest + requestExt *openrtb_ext.ExtRequest + } + tests := []struct { + name string + args args + wantedAllBidderRequests []BidderRequest + }{ + { + name: "No Transparency Object", + args: args{ + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + }, + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + }, + }, + }, + }, + }, + }, + { + name: "No Content Object in App/Site", + args: args{ + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + }, + Site: &openrtb2.Site{ + ID: "1", + Name: "Site1", + }, + }, + + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{ + "pubmatic": { + Include: true, + }, + }, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + }, + Site: &openrtb2.Site{ + ID: "1", + Name: "Site1", + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + }, + Site: &openrtb2.Site{ + ID: "1", + Name: "Site1", + }, + }, + }, + }, + }, + { + name: "No partner/ default rules in tranpsarency", + args: args{ + BidRequest: &openrtb2.BidRequest{ + ID: "1", + Site: &openrtb2.Site{ + ID: "1", + Name: "Test", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{}, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + Site: &openrtb2.Site{ + ID: "1", + Name: "Test", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + Site: &openrtb2.Site{ + ID: "1", + Name: "Test", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + }, + }, + }, + { + name: "Include All keys for bidder", + args: args{ + + BidRequest: &openrtb2.BidRequest{ + ID: "1", + Site: &openrtb2.Site{ + ID: "1", + Name: "Test", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{ + "pubmatic": { + Include: true, + Keys: []string{}, + }, + "appnexus": { + Include: false, + Keys: []string{}, + }, + }, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + Site: &openrtb2.Site{ + ID: "1", + Name: "Test", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + Site: &openrtb2.Site{ + ID: "1", + Name: "Test", + }, + }, + }, + }, + }, + { + name: "Exclude All keys for pubmatic bidder", + args: args{ + + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{ + "pubmatic": { + Include: false, + Keys: []string{}, + }, + "appnexus": { + Include: true, + Keys: []string{}, + }, + }, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + }, + }, + }, + { + name: "Include title field for pubmatic bidder", + args: args{ + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{ + "pubmatic": { + Include: true, + Keys: []string{"title"}, + }, + "appnexus": { + Include: false, + Keys: []string{"genre"}, + }, + }, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + }, + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + }, + }, + }, + }, + }, + }, + { + name: "Exclude title field for pubmatic bidder", + args: args{ + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{ + "pubmatic": { + Include: false, + Keys: []string{"title"}, + }, + "appnexus": { + Include: true, + Keys: []string{"genre"}, + }, + }, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Genre: "Genre1", + }, + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Genre: "Genre1", + }, + }, + }, + }, + }, + }, + { + name: "Use default rule for pubmatic bidder", + args: args{ + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + Series: "Series1", + Season: "Season1", + Artist: "Artist1", + Album: "Album1", + ISRC: "isrc1", + Producer: &openrtb2.Producer{}, + }, + }, + }, + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Transparency: &openrtb_ext.TransparencyExt{ + Content: map[string]openrtb_ext.TransparencyRule{ + "default": { + Include: true, + Keys: []string{ + "id", "episode", "series", "season", "artist", "genre", "album", "isrc", "producer", "url", "cat", "prodq", "videoquality", "context", "contentrating", "userrating", "qagmediarating", "livestream", "sourcerelationship", "len", "language", "embeddable", "data", "ext"}, + }, + "pubmatic": { + Include: true, + Keys: []string{"title", "genre"}, + }, + }, + }, + }, + }, + }, + wantedAllBidderRequests: []BidderRequest{ + { + BidderName: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + ID: "1", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Title: "Title1", + Genre: "Genre1", + }, + }, + }, + }, + { + BidderName: "appnexus", + BidRequest: &openrtb2.BidRequest{ + ID: "2", + App: &openrtb2.App{ + ID: "1", + Name: "Test", + Bundle: "com.pubmatic.app", + Content: &openrtb2.Content{ + Genre: "Genre1", + Series: "Series1", + Season: "Season1", + Artist: "Artist1", + Album: "Album1", + ISRC: "isrc1", + Producer: &openrtb2.Producer{}, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + b.Run(tt.name, func(b *testing.B) { + allBidderRequests := createBidderRequest(tt.args.BidRequest) + for i := 0; i < b.N; i++ { + updateContentObjectForBidder(allBidderRequests, tt.args.requestExt) + } + //assert.Equal(t, tt.wantedAllBidderRequests, allBidderRequests, tt.name) + }) + } +} diff --git a/floors/enforce.go b/floors/enforce.go new file mode 100644 index 00000000000..42fa593377a --- /dev/null +++ b/floors/enforce.go @@ -0,0 +1,45 @@ +package floors + +import ( + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func RequestHasFloors(bidRequest *openrtb2.BidRequest) bool { + for i := range bidRequest.Imp { + if bidRequest.Imp[i].BidFloor > 0 { + return true + } + } + return false +} + +func ShouldEnforce(bidRequest *openrtb2.BidRequest, floorExt *openrtb_ext.PriceFloorRules, configEnforceRate int, f func(int) int) bool { + + if floorExt != nil && floorExt.Skipped != nil && *floorExt.Skipped { + return !*floorExt.Skipped + } + + if floorExt != nil && floorExt.Enforcement != nil && floorExt.Enforcement.EnforcePBS != nil && !*floorExt.Enforcement.EnforcePBS { + return *floorExt.Enforcement.EnforcePBS + } + + if floorExt != nil && floorExt.Enforcement != nil && floorExt.Enforcement.EnforceRate > 0 { + configEnforceRate = floorExt.Enforcement.EnforceRate + } + + shouldEnforce := configEnforceRate > f(enforceRateMax) + if floorExt == nil { + floorExt = new(openrtb_ext.PriceFloorRules) + } + + if floorExt.Enforcement == nil { + floorExt.Enforcement = new(openrtb_ext.PriceFloorEnforcement) + } + + if floorExt.Enforcement.EnforcePBS == nil { + floorExt.Enforcement.EnforcePBS = new(bool) + } + *floorExt.Enforcement.EnforcePBS = shouldEnforce + return shouldEnforce +} diff --git a/floors/enforce_test.go b/floors/enforce_test.go new file mode 100644 index 00000000000..dfdccac729a --- /dev/null +++ b/floors/enforce_test.go @@ -0,0 +1,196 @@ +package floors + +import ( + "testing" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func getFalse() *bool { + b := false + return &b +} + +func getTrue() *bool { + b := true + return &b +} + +func TestShouldEnforceFloors(t *testing.T) { + type args struct { + bidRequest *openrtb2.BidRequest + floorExt *openrtb_ext.PriceFloorRules + configEnforceRate int + f func(int) int + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "No enfocement of floors when enforcePBS is false", + args: args{ + bidRequest: func() *openrtb2.BidRequest { + r := openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + BidFloor: 2.2, + BidFloorCur: "USD", + }, + { + BidFloor: 0, + BidFloorCur: "USD", + }, + }, + } + return &r + }(), + floorExt: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: getFalse(), + }, + Skipped: getFalse(), + }, + configEnforceRate: 10, + f: func(n int) int { + return n + }, + }, + want: false, + }, + { + name: "No enfocement of floors when enforcePBS is true but enforce rate is low", + args: args{ + bidRequest: func() *openrtb2.BidRequest { + r := openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + BidFloor: 2.2, + BidFloorCur: "USD", + }, + { + BidFloor: 0, + BidFloorCur: "USD", + }, + }, + } + return &r + }(), + floorExt: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: getTrue(), + }, + Skipped: getFalse(), + }, + configEnforceRate: 10, + f: func(n int) int { + return n + }, + }, + want: false, + }, + { + name: "No enfocement of floors when enforcePBS is true but enforce rate is low in incoming request", + args: args{ + bidRequest: func() *openrtb2.BidRequest { + r := openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + BidFloor: 2.2, + BidFloorCur: "USD", + }, + { + BidFloor: 0, + BidFloorCur: "USD", + }, + }, + } + return &r + }(), + floorExt: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: getTrue(), + EnforceRate: 10, + }, + Skipped: getFalse(), + }, + configEnforceRate: 100, + f: func(n int) int { + return n + }, + }, + want: false, + }, + { + name: "No Enfocement of floors when skipped is true, non zero value of bidfloor in imp", + args: args{ + bidRequest: func() *openrtb2.BidRequest { + r := openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + BidFloor: 2.2, + BidFloorCur: "USD", + }, + { + BidFloor: 0, + BidFloorCur: "USD", + }, + }, + } + return &r + }(), + floorExt: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: getTrue(), + }, + Skipped: getTrue(), + }, + configEnforceRate: 98, + f: func(n int) int { + return n - 5 + }, + }, + want: false, + }, + { + name: "No enfocement of floors when skipped is true, zero value of bidfloor in imp", + args: args{ + bidRequest: func() *openrtb2.BidRequest { + r := openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + BidFloor: 0, + BidFloorCur: "USD", + }, + { + BidFloor: 0, + BidFloorCur: "USD", + }, + }, + } + return &r + }(), + floorExt: &openrtb_ext.PriceFloorRules{ + Enforcement: &openrtb_ext.PriceFloorEnforcement{ + EnforcePBS: getTrue(), + }, + Skipped: getTrue(), + }, + configEnforceRate: 98, + f: func(n int) int { + return n - 5 + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ShouldEnforce(tt.args.bidRequest, tt.args.floorExt, tt.args.configEnforceRate, tt.args.f); got != tt.want { + t.Errorf("ShouldEnforce() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/floors/floors.go b/floors/floors.go new file mode 100644 index 00000000000..e45916ba67b --- /dev/null +++ b/floors/floors.go @@ -0,0 +1,95 @@ +package floors + +import ( + "fmt" + "math" + "math/rand" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/currency" + "github.com/prebid/prebid-server/openrtb_ext" +) + +const ( + defaultDelimiter string = "|" + catchAll string = "*" + skipRateMin int = 0 + skipRateMax int = 100 + modelWeightMax int = 100 + modelWeightMin int = 0 + enforceRateMin int = 0 + enforceRateMax int = 100 +) + +// ModifyImpsWithFloors will validate floor rules, based on request and rules prepares various combinations +// to match with floor rules and selects appripariate floor rule and update imp.bidfloor and imp.bidfloorcur +func ModifyImpsWithFloors(floorExt *openrtb_ext.PriceFloorRules, request *openrtb2.BidRequest, conversions currency.Conversions) []error { + var ( + floorErrList []error + floorModelErrList []error + floorVal float64 + ) + + if floorExt == nil || floorExt.Data == nil { + return nil + } + + floorData := floorExt.Data + floorSkipRateErr := validateFloorSkipRates(floorExt) + if floorSkipRateErr != nil { + return append(floorModelErrList, floorSkipRateErr) + } + + floorData.ModelGroups, floorModelErrList = selectValidFloorModelGroups(floorData.ModelGroups) + if len(floorData.ModelGroups) == 0 { + return floorModelErrList + } else if len(floorData.ModelGroups) > 1 { + floorData.ModelGroups = selectFloorModelGroup(floorData.ModelGroups, rand.Intn) + } + + modelGroup := floorData.ModelGroups[0] + if modelGroup.Schema.Delimiter == "" { + modelGroup.Schema.Delimiter = defaultDelimiter + } + + floorExt.Skipped = new(bool) + if shouldSkipFloors(floorExt.Data.ModelGroups[0].SkipRate, floorExt.Data.SkipRate, floorExt.SkipRate, rand.Intn) { + *floorExt.Skipped = true + floorData.ModelGroups = nil + return floorModelErrList + } + + floorErrList = validateFloorRulesAndLowerValidRuleKey(modelGroup.Schema, modelGroup.Schema.Delimiter, modelGroup.Values) + if len(modelGroup.Values) > 0 { + for i := 0; i < len(request.Imp); i++ { + desiredRuleKey := createRuleKey(modelGroup.Schema, request, request.Imp[i]) + matchedRule, isRuleMatched := findRule(modelGroup.Values, modelGroup.Schema.Delimiter, desiredRuleKey, len(modelGroup.Schema.Fields)) + + floorVal = modelGroup.Default + if isRuleMatched { + floorVal = modelGroup.Values[matchedRule] + } + + floorMinVal, floorCur, err := getMinFloorValue(floorExt, conversions) + if err == nil { + bidFloor := floorVal + if floorMinVal > 0.0 && floorVal < floorMinVal { + bidFloor = floorMinVal + } + + if bidFloor > 0.0 { + request.Imp[i].BidFloor = math.Round(bidFloor*10000) / 10000 + request.Imp[i].BidFloorCur = floorCur + } + if isRuleMatched { + updateImpExtWithFloorDetails(matchedRule, &request.Imp[i], modelGroup.Values[matchedRule]) + } + } else { + floorModelErrList = append(floorModelErrList, fmt.Errorf("Error in getting FloorMin value : '%v'", err.Error())) + } + + } + } + floorModelErrList = append(floorModelErrList, floorErrList...) + return floorModelErrList +} diff --git a/floors/floors_test.go b/floors/floors_test.go new file mode 100644 index 00000000000..d02ba8e5bdc --- /dev/null +++ b/floors/floors_test.go @@ -0,0 +1,836 @@ +package floors + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/currency" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestIsRequestEnabledWithFloor(t *testing.T) { + FalseFlag := false + TrueFlag := true + + tt := []struct { + name string + in *openrtb_ext.ExtRequest + out bool + }{ + { + name: "Request With Nil Floors", + in: &openrtb_ext.ExtRequest{}, + out: true, + }, + { + name: "Request With Floors Disabled", + in: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Floors: &openrtb_ext.PriceFloorRules{Enabled: &FalseFlag}}}, + out: false, + }, + { + name: "Request With Floors Enabled", + in: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Floors: &openrtb_ext.PriceFloorRules{Enabled: &TrueFlag}}}, + out: true, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + out := tc.in.Prebid.Floors.GetEnabled() + if !reflect.DeepEqual(out, tc.out) { + t.Errorf("error: \nreturn:\t%v\nwant:\t%v", out, tc.out) + } + }) + } +} + +func TestUpdateImpsWithFloorsVariousRuleKeys(t *testing.T) { + + floorExt := &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "country", "deviceType"}}, + Values: map[string]float64{ + "audio|USA|phone": 1.01, + }, Default: 0.01}}}} + + floorExt2 := &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"channel", "country", "deviceType"}}, + Values: map[string]float64{ + "chName|USA|tablet": 1.01, + "*|USA|tablet": 2.01, + }, Default: 0.01}}}} + + floorExt3 := &openrtb_ext.PriceFloorRules{FloorMin: 1.00, Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "gptSlot", "bundle"}}, + Values: map[string]float64{ + "native|adslot123|bundle1": 0.01, + "native|pbadslot123|bundle1": 0.01, + }, Default: 0.01}}}} + + floorExt4 := &openrtb_ext.PriceFloorRules{FloorMin: 1.00, Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "adUnitCode", "bundle"}}, + Values: map[string]float64{ + "native|tag123|bundle1": 1.5, + "native|pbadslot123|bundle1": 2.0, + "native|storedid_123|bundle1": 3.0, + "native|gpid_456|bundle1": 4.0, + "native|*|bundle1": 5.0, + }, Default: 1.0}}}} + + tt := []struct { + name string + floorExt *openrtb_ext.PriceFloorRules + request *openrtb2.BidRequest + floorVal float64 + floorCur string + }{ + { + name: "audio|USA|phone", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Device: &openrtb2.Device{Geo: &openrtb2.Geo{Country: "USA"}, UA: "Phone"}, + Imp: []openrtb2.Imp{{ID: "1234", Audio: &openrtb2.Audio{MaxDuration: 10}}}, + Ext: json.RawMessage(`{"prebid": {"floors": {"data": {"currency": "USD","skipRate": 0, "schema": {"fields": ["channel","size","domain"]},"values": {"chName|USA|tablet": 1.01, "*|*|*": 16.01},"default": 1},"channel": {"name": "chName","version": "ver1"}}}}`), + }, + floorExt: floorExt, + floorVal: 1.01, + floorCur: "USD", + }, + { + name: "chName|USA|tablet", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Device: &openrtb2.Device{Geo: &openrtb2.Geo{Country: "USA"}, UA: "tablet"}, + Imp: []openrtb2.Imp{{ID: "1234", Audio: &openrtb2.Audio{MaxDuration: 10}}}, + Ext: json.RawMessage(`{"prebid": {"channel": {"name": "chName","version": "ver1"}}}`)}, + floorExt: floorExt2, + floorVal: 1.01, + floorCur: "USD", + }, + { + name: "*|USA|tablet", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Device: &openrtb2.Device{Geo: &openrtb2.Geo{Country: "USA"}, UA: "tablet"}, + Imp: []openrtb2.Imp{{ID: "1234", Audio: &openrtb2.Audio{MaxDuration: 10}}}, + Ext: json.RawMessage(`{"prebid": }`)}, + floorExt: floorExt2, + floorVal: 2.01, + floorCur: "USD", + }, + { + name: "native|gptSlot|bundle1", + request: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Bundle: "bundle1", + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Device: &openrtb2.Device{Geo: &openrtb2.Geo{Country: "USA"}, UA: "tablet"}, + Imp: []openrtb2.Imp{{ID: "1234", Native: &openrtb2.Native{}, Ext: json.RawMessage(`{"data": {"adserver": {"name": "gam","adslot": "adslot123"}, "pbadslot": "pbadslot123"}}`)}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt3, + floorVal: 1.00, + floorCur: "USD", + }, + { + name: "native|adUnitCode|bundle1", + request: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Bundle: "bundle1", + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Device: &openrtb2.Device{Geo: &openrtb2.Geo{Country: "USA"}, UA: "tablet"}, + Imp: []openrtb2.Imp{{ID: "1234", Native: &openrtb2.Native{}, Ext: json.RawMessage(`{"data": {"adserver": {"name": "gam","adslot": "adslot123"}, "pbadslot": "pbadslot123"}}`)}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt4, + floorVal: 2.00, + floorCur: "USD", + }, + { + name: "native|adUnitCode|bundle1", + request: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Bundle: "bundle1", + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Device: &openrtb2.Device{Geo: &openrtb2.Geo{Country: "USA"}, UA: "tablet"}, + Imp: []openrtb2.Imp{{ID: "1234", Native: &openrtb2.Native{}, Ext: json.RawMessage(`{"prebid": {"storedrequest": {"id": "storedid_123"}}}`)}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt4, + floorVal: 3.00, + floorCur: "USD", + }, + { + name: "native|adUnitCode|bundle1", + request: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Bundle: "bundle1", + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Device: &openrtb2.Device{Geo: &openrtb2.Geo{Country: "USA"}, UA: "tablet"}, + Imp: []openrtb2.Imp{{ID: "1234", Native: &openrtb2.Native{}, Ext: json.RawMessage(`{"gpid": "gpid_456"}`)}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt4, + floorVal: 4.00, + floorCur: "USD", + }, + { + name: "native|*|bundle1", + request: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Bundle: "bundle1", + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Device: &openrtb2.Device{Geo: &openrtb2.Geo{Country: "USA"}, UA: "tablet"}, + Imp: []openrtb2.Imp{{ID: "1234", Native: &openrtb2.Native{}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt4, + floorVal: 5.00, + floorCur: "USD", + }, + { + name: "native|adUnitCode|bundle1", + request: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Bundle: "bundle1", + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Device: &openrtb2.Device{Geo: &openrtb2.Geo{Country: "USA"}, UA: "tablet"}, + Imp: []openrtb2.Imp{{ID: "1234", TagID: "tag123", Native: &openrtb2.Native{}, Ext: json.RawMessage(`{"data": {"adserver": {"name": "gam","adslot": "adslot123"}, "pbadslot": "pbadslot123"}}`)}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt4, + floorVal: 1.5, + floorCur: "USD", + }, + { + name: "native|gptSlot|bundle1", + request: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Bundle: "bundle1", + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Device: &openrtb2.Device{Geo: &openrtb2.Geo{Country: "USA"}, UA: "tablet"}, + Imp: []openrtb2.Imp{{ID: "1234", Native: &openrtb2.Native{}, Ext: json.RawMessage(`{"data": {"adserver": {"name": "ow","adslot": "adslot123"}, "pbadslot": "pbadslot123"}}`)}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt3, + floorVal: 1.00, + floorCur: "USD", + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + _ = ModifyImpsWithFloors(tc.floorExt, tc.request, nil) + if !reflect.DeepEqual(tc.request.Imp[0].BidFloor, tc.floorVal) { + t.Errorf("Floor Value error: \nreturn:\t%v\nwant:\t%v", tc.request.Imp[0].BidFloor, tc.floorVal) + } + if !reflect.DeepEqual(tc.request.Imp[0].BidFloorCur, tc.floorCur) { + t.Errorf("Floor Currency error: \nreturn:\t%v\nwant:\t%v", tc.request.Imp[0].BidFloor, tc.floorCur) + } + }) + } +} + +func getCurrencyRates(rates map[string]map[string]float64) currency.Conversions { + return currency.NewRates(rates) +} + +func TestUpdateImpsWithFloors(t *testing.T) { + + rates := map[string]map[string]float64{ + "USD": { + "INR": 70, + "EUR": 0.9, + "JPY": 5.09, + }, + } + + floorExt := &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com": 3.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}}}} + + floorExt2 := &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "siteDomain"}, Delimiter: "|"}, + Values: map[string]float64{ + "banner|300x250|www.publisher.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.publisher.com": 3.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|www.website.com|test": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "video|*|*": 9.01, + "*|300x250|www.website.com": 10.01, + "*|300x250|*": 10.11, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}}}} + + floorExt3 := &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{ + {Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "pubDomain"}, Delimiter: "|"}, + Values: map[string]float64{ + "banner|300x250|www.publisher.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.publisher.com": 3.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + }, Currency: "USD", Default: 0.01}}}, FloorMin: 1.0, FloorMinCur: "EUR"} + + floorExt4 := &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{ + {Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "pubDomain"}, Delimiter: "|"}, + Values: map[string]float64{ + "banner|300x250|www.publisher.com": 1.01, + }, SkipRate: 100, Default: 0.01}}}} + width := int64(300) + height := int64(600) + tt := []struct { + name string + floorExt *openrtb_ext.PriceFloorRules + request *openrtb2.BidRequest + floorVal float64 + floorCur string + Skipped bool + }{ + { + name: "banner|300x250|www.website.com", + request: &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","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt, + floorVal: 1.01, + floorCur: "USD", + }, + { + name: "banner|300x600|www.website.com", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Domain: "www.website.com", + }, + Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{W: &width, H: &height}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt, + floorVal: 3.01, + floorCur: "USD", + }, + { + name: "*|*|www.website.com", + request: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Domain: "www.website.com", + }, + Imp: []openrtb2.Imp{{ID: "1234", Video: &openrtb2.Video{W: 640, H: 480}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt, + floorVal: 15.01, + floorCur: "USD", + }, + { + name: "*|300x250|www.website.com", + request: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Imp: []openrtb2.Imp{{ID: "1234", Video: &openrtb2.Video{W: 300, H: 250}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt, + floorVal: 9.01, + floorCur: "USD", + }, + { + name: "siteDomain, banner|300x600|*", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Domain: "www.website.com", + }, + Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 600}}}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "siteDomain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt2, + floorVal: 4.01, + floorCur: "USD", + }, + { + name: "siteDomain, video|*|*", + request: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Domain: "www.website.com", + }, + Imp: []openrtb2.Imp{{ID: "1234", Video: &openrtb2.Video{W: 640, H: 480}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "siteDomain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt2, + floorVal: 9.01, + floorCur: "USD", + }, + { + name: "pubDomain, *|300x250|www.website.com", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Imp: []openrtb2.Imp{{ID: "1234", Video: &openrtb2.Video{W: 300, H: 250}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "pubDomain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt2, + floorVal: 9.01, + floorCur: "USD", + }, + { + name: "pubDomain, Default Floor Value", + request: &openrtb2.BidRequest{ + App: &openrtb2.App{ + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Imp: []openrtb2.Imp{{ID: "1234", Video: &openrtb2.Video{W: 300, H: 250}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "pubDomain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt3, + floorVal: 1.1111, + floorCur: "USD", + }, + { + name: "pubDomain, Default Floor Value", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Imp: []openrtb2.Imp{{ID: "1234", Video: &openrtb2.Video{W: 300, H: 250}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "pubDomain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt3, + floorVal: 1.1111, + floorCur: "USD", + }, + { + name: "Skiprate = 100, Check Skipped Flag", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, + }, + Imp: []openrtb2.Imp{{ID: "1234", Video: &openrtb2.Video{W: 300, H: 250}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "pubDomain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt4, + floorVal: 0.0, + floorCur: "", + Skipped: true, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + _ = ModifyImpsWithFloors(tc.floorExt, tc.request, getCurrencyRates(rates)) + if !reflect.DeepEqual(tc.request.Imp[0].BidFloor, tc.floorVal) { + t.Errorf("Floor Value error: \nreturn:\t%v\nwant:\t%v", tc.request.Imp[0].BidFloor, tc.floorVal) + } + if !reflect.DeepEqual(tc.request.Imp[0].BidFloorCur, tc.floorCur) { + t.Errorf("Floor Currency error: \nreturn:\t%v\nwant:\t%v", tc.request.Imp[0].BidFloor, tc.floorCur) + } + + if !reflect.DeepEqual(*tc.floorExt.Skipped, tc.Skipped) { + t.Errorf("Floor Skipped error: \nreturn:\t%v\nwant:\t%v", tc.floorExt.Skipped, tc.Skipped) + } + }) + } +} + +func TestUpdateImpsWithModelGroups(t *testing.T) { + floorExt := &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ + SkipRate: 30, + ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ + ModelWeight: 50, + SkipRate: 10, + ModelVersion: "Version 1", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com": 3.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}, + { + ModelWeight: 50, + SkipRate: 20, + ModelVersion: "Version 2", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com": 3.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}, + }}} + + rates := map[string]map[string]float64{ + "USD": { + "INR": 70, + "EUR": 0.9, + "JPY": 5.09, + }, + } + tt := []struct { + name string + floorExt *openrtb_ext.PriceFloorRules + request *openrtb2.BidRequest + floorVal float64 + floorCur string + ModelVersion string + }{ + { + name: "banner|300x250|www.website.com", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + 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","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt, + floorVal: 1.01, + floorCur: "USD", + ModelVersion: "Version 2", + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + _ = ModifyImpsWithFloors(tc.floorExt, tc.request, getCurrencyRates(rates)) + if tc.floorExt.Skipped != nil && *tc.floorExt.Skipped != true { + if !reflect.DeepEqual(tc.request.Imp[0].BidFloor, tc.floorVal) { + t.Errorf("Floor Value error: \nreturn:\t%v\nwant:\t%v", tc.request.Imp[0].BidFloor, tc.floorVal) + } + if !reflect.DeepEqual(tc.request.Imp[0].BidFloorCur, tc.floorCur) { + t.Errorf("Floor Currency error: \nreturn:\t%v\nwant:\t%v", tc.request.Imp[0].BidFloor, tc.floorCur) + } + + if !reflect.DeepEqual(tc.floorExt.Data.ModelGroups[0].ModelVersion, tc.ModelVersion) { + t.Errorf("Floor Model Version mismatch error: \nreturn:\t%v\nwant:\t%v", tc.floorExt.Data.ModelGroups[0].ModelVersion, tc.ModelVersion) + } + } + }) + } +} + +func TestUpdateImpsWithInvalidModelGroups(t *testing.T) { + floorExt := &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ + ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ + ModelWeight: 50, + SkipRate: 110, + ModelVersion: "Version 1", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com": 3.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}, + }}} + rates := map[string]map[string]float64{ + "USD": { + "INR": 70, + "EUR": 0.9, + "JPY": 5.09, + }, + } + + tt := []struct { + name string + floorExt *openrtb_ext.PriceFloorRules + request *openrtb2.BidRequest + floorVal float64 + floorCur string + ModelVersion string + Err string + }{ + { + name: "Invalid Skip Rate in model Group 1, with banner|300x250|www.website.com", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + 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","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: floorExt, + floorVal: 0.0, + floorCur: "", + Err: "Invalid Floor Model = 'Version 1' due to SkipRate = '110'", + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + ErrList := ModifyImpsWithFloors(tc.floorExt, tc.request, getCurrencyRates(rates)) + + if !reflect.DeepEqual(tc.request.Imp[0].BidFloor, tc.floorVal) { + t.Errorf("Floor Value error: \nreturn:\t%v\nwant:\t%v", tc.request.Imp[0].BidFloor, tc.floorVal) + } + if !reflect.DeepEqual(tc.request.Imp[0].BidFloorCur, tc.floorCur) { + t.Errorf("Floor Currency error: \nreturn:\t%v\nwant:\t%v", tc.request.Imp[0].BidFloor, tc.floorCur) + } + + if !reflect.DeepEqual(ErrList[0].Error(), tc.Err) { + t.Errorf("Incorrect Error: \nreturn:\t%v\nwant:\t%v", ErrList[0].Error(), tc.Err) + } + + }) + } +} + +func TestUpdateImpsWithFloorsCurrecnyConversion(t *testing.T) { + rates := map[string]map[string]float64{ + "USD": { + "INR": 70, + "EUR": 0.9, + "JPY": 5.09, + }, + } + + tt := []struct { + name string + floorExt *openrtb_ext.PriceFloorRules + request *openrtb2.BidRequest + floorVal float64 + floorCur string + Skipped bool + }{ + { + name: "BidFloor(USD) Less than MinBidFloor(INR) with different currency", + request: &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","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: &openrtb_ext.PriceFloorRules{FloorMin: 80, FloorMinCur: "INR", Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.00, + "banner|300x250|*": 2.01, + "*|*|*": 16.01, + }, Default: 0.01}}}}, + floorVal: 1.1429, + floorCur: "USD", + }, + { + name: "BidFloor(INR) Less than MinBidFloor(USD) with different currency", + request: &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","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: &openrtb_ext.PriceFloorRules{FloorMin: 1, FloorMinCur: "USD", Data: &openrtb_ext.PriceFloorData{Currency: "INR", ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 65.00, + "banner|300x250|*": 110.00, + }, Default: 50.00}}}}, + floorVal: 70, + floorCur: "INR", + }, + { + name: "MinBidFloor Less than BidFloor with same currency", + request: &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","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: &openrtb_ext.PriceFloorRules{FloorMin: 1, FloorMinCur: "USD", Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 2.00, + "banner|300x250|*": 2.01, + "*|*|*": 16.01, + }, Default: 0.01}}}}, + floorVal: 2, + floorCur: "USD", + }, + { + name: "BidFloor Less than MinBidFloor with same currency", + request: &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","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: &openrtb_ext.PriceFloorRules{FloorMin: 3, FloorMinCur: "USD", Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.00, + "banner|300x250|*": 2.01, + "*|*|*": 16.01, + }, Default: 0.01}}}}, + floorVal: 3, + floorCur: "USD", + }, + { + name: "BidFloor greater than MinBidFloor with same currency", + request: &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","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: &openrtb_ext.PriceFloorRules{FloorMin: 3, FloorMinCur: "USD", Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 10.00, + "banner|300x250|*": 2.01, + "*|*|*": 16.01, + }}}}}, + floorVal: 10, + floorCur: "USD", + }, + { + name: "No rule matched, Default value greater than MinBidFloor with same currency", + request: &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","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: &openrtb_ext.PriceFloorRules{FloorMin: 3, FloorMinCur: "USD", Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com1": 10.00, + }, Default: 15}}}}, + floorVal: 15, + floorCur: "USD", + }, + { + name: "No rule matched, Default value leass than MinBidFloor with same currency", + request: &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","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: &openrtb_ext.PriceFloorRules{FloorMin: 5, FloorMinCur: "USD", Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com1": 10.00, + }, Default: 1}}}}, + floorVal: 5, + floorCur: "USD", + }, + { + name: "imp.bidfloor provided, No Rule matching and MinBidFloor, default values not provided in floor JSON", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{Domain: "www.website123.com"}, + }, + Imp: []openrtb2.Imp{{ID: "1234", BidFloor: 1.5, BidFloorCur: "INR", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.00, + }}}}}, + floorVal: 1.5, + floorCur: "INR", + }, + { + name: "imp.bidfloor not provided, No Rule matching and MinBidFloor, default values not provided in floor JSON", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{Domain: "www.website123.com"}, + }, + Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ModelGroups: []openrtb_ext.PriceFloorModelGroup{{Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.00, + }}}}}, + floorVal: 0, + floorCur: "", + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + _ = ModifyImpsWithFloors(tc.floorExt, tc.request, getCurrencyRates(rates)) + if !reflect.DeepEqual(tc.request.Imp[0].BidFloor, tc.floorVal) { + t.Errorf("Floor Value error: \nreturn:\t%v\nwant:\t%v", tc.request.Imp[0].BidFloor, tc.floorVal) + } + if !reflect.DeepEqual(tc.request.Imp[0].BidFloorCur, tc.floorCur) { + t.Errorf("Floor Currency error: \nreturn:\t%v\nwant:\t%v", tc.request.Imp[0].BidFloorCur, tc.floorCur) + } + + }) + } +} diff --git a/floors/rule.go b/floors/rule.go new file mode 100644 index 00000000000..95cddba008d --- /dev/null +++ b/floors/rule.go @@ -0,0 +1,415 @@ +package floors + +import ( + "encoding/json" + "fmt" + "math/bits" + "regexp" + "sort" + "strings" + + "github.com/buger/jsonparser" + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/currency" + "github.com/prebid/prebid-server/openrtb_ext" +) + +const ( + SiteDomain string = "siteDomain" + PubDomain string = "pubDomain" + Domain string = "domain" + Bundle string = "bundle" + Channel string = "channel" + MediaType string = "mediaType" + Size string = "size" + GptSlot string = "gptSlot" + AdUnitCode string = "adUnitCode" + Country string = "country" + DeviceType string = "deviceType" + Tablet string = "tablet" + Phone string = "phone" +) + +func getFloorCurrency(floorExt *openrtb_ext.PriceFloorRules) string { + floorCur := "USD" + if floorExt == nil || floorExt.Data == nil { + return floorCur + } + + if floorExt.Data.Currency != "" { + floorCur = floorExt.Data.Currency + } + + if floorExt.Data.ModelGroups[0].Currency != "" { + floorCur = floorExt.Data.ModelGroups[0].Currency + } + return floorCur +} + +func getMinFloorValue(floorExt *openrtb_ext.PriceFloorRules, conversions currency.Conversions) (float64, string, error) { + var err error + var rate float64 + + if floorExt == nil { + return 0, "USD", err + } + floorMin := floorExt.FloorMin + floorCur := getFloorCurrency(floorExt) + + if floorExt.FloorMin > 0.0 && floorExt.FloorMinCur != "" && floorCur != "" && + floorExt.FloorMinCur != floorCur { + rate, err = conversions.GetRate(floorExt.FloorMinCur, floorCur) + floorMin = rate * floorExt.FloorMin + } + return floorMin, floorCur, err +} + +func updateImpExtWithFloorDetails(matchedRule string, imp *openrtb2.Imp, floorVal float64) { + imp.Ext, _ = jsonparser.Set(imp.Ext, []byte(`"`+matchedRule+`"`), "prebid", "floors", "floorRule") + imp.Ext, _ = jsonparser.Set(imp.Ext, []byte(fmt.Sprintf("%.4f", floorVal)), "prebid", "floors", "floorRuleValue") +} + +func selectFloorModelGroup(modelGroups []openrtb_ext.PriceFloorModelGroup, f func(int) int) []openrtb_ext.PriceFloorModelGroup { + totalModelWeight := 0 + + for i := 0; i < len(modelGroups); i++ { + if modelGroups[i].ModelWeight == 0 { + modelGroups[i].ModelWeight = 1 + } + totalModelWeight += modelGroups[i].ModelWeight + } + + sort.SliceStable(modelGroups, func(i, j int) bool { + return modelGroups[i].ModelWeight < modelGroups[j].ModelWeight + }) + + winWeight := f(totalModelWeight + 1) + debugWeight := winWeight + for i, modelGroup := range modelGroups { + winWeight -= modelGroup.ModelWeight + if winWeight <= 0 { + modelGroups[0], modelGroups[i] = modelGroups[i], modelGroups[0] + modelGroups[0].DebugWeight = debugWeight + return modelGroups[:1] + } + } + return modelGroups[:1] +} + +func shouldSkipFloors(ModelGroupsSkipRate, DataSkipRate, RootSkipRate int, f func(int) int) bool { + skipRate := 0 + + if ModelGroupsSkipRate > 0 { + skipRate = ModelGroupsSkipRate + } else if DataSkipRate > 0 { + skipRate = DataSkipRate + } else { + skipRate = RootSkipRate + } + return skipRate >= f(skipRateMax+1) +} + +func findRule(ruleValues map[string]float64, delimiter string, desiredRuleKey []string, numFields int) (string, bool) { + + ruleKeys := prepareRuleCombinations(desiredRuleKey, numFields, delimiter) + for i := 0; i < len(ruleKeys); i++ { + if _, ok := ruleValues[ruleKeys[i]]; ok { + return ruleKeys[i], true + } + } + return "", false +} + +func createRuleKey(floorSchema openrtb_ext.PriceFloorSchema, request *openrtb2.BidRequest, imp openrtb2.Imp) []string { + var ruleKeys []string + + for _, field := range floorSchema.Fields { + value := catchAll + switch field { + case MediaType: + value = getMediaType(imp) + case Size: + value = getSizeValue(imp) + case Domain: + value = getDomain(request) + case SiteDomain: + value = getSiteDomain(request) + case Bundle: + value = getBundle(request) + case PubDomain: + value = getPublisherDomain(request) + case Country: + value = getDeviceCountry(request) + case DeviceType: + value = getDeviceType(request) + case Channel: + value = extractChanelNameFromBidRequestExt(request) + case GptSlot: + value = getgptslot(imp) + case AdUnitCode: + value = getAdUnitCode(imp) + } + ruleKeys = append(ruleKeys, value) + } + return ruleKeys +} + +func getDeviceType(request *openrtb2.BidRequest) string { + value := catchAll + if request.Device == nil || len(request.Device.UA) == 0 { + return value + } + if isMobileDevice(request.Device.UA) { + value = Phone + } else if isTabletDevice(request.Device.UA) { + value = Tablet + } + return value +} + +func getDeviceCountry(request *openrtb2.BidRequest) string { + value := catchAll + if request.Device != nil && request.Device.Geo != nil { + value = request.Device.Geo.Country + } + return value +} + +func getMediaType(imp openrtb2.Imp) string { + value := catchAll + if imp.Banner != nil { + value = string(openrtb_ext.BidTypeBanner) + } else if imp.Video != nil { + value = string(openrtb_ext.BidTypeVideo) + } else if imp.Audio != nil { + value = string(openrtb_ext.BidTypeAudio) + } else if imp.Native != nil { + value = string(openrtb_ext.BidTypeNative) + } + return value +} + +func getSizeValue(imp openrtb2.Imp) string { + size := catchAll + width := int64(0) + height := int64(0) + if imp.Banner != nil { + if len(imp.Banner.Format) > 0 { + width = imp.Banner.Format[0].W + height = imp.Banner.Format[0].H + } else if imp.Banner.W != nil && imp.Banner.H != nil { + width = *imp.Banner.W + height = *imp.Banner.H + } + } else { + width = imp.Video.W + height = imp.Video.H + } + + if width != 0 && height != 0 { + size = fmt.Sprintf("%dx%d", width, height) + } + return size +} + +func getDomain(request *openrtb2.BidRequest) string { + value := catchAll + if request.Site != nil { + if len(request.Site.Domain) > 0 { + value = request.Site.Domain + } else if request.Site.Publisher != nil && len(request.Site.Publisher.Domain) > 0 { + value = request.Site.Publisher.Domain + } + } else if request.App != nil { + if len(request.App.Domain) > 0 { + value = request.App.Domain + } else if request.App.Publisher != nil && len(request.App.Publisher.Domain) > 0 { + value = request.App.Publisher.Domain + } + } + return value +} + +func getSiteDomain(request *openrtb2.BidRequest) string { + var value string + if request.Site != nil { + value = request.Site.Domain + } else { + value = request.App.Domain + } + return value +} + +func getPublisherDomain(request *openrtb2.BidRequest) string { + value := catchAll + if request.Site != nil && request.Site.Publisher != nil && len(request.Site.Publisher.Domain) > 0 { + value = request.Site.Publisher.Domain + } else if request.App != nil && request.App.Publisher != nil && len(request.App.Publisher.Domain) > 0 { + value = request.App.Publisher.Domain + } + return value +} + +func getBundle(request *openrtb2.BidRequest) string { + value := catchAll + if request.App != nil && len(request.App.Bundle) > 0 { + value = request.App.Bundle + } + return value +} + +func getgptslot(imp openrtb2.Imp) string { + value := catchAll + adsname, err := jsonparser.GetString(imp.Ext, "data", "adserver", "name") + if err == nil && adsname == "gam" { + gptSlot, _ := jsonparser.GetString(imp.Ext, "data", "adserver", "adslot") + if gptSlot != "" { + value = gptSlot + } + } else { + value = getpbadslot(imp) + } + return value +} + +func extractChanelNameFromBidRequestExt(bidRequest *openrtb2.BidRequest) string { + requestExt := &openrtb_ext.ExtRequest{} + if bidRequest == nil { + return catchAll + } + + if len(bidRequest.Ext) > 0 { + err := json.Unmarshal(bidRequest.Ext, &requestExt) + if err != nil { + return catchAll + } + } + + if requestExt.Prebid.Channel != nil { + return requestExt.Prebid.Channel.Name + } + return catchAll +} + +func getpbadslot(imp openrtb2.Imp) string { + value := catchAll + pbAdSlot, err := jsonparser.GetString(imp.Ext, "data", "pbadslot") + if err == nil { + value = pbAdSlot + } + return value +} + +func getAdUnitCode(imp openrtb2.Imp) string { + adUnitCode := catchAll + gpId, err := jsonparser.GetString(imp.Ext, "gpid") + if err == nil && gpId != "" { + return gpId + } + + if imp.TagID != "" { + return imp.TagID + } + + pbAdSlot, err := jsonparser.GetString(imp.Ext, "data", "pbadslot") + if err == nil && pbAdSlot != "" { + return pbAdSlot + } + + storedrequestID, err := jsonparser.GetString(imp.Ext, "prebid", "storedrequest", "id") + if err == nil && storedrequestID != "" { + return storedrequestID + } + return adUnitCode +} + +func isMobileDevice(userAgent string) bool { + isMobile, err := regexp.MatchString("(?i)Phone|iPhone|Android|Mobile", userAgent) + if err != nil { + return false + } + return isMobile +} + +func isTabletDevice(userAgent string) bool { + isTablet, err := regexp.MatchString("(?i)tablet|iPad|Windows NT", userAgent) + if err != nil { + return false + } + return isTablet +} + +func prepareRuleCombinations(keys []string, numSchemaFields int, delimiter string) []string { + var subset []string + var comb []int + var desiredkeys [][]string + var ruleKeys []string + + segNum := 1 << numSchemaFields + for i := 0; i < numSchemaFields; i++ { + subset = append(subset, strings.ToLower(keys[i])) + comb = append(comb, i) + } + desiredkeys = append(desiredkeys, subset) + for numWildCart := 1; numWildCart <= numSchemaFields; numWildCart++ { + newComb := generateCombinations(comb, numWildCart, segNum) + for i := 0; i < len(newComb); i++ { + eachSet := make([]string, len(desiredkeys[0])) + _ = copy(eachSet, desiredkeys[0]) + for j := 0; j < len(newComb[i]); j++ { + eachSet[newComb[i][j]] = catchAll + } + desiredkeys = append(desiredkeys, eachSet) + } + } + ruleKeys = prepareRuleKeys(desiredkeys, delimiter) + return ruleKeys +} + +func prepareRuleKeys(desiredkeys [][]string, delimiter string) []string { + var ruleKeys []string + for i := 0; i < len(desiredkeys); i++ { + subset := desiredkeys[i][0] + for j := 1; j < len(desiredkeys[i]); j++ { + subset += delimiter + desiredkeys[i][j] + } + ruleKeys = append(ruleKeys, subset) + } + return ruleKeys +} + +func generateCombinations(set []int, numWildCart int, segNum int) (comb [][]int) { + length := uint(len(set)) + + if numWildCart > len(set) { + numWildCart = len(set) + } + + for subsetBits := 1; subsetBits < (1 << length); subsetBits++ { + if numWildCart > 0 && bits.OnesCount(uint(subsetBits)) != numWildCart { + continue + } + var subset []int + for object := uint(0); object < length; object++ { + if (subsetBits>>object)&1 == 1 { + subset = append(subset, set[object]) + } + } + comb = append(comb, subset) + } + + // Sort combinations based on priority mentioned in https://docs.prebid.org/dev-docs/modules/floors.html#rule-selection-process + sort.SliceStable(comb, func(i, j int) bool { + wt1 := 0 + for k := 0; k < len(comb[i]); k++ { + wt1 += 1 << (segNum - comb[i][k]) + } + + wt2 := 0 + for k := 0; k < len(comb[j]); k++ { + wt2 += 1 << (segNum - comb[j][k]) + } + return wt1 < wt2 + }) + return comb +} diff --git a/floors/rule_test.go b/floors/rule_test.go new file mode 100644 index 00000000000..aed71ffb108 --- /dev/null +++ b/floors/rule_test.go @@ -0,0 +1,350 @@ +package floors + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/mxmCherry/openrtb/v16/openrtb2" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestPrepareRuleCombinations(t *testing.T) { + tt := []struct { + name string + in []string + n int + del string + out []string + }{ + { + name: "Schema items, n = 1", + in: []string{"A"}, + n: 1, + del: "|", + out: []string{ + "a", + "*", + }, + }, + { + name: "Schema items, n = 2", + in: []string{"A", "B"}, + n: 2, + del: "|", + out: []string{ + "a|b", + "a|*", + "*|b", + "*|*", + }, + }, + { + name: "Schema items, n = 3", + in: []string{"A", "B", "C"}, + n: 3, + del: "|", + out: []string{ + "a|b|c", + "a|b|*", + "a|*|c", + "*|b|c", + "a|*|*", + "*|b|*", + "*|*|c", + "*|*|*", + }, + }, + { + name: "Schema items, n = 4", + in: []string{"A", "B", "C", "D"}, + n: 4, + del: "|", + out: []string{ + "a|b|c|d", + "a|b|c|*", + "a|b|*|d", + "a|*|c|d", + "*|b|c|d", + "a|b|*|*", + "a|*|c|*", + "a|*|*|d", + "*|b|c|*", + "*|b|*|d", + "*|*|c|d", + "a|*|*|*", + "*|b|*|*", + "*|*|c|*", + "*|*|*|d", + "*|*|*|*", + }, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + out := prepareRuleCombinations(tc.in, tc.n, tc.del) + if !reflect.DeepEqual(out, tc.out) { + t.Errorf("error: \nreturn:\t%v\nwant:\t%v", out, tc.out) + } + }) + } +} + +func TestUpdateImpExtWithFloorDetails(t *testing.T) { + tt := []struct { + name string + matchedRule string + floorRuleVal float64 + imp openrtb2.Imp + expected json.RawMessage + }{ + { + name: "Nil ImpExt", + matchedRule: "test|123|xyz", + floorRuleVal: 5.5, + imp: openrtb2.Imp{ID: "1234", Video: &openrtb2.Video{W: 300, H: 250}}, + expected: json.RawMessage{}, + }, + { + name: "Empty ImpExt", + matchedRule: "test|123|xyz", + floorRuleVal: 5.5, + imp: openrtb2.Imp{ID: "1234", Video: &openrtb2.Video{W: 300, H: 250}, Ext: json.RawMessage{}}, + expected: json.RawMessage{}, + }, + { + name: "With prebid Ext", + matchedRule: "banner|www.test.com|*", + floorRuleVal: 5.5, + imp: openrtb2.Imp{ID: "1234", Video: &openrtb2.Video{W: 300, H: 250}, Ext: []byte(`{"prebid": {"test": true}}`)}, + expected: []byte(`{"prebid": {"test": true,"floors":{"floorRule":"banner|www.test.com|*","floorRuleValue":5.5000}}}`), + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + updateImpExtWithFloorDetails(tc.matchedRule, &tc.imp, tc.floorRuleVal) + if tc.imp.Ext != nil && !reflect.DeepEqual(tc.imp.Ext, tc.expected) { + t.Errorf("error: \nreturn:\t%v\n want:\t%v", string(tc.imp.Ext), string(tc.expected)) + } + }) + } +} + +func TestCreateRuleKeys(t *testing.T) { + tt := []struct { + name string + floorSchema openrtb_ext.PriceFloorSchema + request *openrtb2.BidRequest + imp openrtb2.Imp + out []string + }{ + { + name: "CreateRule with banner mediatype, size and domain", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Domain: "www.test.com", + }, + Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorSchema: openrtb_ext.PriceFloorSchema{Delimiter: "|", Fields: []string{"mediaType", "size", "domain"}}, + imp: openrtb2.Imp{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}, + out: []string{"banner", "300x250", "www.test.com"}, + }, + { + name: "CreateRule with video mediatype, size and domain", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Domain: "www.test.com", + }, + Imp: []openrtb2.Imp{{ID: "1234", Video: &openrtb2.Video{W: 640, H: 480}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorSchema: openrtb_ext.PriceFloorSchema{Delimiter: "|", Fields: []string{"mediaType", "size", "domain"}}, + imp: openrtb2.Imp{ID: "1234", Video: &openrtb2.Video{W: 640, H: 480}}, + out: []string{"video", "640x480", "www.test.com"}, + }, + { + name: "CreateRule with video mediatype, size and domain", + request: &openrtb2.BidRequest{ + Site: &openrtb2.Site{ + Domain: "www.test.com", + }, + Imp: []openrtb2.Imp{{ID: "1234", Video: &openrtb2.Video{W: 300, H: 250}}}, + Ext: json.RawMessage(`{"prebid": { "floors": {"data": {"currency": "USD","skipRate": 0,"schema": {"fields": [ "mediaType", "size", "domain" ] },"values": { "banner|300x250|www.website.com": 1.01, "banner|300x250|*": 2.01, "banner|300x600|www.website.com": 3.01, "banner|300x600|*": 4.01, "banner|728x90|www.website.com": 5.01, "banner|728x90|*": 6.01, "banner|*|www.website.com": 7.01, "banner|*|*": 8.01, "*|300x250|www.website.com": 9.01, "*|300x250|*": 10.01, "*|300x600|www.website.com": 11.01, "*|300x600|*": 12.01, "*|728x90|www.website.com": 13.01, "*|728x90|*": 14.01, "*|*|www.website.com": 15.01, "*|*|*": 16.01 }, "default": 1}}}}`), + }, + floorSchema: openrtb_ext.PriceFloorSchema{Delimiter: "|", Fields: []string{"mediaType", "size", "domain"}}, + imp: openrtb2.Imp{ID: "1234", Video: &openrtb2.Video{W: 300, H: 250}}, + out: []string{"video", "300x250", "www.test.com"}, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + out := createRuleKey(tc.floorSchema, tc.request, tc.imp) + if !reflect.DeepEqual(out, tc.out) { + t.Errorf("error: \nreturn:\t%v\nwant:\t%v", out, tc.out) + } + }) + } +} + +func TestShouldSkipFloors(t *testing.T) { + + tt := []struct { + name string + ModelGroupsSkipRate int + DataSkipRate int + RootSkipRate int + out bool + randomGen func(int) int + }{ + { + name: "ModelGroupsSkipRate=10 with skip = true", + ModelGroupsSkipRate: 10, + DataSkipRate: 0, + RootSkipRate: 0, + randomGen: func(i int) int { return 5 }, + out: true, + }, + { + name: "ModelGroupsSkipRate=100 with skip = true", + ModelGroupsSkipRate: 100, + DataSkipRate: 0, + RootSkipRate: 0, + randomGen: func(i int) int { return 5 }, + out: true, + }, + { + name: "ModelGroupsSkipRate=0 with skip = false", + ModelGroupsSkipRate: 0, + DataSkipRate: 0, + RootSkipRate: 0, + randomGen: func(i int) int { return 5 }, + out: false, + }, + { + name: "DataSkipRate=50 with with skip = true", + ModelGroupsSkipRate: 0, + DataSkipRate: 50, + RootSkipRate: 0, + randomGen: func(i int) int { return 40 }, + out: true, + }, + { + name: "RootSkipRate=50 with with skip = true", + ModelGroupsSkipRate: 0, + DataSkipRate: 0, + RootSkipRate: 60, + randomGen: func(i int) int { return 40 }, + out: true, + }, + { + name: "RootSkipRate=50 with with skip = false", + ModelGroupsSkipRate: 0, + DataSkipRate: 0, + RootSkipRate: 60, + randomGen: func(i int) int { return 70 }, + out: false, + }, + { + name: "RootSkipRate=100 with with skip = true", + ModelGroupsSkipRate: 0, + DataSkipRate: 0, + RootSkipRate: 100, + randomGen: func(i int) int { return 100 }, + out: true, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + out := shouldSkipFloors(tc.ModelGroupsSkipRate, tc.DataSkipRate, tc.RootSkipRate, tc.randomGen) + if !reflect.DeepEqual(out, tc.out) { + t.Errorf("error: \nreturn:\t%v\nwant:\t%v", out, tc.out) + } + }) + } + +} + +func TestSelectFloorModelGroup(t *testing.T) { + floorExt := &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ + SkipRate: 30, + ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ + ModelWeight: 50, + SkipRate: 10, + ModelVersion: "Version 1", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com": 3.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}, + { + ModelWeight: 25, + SkipRate: 20, + ModelVersion: "Version 2", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com": 3.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}, + }}} + + tt := []struct { + name string + floorExt *openrtb_ext.PriceFloorRules + ModelVersion string + fn func(int) int + }{ + { + name: "Version 2 Selection", + floorExt: floorExt, + ModelVersion: "Version 2", + fn: func(i int) int { return 5 }, + }, + { + name: "Version 1 Selection", + floorExt: floorExt, + ModelVersion: "Version 1", + fn: func(i int) int { return 55 }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + selectFloorModelGroup(tc.floorExt.Data.ModelGroups, tc.fn) + + if !reflect.DeepEqual(tc.floorExt.Data.ModelGroups[0].ModelVersion, tc.ModelVersion) { + t.Errorf("Floor Model Version mismatch error: \nreturn:\t%v\nwant:\t%v", tc.floorExt.Data.ModelGroups[0].ModelVersion, tc.ModelVersion) + } + + }) + } +} diff --git a/floors/validate.go b/floors/validate.go new file mode 100644 index 00000000000..054f1eb2dfc --- /dev/null +++ b/floors/validate.go @@ -0,0 +1,55 @@ +package floors + +import ( + "fmt" + "strings" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +func validateFloorRulesAndLowerValidRuleKey(schema openrtb_ext.PriceFloorSchema, delimiter string, ruleValues map[string]float64) []error { + var errs []error + for key, val := range ruleValues { + parsedKey := strings.Split(key, delimiter) + delete(ruleValues, key) + if len(parsedKey) != len(schema.Fields) { + // Number of fields in rule and number of schema fields are not matching + errs = append(errs, fmt.Errorf("Invalid Floor Rule = '%s' for Schema Fields = '%v'", key, schema.Fields)) + continue + } + newKey := strings.ToLower(key) + ruleValues[newKey] = val + } + return errs +} + +func validateFloorSkipRates(floorExt *openrtb_ext.PriceFloorRules) error { + + if floorExt.Data != nil && (floorExt.Data.SkipRate < skipRateMin || floorExt.Data.SkipRate > skipRateMax) { + return fmt.Errorf("Invalid SkipRate at data level = '%v'", floorExt.Data.SkipRate) + } + + if floorExt.SkipRate < skipRateMin || floorExt.SkipRate > skipRateMax { + return fmt.Errorf("Invalid SkipRate at root level = '%v'", floorExt.SkipRate) + } + return nil +} + +func selectValidFloorModelGroups(modelGroups []openrtb_ext.PriceFloorModelGroup) ([]openrtb_ext.PriceFloorModelGroup, []error) { + var errs []error + var validModelGroups []openrtb_ext.PriceFloorModelGroup + for _, modelGroup := range modelGroups { + if modelGroup.SkipRate < skipRateMin || modelGroup.SkipRate > skipRateMax { + errs = append(errs, fmt.Errorf("Invalid Floor Model = '%v' due to SkipRate = '%v'", modelGroup.ModelVersion, modelGroup.SkipRate)) + continue + } + + if modelGroup.ModelWeight < modelWeightMin || modelGroup.ModelWeight > modelWeightMax { + errs = append(errs, fmt.Errorf("Invalid Floor Model = '%v' due to ModelWeight = '%v'", modelGroup.ModelVersion, modelGroup.ModelWeight)) + continue + } + + validModelGroups = append(validModelGroups, modelGroup) + } + return validModelGroups, errs +} diff --git a/floors/validate_test.go b/floors/validate_test.go new file mode 100644 index 00000000000..5635009f0b9 --- /dev/null +++ b/floors/validate_test.go @@ -0,0 +1,307 @@ +package floors + +import ( + "reflect" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestValidateFloorSkipRates(t *testing.T) { + + tt := []struct { + name string + floorExt *openrtb_ext.PriceFloorRules + Err string + }{ + { + name: "Valid Skip Rate", + floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ + ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ + ModelVersion: "Version 1", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}, Delimiter: "|"}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com|www.test.com": 3.01, + "banner|300x600|*": 4.01, + }, Default: 0.01}, + }}}, + Err: "", + }, + { + name: "Invalid Skip Rate at Root level", + floorExt: &openrtb_ext.PriceFloorRules{SkipRate: -10}, + Err: "Invalid SkipRate at root level = '-10'", + }, + { + name: "Invalid Skip Rate at Date level", + floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ + SkipRate: -10, + ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ + ModelVersion: "Version 1", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}, + }}}, + Err: "Invalid SkipRate at data level = '-10'", + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + if actErr := validateFloorSkipRates(tc.floorExt); actErr != nil { + if !reflect.DeepEqual(actErr.Error(), tc.Err) { + t.Errorf("Incorrect Error: \nreturn:\t%v\nwant:\t%v", actErr.Error(), tc.Err) + } + } + + }) + } +} + +func TestSelectValidFloorModelGroups(t *testing.T) { + + tt := []struct { + name string + floorExt *openrtb_ext.PriceFloorRules + ModelVersion string + Err string + }{ + { + name: "Invalid Skip Rate in model Group 1, with banner|300x250|www.website.com", + floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ + ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ + ModelWeight: 50, + SkipRate: 110, + ModelVersion: "Version 1", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com": 3.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}, + { + ModelWeight: 50, + SkipRate: 20, + ModelVersion: "Version 2", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com": 3.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}, + }}}, + ModelVersion: "Version 1", + Err: "Invalid Floor Model = 'Version 1' due to SkipRate = '110'", + }, + { + name: "Invalid model weight Model Group 1, with banner|300x250|www.website.com", + floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ + ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ + ModelWeight: -1, + SkipRate: 10, + ModelVersion: "Version 1", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com": 3.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}, + { + ModelWeight: 50, + SkipRate: 20, + ModelVersion: "Version 2", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com": 3.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}, + }}}, + ModelVersion: "Version 1", + Err: "Invalid Floor Model = 'Version 1' due to ModelWeight = '-1'", + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + _, ErrList := selectValidFloorModelGroups(tc.floorExt.Data.ModelGroups) + + if !reflect.DeepEqual(tc.floorExt.Data.ModelGroups[0].ModelVersion, tc.ModelVersion) { + t.Errorf("Floor Model Version mismatch error: \nreturn:\t%v\nwant:\t%v", tc.floorExt.Data.ModelGroups[0].ModelVersion, tc.ModelVersion) + } + + if !reflect.DeepEqual(ErrList[0].Error(), tc.Err) { + t.Errorf("Incorrect Error: \nreturn:\t%v\nwant:\t%v", ErrList[0].Error(), tc.Err) + } + + }) + } +} + +func TestValidateFloorRulesAndLowerValidRuleKey(t *testing.T) { + + tt := []struct { + name string + floorExt *openrtb_ext.PriceFloorRules + Err string + expctedFloor map[string]float64 + }{ + { + name: "Invalid floor rule banner|300x600|www.website.com|www.test.com", + floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ + ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ + ModelVersion: "Version 1", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}, Delimiter: "|"}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com|www.test.com": 3.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}, + }}}, + Err: "Invalid Floor Rule = 'banner|300x600|www.website.com|www.test.com' for Schema Fields = '[mediaType size domain]'", + expctedFloor: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|*": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, + }, + { + name: "Invalid floor rule banner|300x600|www.website.com|www.test.com", + floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ + ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ + ModelVersion: "Version 1", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}, Delimiter: "|"}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com": 3.01, + "banner|300x600": 4.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, Default: 0.01}, + }}}, + Err: "Invalid Floor Rule = 'banner|300x600' for Schema Fields = '[mediaType size domain]'", + expctedFloor: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x250|*": 2.01, + "banner|300x600|www.website.com": 3.01, + "banner|728x90|www.website.com": 5.01, + "banner|728x90|*": 6.01, + "banner|*|www.website.com": 7.01, + "banner|*|*": 8.01, + "*|300x250|www.website.com": 9.01, + "*|300x250|*": 10.01, + "*|300x600|www.website.com": 11.01, + "*|300x600|*": 12.01, + "*|728x90|www.website.com": 13.01, + "*|728x90|*": 14.01, + "*|*|www.website.com": 15.01, + "*|*|*": 16.01, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + ErrList := validateFloorRulesAndLowerValidRuleKey(tc.floorExt.Data.ModelGroups[0].Schema, tc.floorExt.Data.ModelGroups[0].Schema.Delimiter, tc.floorExt.Data.ModelGroups[0].Values) + + if !reflect.DeepEqual(ErrList[0].Error(), tc.Err) { + t.Errorf("Incorrect Error: \nreturn:\t%v\nwant:\t%v", ErrList[0].Error(), tc.Err) + } + + if !reflect.DeepEqual(tc.floorExt.Data.ModelGroups[0].Values, tc.expctedFloor) { + t.Errorf("Mismatch in floor rules: \nreturn:\t%v\nwant:\t%v", tc.floorExt.Data.ModelGroups[0].Values, tc.expctedFloor) + } + + }) + } +} diff --git a/gdpr/vendorlist-fetching.go b/gdpr/vendorlist-fetching.go index 51c41d227f0..c46702735b8 100644 --- a/gdpr/vendorlist-fetching.go +++ b/gdpr/vendorlist-fetching.go @@ -21,6 +21,9 @@ import ( type saveVendors func(uint16, api.VendorList) type VendorListFetcher func(ctx context.Context, id uint16) (vendorlist.VendorList, error) +var cacheSave func(vendorListVersion uint16, list api.VendorList) +var cacheLoad func(vendorListVersion uint16) api.VendorList + // This file provides the vendorlist-fetching function for Prebid Server. // // For more info, see https://github.com/prebid/prebid-server/issues/504 @@ -28,7 +31,7 @@ type VendorListFetcher func(ctx context.Context, id uint16) (vendorlist.VendorLi // Nothing in this file is exported. Public APIs can be found in gdpr.go func NewVendorListFetcher(initCtx context.Context, cfg config.GDPR, client *http.Client, urlMaker func(uint16) string) VendorListFetcher { - cacheSave, cacheLoad := newVendorListCache() + cacheSave, cacheLoad = newVendorListCache() preloadContext, cancel := context.WithTimeout(initCtx, cfg.Timeouts.InitTimeout()) defer cancel() diff --git a/gdpr/vendorlist-scheduler.go b/gdpr/vendorlist-scheduler.go new file mode 100644 index 00000000000..5dc8be88373 --- /dev/null +++ b/gdpr/vendorlist-scheduler.go @@ -0,0 +1,120 @@ +package gdpr + +import ( + "context" + "errors" + "net/http" + "sync" + "time" + + "github.com/golang/glog" +) + +type vendorListScheduler struct { + ticker *time.Ticker + interval time.Duration + done chan bool + isRunning bool + isStarted bool + lastRun time.Time + + httpClient *http.Client + timeout time.Duration +} + +//Only single instance must be created +var _instance *vendorListScheduler +var once sync.Once + +func GetVendorListScheduler(interval, timeout string, httpClient *http.Client) (*vendorListScheduler, error) { + if _instance != nil { + return _instance, nil + } + + intervalDuration, err := time.ParseDuration(interval) + if err != nil { + return nil, errors.New("error parsing vendor list scheduler interval: " + err.Error()) + } + + timeoutDuration, err := time.ParseDuration(timeout) + if err != nil { + return nil, errors.New("error parsing vendor list scheduler timeout: " + err.Error()) + } + + if httpClient == nil { + return nil, errors.New("http-client can not be nil") + } + + once.Do(func() { + _instance = &vendorListScheduler{ + ticker: nil, + interval: intervalDuration, + done: make(chan bool), + httpClient: httpClient, + timeout: timeoutDuration, + } + }) + + return _instance, nil +} + +func (scheduler *vendorListScheduler) Start() { + if scheduler == nil || scheduler.isStarted { + return + } + + scheduler.ticker = time.NewTicker(scheduler.interval) + scheduler.isStarted = true + go func() { + for { + select { + case <-scheduler.done: + scheduler.isRunning = false + scheduler.isStarted = false + scheduler.ticker = nil + return + case t := <-scheduler.ticker.C: + if !scheduler.isRunning { + scheduler.isRunning = true + + glog.Info("Running vendor list scheduler at ", t) + scheduler.runLoadCache() + + scheduler.lastRun = t + scheduler.isRunning = false + } + } + } + }() +} + +func (scheduler *vendorListScheduler) Stop() { + if scheduler == nil || !scheduler.isStarted { + return + } + scheduler.ticker.Stop() + scheduler.done <- true +} + +func (scheduler *vendorListScheduler) runLoadCache() { + if scheduler == nil { + return + } + + preloadContext, cancel := context.WithTimeout(context.Background(), scheduler.timeout) + defer cancel() + + latestVersion := saveOne(preloadContext, scheduler.httpClient, VendorListURLMaker(0), cacheSave) + + // The GVL for TCF2 has no vendors defined in its first version. It's very unlikely to be used, so don't preload it. + firstVersionToLoad := uint16(2) + + for i := latestVersion; i >= firstVersionToLoad; i-- { + // Check if version is present in the cache + if list := cacheLoad(i); list != nil { + continue + } + glog.Infof("Downloading: " + VendorListURLMaker(i)) + saveOne(preloadContext, scheduler.httpClient, VendorListURLMaker(i), cacheSave) + } +} diff --git a/gdpr/vendorlist-scheduler_test.go b/gdpr/vendorlist-scheduler_test.go new file mode 100644 index 00000000000..f343a270b43 --- /dev/null +++ b/gdpr/vendorlist-scheduler_test.go @@ -0,0 +1,194 @@ +package gdpr + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/prebid/go-gdpr/api" + "github.com/stretchr/testify/assert" +) + +func TestGetVendorListScheduler(t *testing.T) { + type args struct { + interval string + timeout string + httpClient *http.Client + } + tests := []struct { + name string + args args + want *vendorListScheduler + wantErr bool + }{ + { + name: "Test singleton", + args: args{ + interval: "1m", + timeout: "1s", + httpClient: http.DefaultClient, + }, + want: GetExpectedVendorListScheduler("1m", "1s", http.DefaultClient), + wantErr: false, + }, + { + name: "Test singleton again", + args: args{ + interval: "2m", + timeout: "2s", + httpClient: http.DefaultClient, + }, + want: GetExpectedVendorListScheduler("2m", "2s", http.DefaultClient), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + //Mark instance as nil for recreating new instance + if tt.want == nil { + //_instance = nil + } + + got, err := GetVendorListScheduler(tt.args.interval, tt.args.timeout, tt.args.httpClient) + if got != tt.want { + t.Errorf("GetVendorListScheduler() got = %v, want %v", got, tt.want) + } + if (err != nil) != tt.wantErr { + t.Errorf("GetVendorListScheduler() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func GetExpectedVendorListScheduler(interval string, timeout string, httpClient *http.Client) *vendorListScheduler { + s, _ := GetVendorListScheduler(interval, timeout, httpClient) + return s +} + +func Test_vendorListScheduler_Start(t *testing.T) { + type fields struct { + scheduler *vendorListScheduler + } + tests := []struct { + name string + fields fields + }{ + { + name: "Start test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheduler, err := GetVendorListScheduler("1m", "30s", http.DefaultClient) + assert.Nil(t, err, "error should be nil") + assert.NotNil(t, scheduler, "scheduler instance should not be nil") + + scheduler.Start() + + assert.NotNil(t, scheduler.ticker, "ticker should not be nil") + assert.True(t, scheduler.isStarted, "isStarted should be true") + + scheduler.Stop() + }) + } +} + +func Test_vendorListScheduler_Stop(t *testing.T) { + type fields struct { + scheduler *vendorListScheduler + } + tests := []struct { + name string + fields fields + }{ + { + name: "Stop test", + }, + { + name: "Calling stop again", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheduler, err := GetVendorListScheduler("1m", "30s", http.DefaultClient) + assert.Nil(t, err, "error should be nil") + assert.NotNil(t, scheduler, "scheduler instance should not be nil") + + scheduler.Start() + scheduler.Stop() + + assert.Nil(t, scheduler.ticker, "ticker should not be nil") + assert.False(t, scheduler.isStarted, "isStarted should be true") + }) + } +} + +func Test_vendorListScheduler_runLoadCache(t *testing.T) { + type fields struct { + scheduler *vendorListScheduler + } + tests := []struct { + name string + fields fields + }{ + { + name: "runLoadCache caches all files", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var err error + tt.fields.scheduler, err = GetVendorListScheduler("5m", "5m", http.DefaultClient) + assert.Nil(t, err, "error should be nil") + assert.False(t, tt.fields.scheduler.isStarted, "VendorListScheduler should not be already running") + + tt.fields.scheduler.timeout = 2 * time.Minute + + mockCacheSave := func(uint16, api.VendorList) {} + latestVersion := saveOne(context.Background(), http.DefaultClient, VendorListURLMaker(0), mockCacheSave) + + cacheSave, cacheLoad = newVendorListCache() + tt.fields.scheduler.runLoadCache() + + firstVersionToLoad := uint16(2) + for i := latestVersion; i >= firstVersionToLoad; i-- { + list := cacheLoad(i) + assert.NotNil(t, list, "vendor-list file should be present in cache") + } + }) + } +} + +func Benchmark_vendorListScheduler_runLoadCache(b *testing.B) { + scheduler, err := GetVendorListScheduler("1m", "30m", http.DefaultClient) + assert.Nil(b, err, "") + assert.NotNil(b, scheduler, "") + + scheduler.timeout = 2 * time.Minute + + for n := 0; n < b.N; n++ { + cacheSave, cacheLoad = newVendorListCache() + scheduler.runLoadCache() + } + +} + +func Test_vendorListScheduler_cacheFuncs(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: vendorList1, + 2: vendorList2, + }, + }))) + defer server.Close() + config := testConfig() + + _ = NewVendorListFetcher(context.Background(), config, server.Client(), testURLMaker(server)) + + assert.NotNil(t, cacheSave, "Error gdpr.cacheSave nil") + assert.NotNil(t, cacheLoad, "Error gdpr.cacheLoad nil") +} diff --git a/go.mod b/go.mod index 91faf67ef73..cf6e44ab63b 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/prebid/prebid-server +module github.com/PubMatic-OpenWrap/prebid-server go 1.16 @@ -7,6 +7,7 @@ require ( github.com/IABTechLab/adscert v0.34.0 github.com/NYTimes/gziphandler v1.1.1 github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d + github.com/beevik/etree v1.0.2 github.com/benbjohnson/clock v1.3.0 github.com/buger/jsonparser v1.1.1 github.com/chasex/glog v0.0.0-20160217080310-c62392af379c @@ -14,11 +15,14 @@ require ( github.com/docker/go-units v0.4.0 github.com/gofrs/uuid v4.2.0+incompatible github.com/golang/glog v1.0.0 + github.com/google/uuid v1.1.2 github.com/julienschmidt/httprouter v1.3.0 github.com/lib/pq v1.10.4 + github.com/magiconair/properties v1.8.6 github.com/mitchellh/copystructure v1.2.0 - github.com/mxmCherry/openrtb/v16 v16.0.0-alpha.2 + github.com/mxmCherry/openrtb/v16 v16.0.0 github.com/prebid/go-gdpr v1.11.0 + github.com/prebid/prebid-server v0.0.0-00010101000000-000000000000 github.com/prometheus/client_golang v1.12.1 github.com/prometheus/client_model v0.2.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 @@ -34,7 +38,12 @@ require ( golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 golang.org/x/text v0.3.7 google.golang.org/grpc v1.46.2 - google.golang.org/protobuf v1.28.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 gopkg.in/yaml.v3 v3.0.1 ) + +replace github.com/prebid/prebid-server => ./ + +replace github.com/mxmCherry/openrtb/v16 => github.com/PubMatic-OpenWrap/openrtb/v16 v16.0.0-ow2 + +replace github.com/beevik/etree v1.0.2 => github.com/PubMatic-OpenWrap/etree v1.0.2-0.20210129100623-8f30cfecf9f4 diff --git a/go.sum b/go.sum index d55a80cdbe6..add0970986b 100644 --- a/go.sum +++ b/go.sum @@ -29,55 +29,78 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.6.1 h1:8rBq3zRjnHx8UtBvaOWqBB1xq9jH6/wltfQLlTMh2Fw= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/IABTechLab/adscert v0.34.0 h1:UNM2gMfRPGUbv3KDiLJmy2ajaVCfF3jWqgVKkz8wBu8= github.com/IABTechLab/adscert v0.34.0/go.mod h1:pCLd3Up1kfTrH6kYFUGGeavxIc1f6Tvvj8yJeFRb7mA= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PubMatic-OpenWrap/etree v1.0.2-0.20210129100623-8f30cfecf9f4 h1:EhiijwjoKTx7FVP8p2wwC/z4n5l4c8l2CGmsrFv2uhI= +github.com/PubMatic-OpenWrap/etree v1.0.2-0.20210129100623-8f30cfecf9f4/go.mod h1:5Y8qgcuDoy3XXG907UXkGnVTwihF16rXyJa4zRT7hOE= +github.com/PubMatic-OpenWrap/openrtb/v16 v16.0.0-ow2 h1:NaKQYOnNyWBEiL1S8t4Xin4e+8UM8W/88ww718a5UEI= +github.com/PubMatic-OpenWrap/openrtb/v16 v16.0.0-ow2/go.mod h1:wjir/n1D39qF0bj1jKjUQJ9oTpdQ53wvvVW7p3Q0qq8= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10 h1:FR+drcQStOe+32sYyJYyZ7FIdgoGGBnwLl+flodp8Uo= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.36.29 h1:lM1G3AF1+7vzFm0n7hfH8r2+750BTo+6Lo6FtPB7kzk= github.com/aws/aws-sdk-go v1.36.29/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -85,10 +108,12 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -97,33 +122,46 @@ github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cb github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chasex/glog v0.0.0-20160217080310-c62392af379c h1:eXqCBUHfmjbeDqcuvzjsd+bM6A+bnwo5N9FVbV6m5/s= github.com/chasex/glog v0.0.0-20160217080310-c62392af379c/go.mod h1:omJZNg0Qu76bxJd+ExohVo8uXzNcGOk2bv7vel460xk= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 h1:KwaoQzs/WeUxxJqiJsZ4euOly1Az/IgZXXSxlD/UBNk= github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coocood/freecache v1.2.1 h1:/v1CqMq45NFH9mp/Pt142reundeBM0dVUD3osQBeu/U= github.com/coocood/freecache v1.2.1/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -134,12 +172,16 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 h1:xvqufLtNVwAhN8NMyWklVgxnWohi+wtMGQMhtxexlm0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.2 h1:JiO+kJTpmYGjEodY7O1Zk8oZcNz1+f30UtwtXoFUPzE= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= @@ -148,24 +190,36 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-ldap/ldap v3.0.2+incompatible h1:kD5HQcAzlQ7yrhfn+h+MSABeAy/jAJhvIJ/QDllP44g= github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31 h1:28FVBuwkwowZMjbA7M0wXsI6t3PYulRTMio3SO+eKCM= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= @@ -173,6 +227,7 @@ github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0L github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -182,6 +237,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -202,8 +258,10 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -220,10 +278,13 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -239,9 +300,13 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -249,91 +314,134 @@ github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pf github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/api v1.12.0 h1:k3y1FYv6nuKyNTqj6w9gXOx5r5CfLj/k/euUeBXj1OY= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= +github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE= github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.5.4 h1:1BZvpawXoJCWX6pNtow9+rpEj+3itIlutiqnntI6jOE= github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0 h1:8+567mCcFDnS5ADl7lrpxPMWiFCElyUEeW0gtj34fMA= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.9.7 h1:hkdgbqizGQHuU5IPqYM1JdSMV8nKfpuOnZYXssk9muY= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/vault/api v1.0.4 h1:j08Or/wryXT4AcHj1oCbMd7IijXcKzYUGw59LGu9onU= github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= +github.com/hashicorp/vault/sdk v0.1.13 h1:mOEPeOhT7jl0J4AMl1E705+BcmeRs1VmKNb9F0sMLy8= github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d h1:/WZQPMZNsjZ7IlCpsLGdQBINg5bxKQ1K1sh6awxLtkA= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lyft/protoc-gen-star v0.5.3 h1:zSGLzsUew8RT+ZKPHc3jnf8XLaVyHzTcAFBzHtCNR20= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= @@ -355,15 +463,20 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.0 h1:tEElEatulEHDeedTxwckzyYMA5c86fbmNIUL1hBIiTg= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -374,17 +487,19 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxmCherry/openrtb/v16 v16.0.0-alpha.2 h1:Iwp8+KHfzDIhaKFynJ4xVstU1LTVLzG+Unur5d0CeWo= -github.com/mxmCherry/openrtb/v16 v16.0.0-alpha.2/go.mod h1:k+21QiTXLLjjXCroputuQNIDJC2GtrzFcHbJbDz4RcE= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= @@ -395,22 +510,26 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug= github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prebid/go-gdpr v1.11.0 h1:QbMjscuw3Ul0mDVWeMy5tP0Kii6lmTSSVhV6fm8rY9s= github.com/prebid/go-gdpr v1.11.0/go.mod h1:mPZAdkRxn+iuSjaUuJAi9+0SppBOdM1PCzv/55UH3pY= @@ -443,24 +562,32 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= @@ -469,6 +596,7 @@ github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfA github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -491,6 +619,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/vrischmann/go-metrics-influxdb v0.1.1 h1:xneKFRjsS4BiVYvAKaM/rOlXYd1pGHksnES0ECCJLgo= github.com/vrischmann/go-metrics-influxdb v0.1.1/go.mod h1:q7YC8bFETCYopXRMtUvQQdLaoVhpsEwvQS2zZEYCqg8= @@ -510,13 +639,18 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.etcd.io/etcd/client/v2 v2.305.4 h1:Dcx3/MYyfKcPNLpR4VVQUP5KgYrBeJtktBwEKkw08Ao= go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= +go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -524,10 +658,15 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0 h1:rwOQPCuKAKmwGKq2aVNnYIibI6wnV7EvzgfTCzcdGg8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -554,8 +693,10 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -568,8 +709,10 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= @@ -580,6 +723,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0 h1:UG21uOlmZabA4fW5i7ZX6bjw1xELEGg/ZLgZq9auk/Q= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -654,6 +798,7 @@ golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -666,6 +811,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -758,6 +904,7 @@ golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -772,6 +919,7 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -828,12 +976,14 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -875,6 +1025,7 @@ google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.81.0 h1:o8WF5AvfidafWbFjsRyupxyEQJNUWxLZJCK5NXrxZZ8= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -882,6 +1033,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -999,6 +1151,7 @@ google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11 google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1015,19 +1168,24 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -1051,8 +1209,13 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/main.go b/main.go index c103863107d..6e1c366f5b8 100644 --- a/main.go +++ b/main.go @@ -1,30 +1,25 @@ -package main +package prebidServer import ( - "flag" "math/rand" "net/http" "runtime" "time" + "github.com/golang/glog" + "github.com/prebid/prebid-server/usersync" + "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/currency" "github.com/prebid/prebid-server/router" - "github.com/prebid/prebid-server/server" "github.com/prebid/prebid-server/util/task" - "github.com/golang/glog" "github.com/spf13/viper" ) -func init() { +func InitPrebidServer(configFile string) { rand.Seed(time.Now().UnixNano()) -} - -func main() { - flag.Parse() // required for glog flags and testing package flags - - cfg, err := loadConfig() + cfg, err := loadConfig(configFile) if err != nil { glog.Exitf("Configuration could not be loaded or did not pass validation: %v", err) } @@ -43,11 +38,11 @@ func main() { } } -const configFileName = "pbs" - -func loadConfig() (*config.Configuration, error) { +func loadConfig(configFileName string) (*config.Configuration, error) { v := viper.New() config.SetupViper(v, configFileName) + v.SetConfigFile(configFileName) + v.ReadInConfig() return config.New(v) } @@ -59,14 +54,34 @@ func serve(cfg *config.Configuration) error { currencyConverterTickerTask := task.NewTickerTask(fetchingInterval, currencyConverter) currencyConverterTickerTask.Start() - r, err := router.New(cfg, currencyConverter) + _, err := router.New(cfg, currencyConverter) if err != nil { return err } - corsRouter := router.SupportCORS(r) - server.Listen(cfg, router.NoCache{Handler: corsRouter}, router.Admin(currencyConverter, fetchingInterval), r.MetricsEngine) - - r.Shutdown() return nil } + +func OrtbAuction(w http.ResponseWriter, r *http.Request) error { + return router.OrtbAuctionEndpointWrapper(w, r) +} + +var VideoAuction = func(w http.ResponseWriter, r *http.Request) error { + return router.VideoAuctionEndpointWrapper(w, r) +} + +func GetUIDS(w http.ResponseWriter, r *http.Request) { + router.GetUIDSWrapper(w, r) +} + +func SetUIDS(w http.ResponseWriter, r *http.Request) { + router.SetUIDSWrapper(w, r) +} + +func CookieSync(w http.ResponseWriter, r *http.Request) { + router.CookieSync(w, r) +} + +func SyncerMap() map[string]usersync.Syncer { + return router.SyncerMap() +} diff --git a/main_test.go b/main_test.go index 70eea2825f0..1d2ec332164 100644 --- a/main_test.go +++ b/main_test.go @@ -1,4 +1,4 @@ -package main +package prebidServer import ( "os" diff --git a/metrics/config/metrics.go b/metrics/config/metrics.go index 4079a785e3c..5e884923c6a 100644 --- a/metrics/config/metrics.go +++ b/metrics/config/metrics.go @@ -128,6 +128,13 @@ func (me *MultiMetricsEngine) RecordAdapterRequest(labels metrics.AdapterLabels) } } +// RecordRejectedBidsForBidder across all engines +func (me *MultiMetricsEngine) RecordRejectedBidsForBidder(bidder openrtb_ext.BidderName) { + for _, thisME := range *me { + thisME.RecordRejectedBidsForBidder(bidder) + } +} + // Keeps track of created and reused connections to adapter bidders and the time from the // connection request, to the connection creation, or reuse from the pool across all engines func (me *MultiMetricsEngine) RecordAdapterConnections(bidderName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) { @@ -143,9 +150,9 @@ func (me *MultiMetricsEngine) RecordDNSTime(dnsLookupTime time.Duration) { } } -func (me *MultiMetricsEngine) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) { +func (me *MultiMetricsEngine) RecordTLSHandshakeTime(adapterName openrtb_ext.BidderName, tlsHandshakeTime time.Duration) { for _, thisME := range *me { - thisME.RecordTLSHandshakeTime(tlsHandshakeTime) + thisME.RecordTLSHandshakeTime(adapterName, tlsHandshakeTime) } } @@ -247,6 +254,52 @@ func (me *MultiMetricsEngine) RecordRequestPrivacy(privacy metrics.PrivacyLabels } } +// RecordAdapterDuplicateBidID across all engines +func (me *MultiMetricsEngine) RecordAdapterDuplicateBidID(adaptor string, collisions int) { + for _, thisME := range *me { + thisME.RecordAdapterDuplicateBidID(adaptor, collisions) + } +} + +// RecordRequestHavingDuplicateBidID across all engines +func (me *MultiMetricsEngine) RecordRequestHavingDuplicateBidID() { + for _, thisME := range *me { + thisME.RecordRequestHavingDuplicateBidID() + } +} + +// RecordPodImpGenTime across all engines +func (me *MultiMetricsEngine) RecordPodImpGenTime(labels metrics.PodLabels, startTime time.Time) { + for _, thisME := range *me { + thisME.RecordPodImpGenTime(labels, startTime) + } +} + +// RecordRejectedBidsForBidder as a noop +func (me *NilMetricsEngine) RecordRejectedBidsForBidder(bidder openrtb_ext.BidderName) { +} + +// RecordPodCombGenTime as a noop +func (me *MultiMetricsEngine) RecordPodCombGenTime(labels metrics.PodLabels, elapsedTime time.Duration) { + for _, thisME := range *me { + thisME.RecordPodCombGenTime(labels, elapsedTime) + } +} + +// RecordPodCompititveExclusionTime as a noop +func (me *MultiMetricsEngine) RecordPodCompititveExclusionTime(labels metrics.PodLabels, elapsedTime time.Duration) { + for _, thisME := range *me { + thisME.RecordPodCompititveExclusionTime(labels, elapsedTime) + } +} + +// RecordAdapterVideoBidDuration as a noop +func (me *MultiMetricsEngine) RecordAdapterVideoBidDuration(labels metrics.AdapterLabels, videoBidDuration int) { + for _, thisME := range *me { + thisME.RecordAdapterVideoBidDuration(labels, videoBidDuration) + } +} + // RecordAdapterGDPRRequestBlocked across all engines func (me *MultiMetricsEngine) RecordAdapterGDPRRequestBlocked(adapter openrtb_ext.BidderName) { for _, thisME := range *me { @@ -267,6 +320,18 @@ func (me *MultiMetricsEngine) RecordStoredResponse(pubId string) { } } +func (me *MultiMetricsEngine) RecordRejectedBidsForAccount(pubId string) { + for _, thisME := range *me { + thisME.RecordRejectedBidsForAccount(pubId) + } +} + +func (me *MultiMetricsEngine) RecordFloorsRequestForAccount(pubId string) { + for _, thisME := range *me { + thisME.RecordFloorsRequestForAccount(pubId) + } +} + func (me *MultiMetricsEngine) RecordAdsCertReq(success bool) { for _, thisME := range *me { thisME.RecordAdsCertReq(success) @@ -283,6 +348,24 @@ func (me *MultiMetricsEngine) RecordAdsCertSignTime(adsCertSignTime time.Duratio // used if no metric backend is configured and also for tests. type NilMetricsEngine struct{} +func (me *NilMetricsEngine) RecordAdapterDuplicateBidID(adaptor string, collisions int) { +} + +func (me *NilMetricsEngine) RecordRequestHavingDuplicateBidID() { +} + +func (me *NilMetricsEngine) RecordPodImpGenTime(labels metrics.PodLabels, startTime time.Time) { +} + +func (me *NilMetricsEngine) RecordPodCombGenTime(labels metrics.PodLabels, elapsedTime time.Duration) { +} + +func (me *NilMetricsEngine) RecordPodCompititveExclusionTime(labels metrics.PodLabels, elapsedTime time.Duration) { +} + +func (me *NilMetricsEngine) RecordAdapterVideoBidDuration(labels metrics.AdapterLabels, videoBidDuration int) { +} + // RecordRequest as a noop func (me *NilMetricsEngine) RecordRequest(labels metrics.Labels) { } @@ -328,7 +411,7 @@ func (me *NilMetricsEngine) RecordDNSTime(dnsLookupTime time.Duration) { } // RecordTLSHandshakeTime as a noop -func (me *NilMetricsEngine) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) { +func (me *NilMetricsEngine) RecordTLSHandshakeTime(adapterName openrtb_ext.BidderName, tlsHandshakeTime time.Duration) { } // RecordAdapterBidReceived as a noop @@ -398,6 +481,12 @@ func (me *NilMetricsEngine) RecordDebugRequest(debugEnabled bool, pubId string) func (me *NilMetricsEngine) RecordStoredResponse(pubId string) { } +func (me *NilMetricsEngine) RecordRejectedBidsForAccount(pubId string) { +} + +func (me *NilMetricsEngine) RecordFloorsRequestForAccount(pubId string) { +} + func (me *NilMetricsEngine) RecordAdsCertReq(success bool) { } diff --git a/metrics/go_metrics.go b/metrics/go_metrics.go index 9e6d8d1ba4f..e619e75d1ac 100644 --- a/metrics/go_metrics.go +++ b/metrics/go_metrics.go @@ -31,19 +31,19 @@ type Metrics struct { StoredImpCacheMeter map[CacheResult]metrics.Meter AccountCacheMeter map[CacheResult]metrics.Meter DNSLookupTimer metrics.Timer - TLSHandshakeTimer metrics.Timer StoredResponsesMeter metrics.Meter // Metrics for OpenRTB requests specifically. So we can track what % of RequestsMeter are OpenRTB // and know when legacy requests have been abandoned. - RequestStatuses map[RequestType]map[RequestStatus]metrics.Meter - AmpNoCookieMeter metrics.Meter - CookieSyncMeter metrics.Meter - CookieSyncStatusMeter map[CookieSyncStatus]metrics.Meter - SyncerRequestsMeter map[string]map[SyncerCookieSyncStatus]metrics.Meter - SetUidMeter metrics.Meter - SetUidStatusMeter map[SetUidStatus]metrics.Meter - SyncerSetsMeter map[string]map[SyncerSetUidStatus]metrics.Meter + RequestStatuses map[RequestType]map[RequestStatus]metrics.Meter + AmpNoCookieMeter metrics.Meter + CookieSyncMeter metrics.Meter + CookieSyncStatusMeter map[CookieSyncStatus]metrics.Meter + SyncerRequestsMeter map[string]map[SyncerCookieSyncStatus]metrics.Meter + SetUidMeter metrics.Meter + SetUidStatusMeter map[SetUidStatus]metrics.Meter + SyncerSetsMeter map[string]map[SyncerSetUidStatus]metrics.Meter + FloorRejectedBidsMeter map[openrtb_ext.BidderName]metrics.Meter // Media types found in the "imp" JSON object ImpsTypeBanner metrics.Meter @@ -62,6 +62,20 @@ type Metrics struct { PrivacyLMTRequest metrics.Meter PrivacyTCFRequestVersion map[TCFVersionValue]metrics.Meter + // Ad Pod Metrics + + // podImpGenTimer indicates time taken by impression generator + // algorithm to generate impressions for given ad pod request + podImpGenTimer metrics.Timer + + // podImpGenTimer indicates time taken by combination generator + // algorithm to generate combination based on bid response and ad pod request + podCombGenTimer metrics.Timer + + // podCompExclTimer indicates time taken by compititve exclusion + // algorithm to generate final pod response based on bid response and ad pod request + podCompExclTimer metrics.Timer + AdapterMetrics map[openrtb_ext.BidderName]*AdapterMetrics // Don't export accountMetrics because we need helper functions here to insure its properly populated dynamically accountMetrics map[string]*accountMetrics @@ -92,6 +106,7 @@ type AdapterMetrics struct { ConnReused metrics.Counter ConnWaitTime metrics.Timer GDPRRequestBlocked metrics.Meter + TLSHandshakeTimer metrics.Timer } type MarkupDeliveryMetrics struct { @@ -100,10 +115,12 @@ type MarkupDeliveryMetrics struct { } type accountMetrics struct { - requestMeter metrics.Meter - debugRequestMeter metrics.Meter - bidsReceivedMeter metrics.Meter - priceHistogram metrics.Histogram + requestMeter metrics.Meter + rejecteBidMeter metrics.Meter + floorsRequestMeter metrics.Meter + debugRequestMeter metrics.Meter + bidsReceivedMeter metrics.Meter + priceHistogram metrics.Histogram // store account by adapter metrics. Type is map[PBSBidder.BidderCode] adapterMetrics map[openrtb_ext.BidderName]*AdapterMetrics storedResponsesMeter metrics.Meter @@ -132,7 +149,6 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa NoCookieMeter: blankMeter, RequestTimer: blankTimer, DNSLookupTimer: blankTimer, - TLSHandshakeTimer: blankTimer, RequestsQueueTimer: make(map[RequestType]map[bool]metrics.Timer), PrebidCacheRequestTimerSuccess: blankTimer, PrebidCacheRequestTimerError: blankTimer, @@ -149,6 +165,7 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa SetUidStatusMeter: make(map[SetUidStatus]metrics.Meter), SyncerSetsMeter: make(map[string]map[SyncerSetUidStatus]metrics.Meter), StoredResponsesMeter: blankMeter, + FloorRejectedBidsMeter: make(map[openrtb_ext.BidderName]metrics.Meter), ImpsTypeBanner: blankMeter, ImpsTypeVideo: blankMeter, @@ -237,7 +254,6 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d newMetrics.DebugRequestMeter = metrics.GetOrRegisterMeter("debug_requests", registry) newMetrics.RequestTimer = metrics.GetOrRegisterTimer("request_time", registry) newMetrics.DNSLookupTimer = metrics.GetOrRegisterTimer("dns_lookup_time", registry) - newMetrics.TLSHandshakeTimer = metrics.GetOrRegisterTimer("tls_handshake_time", registry) newMetrics.PrebidCacheRequestTimerSuccess = metrics.GetOrRegisterTimer("prebid_cache_request_time.ok", registry) newMetrics.PrebidCacheRequestTimerError = metrics.GetOrRegisterTimer("prebid_cache_request_time.err", registry) newMetrics.StoredResponsesMeter = metrics.GetOrRegisterMeter("stored_responses", registry) @@ -279,6 +295,7 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d for _, a := range exchanges { registerAdapterMetrics(registry, "adapter", string(a), newMetrics.AdapterMetrics[a]) + newMetrics.FloorRejectedBidsMeter[a] = metrics.GetOrRegisterMeter(fmt.Sprintf("rejected_bid.%s", a), registry) } for typ, statusMap := range newMetrics.RequestStatuses { @@ -332,6 +349,7 @@ func makeBlankAdapterMetrics(disabledMetrics config.DisabledMetrics) *AdapterMet newAdapter.ConnCreated = metrics.NilCounter{} newAdapter.ConnReused = metrics.NilCounter{} newAdapter.ConnWaitTime = &metrics.NilTimer{} + newAdapter.TLSHandshakeTimer = &metrics.NilTimer{} } if !disabledMetrics.AdapterGDPRRequestBlocked { newAdapter.GDPRRequestBlocked = blankMeter @@ -373,6 +391,7 @@ func registerAdapterMetrics(registry metrics.Registry, adapterOrAccount string, am.ConnCreated = metrics.GetOrRegisterCounter(fmt.Sprintf("%[1]s.%[2]s.connections_created", adapterOrAccount, exchange), registry) am.ConnReused = metrics.GetOrRegisterCounter(fmt.Sprintf("%[1]s.%[2]s.connections_reused", adapterOrAccount, exchange), registry) am.ConnWaitTime = metrics.GetOrRegisterTimer(fmt.Sprintf("%[1]s.%[2]s.connection_wait_time", adapterOrAccount, exchange), registry) + am.TLSHandshakeTimer = metrics.GetOrRegisterTimer(fmt.Sprintf("%[1]s.%[2]s.tls_handshake_time", adapterOrAccount, exchange), registry) for err := range am.ErrorMeters { am.ErrorMeters[err] = metrics.GetOrRegisterMeter(fmt.Sprintf("%s.%s.requests.%s", adapterOrAccount, exchange, err), registry) } @@ -415,6 +434,8 @@ func (me *Metrics) getAccountMetrics(id string) *accountMetrics { } am = &accountMetrics{} am.requestMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("account.%s.requests", id), me.MetricsRegistry) + am.rejecteBidMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("account.%s.rejected_bidrequests", id), me.MetricsRegistry) + am.floorsRequestMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("account.%s.bidfloor_requests", id), me.MetricsRegistry) am.debugRequestMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("account.%s.debug_requests", id), me.MetricsRegistry) am.bidsReceivedMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("account.%s.bids_received", id), me.MetricsRegistry) am.priceHistogram = metrics.GetOrRegisterHistogram(fmt.Sprintf("account.%s.prices", id), me.MetricsRegistry, metrics.NewExpDecaySample(1028, 0.015)) @@ -472,6 +493,18 @@ func (me *Metrics) RecordStoredResponse(pubId string) { } } +func (me *Metrics) RecordRejectedBidsForAccount(pubId string) { + if pubId != PublisherUnknown { + me.getAccountMetrics(pubId).rejecteBidMeter.Mark(1) + } +} + +func (me *Metrics) RecordFloorsRequestForAccount(pubId string) { + if pubId != PublisherUnknown { + me.getAccountMetrics(pubId).floorsRequestMeter.Mark(1) + } +} + func (me *Metrics) RecordImps(labels ImpLabels) { me.ImpMeter.Mark(int64(1)) if labels.BannerImps { @@ -593,8 +626,18 @@ func (me *Metrics) RecordDNSTime(dnsLookupTime time.Duration) { me.DNSLookupTimer.Update(dnsLookupTime) } -func (me *Metrics) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) { - me.TLSHandshakeTimer.Update(tlsHandshakeTime) +func (me *Metrics) RecordTLSHandshakeTime(adapterName openrtb_ext.BidderName, tlsHandshakeTime time.Duration) { + if me.MetricsDisabled.AdapterConnectionMetrics { + return + } + + am, ok := me.AdapterMetrics[adapterName] + if !ok { + glog.Errorf("Trying to log adapter TLS Handshake metrics for %s: adapter not found", string(adapterName)) + return + } + + am.TLSHandshakeTimer.Update(tlsHandshakeTime) } // RecordAdapterBidReceived implements a part of the MetricsEngine interface. @@ -639,6 +682,13 @@ func (me *Metrics) RecordAdapterPrice(labels AdapterLabels, cpm float64) { } } +// RecordRejectedBidsForBidder implements a part of the MetricsEngine interface. Records rejected bids from bidder +func (me *Metrics) RecordRejectedBidsForBidder(bidder openrtb_ext.BidderName) { + if keyMeter, exists := me.FloorRejectedBidsMeter[bidder]; exists { + keyMeter.Mark(1) + } +} + // RecordAdapterTime implements a part of the MetricsEngine interface. Records the adapter response time func (me *Metrics) RecordAdapterTime(labels AdapterLabels, length time.Duration) { am, ok := me.AdapterMetrics[labels.Adapter] diff --git a/metrics/go_metrics_ow.go b/metrics/go_metrics_ow.go new file mode 100644 index 00000000000..96fab9a3853 --- /dev/null +++ b/metrics/go_metrics_ow.go @@ -0,0 +1,27 @@ +package metrics + +import "time" + +// RecordAdapterDuplicateBidID as noop +func (me *Metrics) RecordAdapterDuplicateBidID(adaptor string, collisions int) { +} + +// RecordRequestHavingDuplicateBidID as noop +func (me *Metrics) RecordRequestHavingDuplicateBidID() { +} + +// RecordPodImpGenTime as a noop +func (me *Metrics) RecordPodImpGenTime(labels PodLabels, startTime time.Time) { +} + +// RecordPodCombGenTime as a noop +func (me *Metrics) RecordPodCombGenTime(labels PodLabels, elapsedTime time.Duration) { +} + +// RecordPodCompititveExclusionTime as a noop +func (me *Metrics) RecordPodCompititveExclusionTime(labels PodLabels, elapsedTime time.Duration) { +} + +// RecordAdapterVideoBidDuration as a noop +func (me *Metrics) RecordAdapterVideoBidDuration(labels AdapterLabels, videoBidDuration int) { +} diff --git a/metrics/go_metrics_test.go b/metrics/go_metrics_test.go index 709519e0238..cf8cb4cfffe 100644 --- a/metrics/go_metrics_test.go +++ b/metrics/go_metrics_test.go @@ -280,29 +280,51 @@ func TestRecordDNSTime(t *testing.T) { } func TestRecordTLSHandshakeTime(t *testing.T) { + type testIn struct { + adapterName openrtb_ext.BidderName + tLSHandshakeDuration time.Duration + adapterMetricsEnabled bool + } + + type testOut struct { + expectedDuration time.Duration + } + testCases := []struct { - description string - tLSHandshakeDuration time.Duration - expectedDuration time.Duration + description string + in testIn + out testOut }{ { - description: "Five second TLS handshake time", - tLSHandshakeDuration: time.Second * 5, - expectedDuration: time.Second * 5, + description: "Five second TLS handshake time", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + tLSHandshakeDuration: time.Second * 5, + adapterMetricsEnabled: true, + }, + out: testOut{ + expectedDuration: time.Second * 5, + }, }, { - description: "Zero TLS handshake time", - tLSHandshakeDuration: time.Duration(0), - expectedDuration: time.Duration(0), + description: "Zero TLS handshake time", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + tLSHandshakeDuration: time.Duration(0), + adapterMetricsEnabled: true, + }, + out: testOut{ + expectedDuration: time.Duration(0), + }, }, } for _, test := range testCases { registry := metrics.NewRegistry() m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AccountAdapterDetails: true}, nil) - m.RecordTLSHandshakeTime(test.tLSHandshakeDuration) + m.RecordTLSHandshakeTime(test.in.adapterName, test.in.tLSHandshakeDuration) - assert.Equal(t, test.expectedDuration.Nanoseconds(), m.TLSHandshakeTimer.Sum(), test.description) + assert.Equal(t, test.out.expectedDuration.Nanoseconds(), m.AdapterMetrics[openrtb_ext.BidderAppnexus].TLSHandshakeTimer.Sum(), test.description) } } @@ -743,6 +765,25 @@ func TestRecordSyncerSet(t *testing.T) { assert.Equal(t, m.SyncerSetsMeter["foo"][SyncerSetUidCleared].Count(), int64(1)) } +func TestRecordRejectedBidsForBidders(t *testing.T) { + registry := metrics.NewRegistry() + m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon, openrtb_ext.BidderPubmatic}, config.DisabledMetrics{}, nil) + + m.RecordFloorsRequestForAccount("1234") + + m.RecordRejectedBidsForAccount("1234") + m.RecordRejectedBidsForBidder(openrtb_ext.BidderAppnexus) + m.RecordRejectedBidsForBidder(openrtb_ext.BidderAppnexus) + + m.RecordRejectedBidsForBidder(openrtb_ext.BidderRubicon) + + assert.Equal(t, m.accountMetrics["1234"].floorsRequestMeter.Count(), int64(1)) + assert.Equal(t, m.accountMetrics["1234"].rejecteBidMeter.Count(), int64(1)) + assert.Equal(t, m.FloorRejectedBidsMeter[openrtb_ext.BidderAppnexus].Count(), int64(2)) + assert.Equal(t, m.FloorRejectedBidsMeter[openrtb_ext.BidderRubicon].Count(), int64(1)) + assert.Equal(t, m.FloorRejectedBidsMeter[openrtb_ext.BidderPubmatic].Count(), int64(0)) +} + func TestStoredResponses(t *testing.T) { testCases := []struct { description string diff --git a/metrics/metrics.go b/metrics/metrics.go index 51faafb1c63..4d197ca00d9 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -34,6 +34,15 @@ type ImpLabels struct { NativeImps bool } +// PodLabels defines metric labels describing algorithm type +// and other labels as per scenario +type PodLabels struct { + AlgorithmName string // AlgorithmName which is used for generating impressions + NoOfImpressions *int // NoOfImpressions represents number of impressions generated + NoOfCombinations *int // NoOfCombinations represents number of combinations generated + NoOfResponseBids *int // NoOfResponseBids represents number of bids responded (including bids with similar duration) +} + // RequestLabels defines metric labels describing the result of a network request. type RequestLabels struct { RequestStatus RequestStatus @@ -398,7 +407,7 @@ type MetricsEngine interface { RecordAdapterRequest(labels AdapterLabels) RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) RecordDNSTime(dnsLookupTime time.Duration) - RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) + RecordTLSHandshakeTime(adapterName openrtb_ext.BidderName, tlsHandshakeTime time.Duration) RecordAdapterPanic(labels AdapterLabels) // This records whether or not a bid of a particular type uses `adm` or `nurl`. // Since the legacy endpoints don't have a bid type, it can only count bids from OpenRTB and AMP. @@ -409,6 +418,9 @@ type MetricsEngine interface { RecordSyncerRequest(key string, status SyncerCookieSyncStatus) RecordSetUid(status SetUidStatus) RecordSyncerSet(key string, status SyncerSetUidStatus) + RecordRejectedBidsForBidder(bidder openrtb_ext.BidderName) + RecordRejectedBidsForAccount(pubId string) + RecordFloorsRequestForAccount(pubId string) RecordStoredReqCacheResult(cacheResult CacheResult, inc int) RecordStoredImpCacheResult(cacheResult CacheResult, inc int) RecordAccountCacheResult(cacheResult CacheResult, inc int) @@ -423,4 +435,38 @@ type MetricsEngine interface { RecordStoredResponse(pubId string) RecordAdsCertReq(success bool) RecordAdsCertSignTime(adsCertSignTime time.Duration) + + // RecordAdapterDuplicateBidID captures the bid.ID collisions when adaptor + // gives the bid response with multiple bids containing same bid.ID + RecordAdapterDuplicateBidID(adaptor string, collisions int) + + // RecordRequestHavingDuplicateBidID keeps track off how many request got bid.id collision + // detected + RecordRequestHavingDuplicateBidID() + + // ad pod specific metrics + + // RecordPodImpGenTime records number of impressions generated and time taken + // by underneath algorithm to generate them + // labels accept name of the algorithm and no of impressions generated + // startTime indicates the time at which algorithm started + // This function will take care of computing the elpased time + RecordPodImpGenTime(labels PodLabels, startTime time.Time) + + // RecordPodCombGenTime records number of combinations generated and time taken + // by underneath algorithm to generate them + // labels accept name of the algorithm and no of combinations generated + // elapsedTime indicates the time taken by combination generator to compute all requested combinations + // This function will take care of computing the elpased time + RecordPodCombGenTime(labels PodLabels, elapsedTime time.Duration) + + // RecordPodCompititveExclusionTime records time take by competitive exclusion + // to compute the final Ad pod Response. + // labels accept name of the algorithm and no of combinations evaluated, total bids + // elapsedTime indicates the time taken by competitive exclusion to form final ad pod response using combinations and exclusion algorithm + // This function will take care of computing the elpased time + RecordPodCompititveExclusionTime(labels PodLabels, elapsedTime time.Duration) + + //RecordAdapterVideoBidDuration records actual ad duration returned by the bidder + RecordAdapterVideoBidDuration(labels AdapterLabels, videoBidDuration int) } diff --git a/metrics/metrics_mock.go b/metrics/metrics_mock.go index 586a4783ef8..f147cd9cf48 100644 --- a/metrics/metrics_mock.go +++ b/metrics/metrics_mock.go @@ -67,8 +67,8 @@ func (me *MetricsEngineMock) RecordDNSTime(dnsLookupTime time.Duration) { me.Called(dnsLookupTime) } -func (me *MetricsEngineMock) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) { - me.Called(tlsHandshakeTime) +func (me *MetricsEngineMock) RecordTLSHandshakeTime(bidderName openrtb_ext.BidderName, tlsHandshakeTime time.Duration) { + me.Called(bidderName, tlsHandshakeTime) } // RecordAdapterBidReceived mock @@ -106,6 +106,10 @@ func (me *MetricsEngineMock) RecordSyncerSet(key string, status SyncerSetUidStat me.Called(key, status) } +func (me *MetricsEngineMock) RecordRejectedBidsForBidder(bidder openrtb_ext.BidderName) { + me.Called(bidder) +} + // RecordStoredReqCacheResult mock func (me *MetricsEngineMock) RecordStoredReqCacheResult(cacheResult CacheResult, inc int) { me.Called(cacheResult, inc) @@ -155,6 +159,14 @@ func (me *MetricsEngineMock) RecordStoredResponse(pubId string) { me.Called(pubId) } +func (me *MetricsEngineMock) RecordRejectedBidsForAccount(pubId string) { + me.Called(pubId) +} + +func (me *MetricsEngineMock) RecordFloorsRequestForAccount(pubId string) { + me.Called(pubId) +} + func (me *MetricsEngineMock) RecordAdsCertReq(success bool) { me.Called(success) } diff --git a/metrics/metrics_mock_ow.go b/metrics/metrics_mock_ow.go new file mode 100644 index 00000000000..61e737e107e --- /dev/null +++ b/metrics/metrics_mock_ow.go @@ -0,0 +1,33 @@ +package metrics + +import "time" + +// RecordAdapterDuplicateBidID mock +func (me *MetricsEngineMock) RecordAdapterDuplicateBidID(adaptor string, collisions int) { + me.Called(adaptor, collisions) +} + +// RecordRequestHavingDuplicateBidID mock +func (me *MetricsEngineMock) RecordRequestHavingDuplicateBidID() { + me.Called() +} + +// RecordPodImpGenTime mock +func (me *MetricsEngineMock) RecordPodImpGenTime(labels PodLabels, startTime time.Time) { + me.Called(labels, startTime) +} + +// RecordPodCombGenTime mock +func (me *MetricsEngineMock) RecordPodCombGenTime(labels PodLabels, elapsedTime time.Duration) { + me.Called(labels, elapsedTime) +} + +// RecordPodCompititveExclusionTime mock +func (me *MetricsEngineMock) RecordPodCompititveExclusionTime(labels PodLabels, elapsedTime time.Duration) { + me.Called(labels, elapsedTime) +} + +//RecordAdapterVideoBidDuration mock +func (me *MetricsEngineMock) RecordAdapterVideoBidDuration(labels AdapterLabels, videoBidDuration int) { + me.Called(labels, videoBidDuration) +} diff --git a/metrics/prometheus/prometheus.go b/metrics/prometheus/prometheus.go index df54df92248..e9a692fe7ea 100644 --- a/metrics/prometheus/prometheus.go +++ b/metrics/prometheus/prometheus.go @@ -46,7 +46,6 @@ type Metrics struct { storedVideoErrors *prometheus.CounterVec timeoutNotifications *prometheus.CounterVec dnsLookupTimer prometheus.Histogram - tlsHandhakeTimer prometheus.Histogram privacyCCPA *prometheus.CounterVec privacyCOPPA *prometheus.CounterVec privacyLMT *prometheus.CounterVec @@ -57,6 +56,8 @@ type Metrics struct { adsCertRequests *prometheus.CounterVec adsCertSignTimer prometheus.Histogram + requestsDuplicateBidIDCounter prometheus.Counter // total request having duplicate bid.id for given bidder + // Adapter Metrics adapterBids *prometheus.CounterVec adapterErrors *prometheus.CounterVec @@ -69,15 +70,38 @@ type Metrics struct { adapterConnectionWaitTime *prometheus.HistogramVec adapterGDPRBlockedRequests *prometheus.CounterVec + adapterDuplicateBidIDCounter *prometheus.CounterVec + adapterVideoBidDuration *prometheus.HistogramVec + tlsHandhakeTimer *prometheus.HistogramVec + // Syncer Metrics syncerRequests *prometheus.CounterVec syncerSets *prometheus.CounterVec + // Rejected Bids + rejectedBids *prometheus.CounterVec + accountRejectedBid *prometheus.CounterVec + accountFloorsRequest *prometheus.CounterVec + // Account Metrics accountRequests *prometheus.CounterVec accountDebugRequests *prometheus.CounterVec accountStoredResponses *prometheus.CounterVec + // Ad Pod Metrics + + // podImpGenTimer indicates time taken by impression generator + // algorithm to generate impressions for given ad pod request + podImpGenTimer *prometheus.HistogramVec + + // podImpGenTimer indicates time taken by combination generator + // algorithm to generate combination based on bid response and ad pod request + podCombGenTimer *prometheus.HistogramVec + + // podCompExclTimer indicates time taken by compititve exclusion + // algorithm to generate final pod response based on bid response and ad pod request + podCompExclTimer *prometheus.HistogramVec + metricsDisabled config.DisabledMetrics } @@ -126,6 +150,14 @@ const ( requestFailed = "failed" ) +// pod specific constants +const ( + podAlgorithm = "algorithm" + podNoOfImpressions = "no_of_impressions" + podTotalCombinations = "total_combinations" + podNoOfResponseBids = "no_of_response_bids" +) + const ( sourceLabel = "source" sourceRequest = "request" @@ -285,10 +317,10 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet "Seconds to resolve DNS", standardTimeBuckets) - metrics.tlsHandhakeTimer = newHistogram(cfg, reg, - "tls_handshake_time", - "Seconds to perform TLS Handshake", - standardTimeBuckets) + // metrics.tlsHandhakeTimer = newHistogram(cfg, reg, + // "tls_handshake_time", + // "Seconds to perform TLS Handshake", + // standardTimeBuckets) metrics.privacyCCPA = newCounter(cfg, reg, "privacy_ccpa", @@ -374,6 +406,12 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet "Seconds from when the connection was requested until it is either created or reused", []string{adapterLabel}, standardTimeBuckets) + + metrics.tlsHandhakeTimer = newHistogramVec(cfg, reg, + "tls_handshake_time", + "Seconds to perform TLS Handshake", + []string{adapterLabel}, + standardTimeBuckets) } metrics.adapterRequestsTimer = newHistogramVec(cfg, reg, @@ -413,6 +451,21 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet "Count of total requests to Prebid Server that have stored responses labled by account", []string{accountLabel}) + metrics.accountRejectedBid = newCounter(cfg, reg, + "floors_account_rejected_bid_requests", + "Count of total requests to Prebid Server that have rejected bids due to floors enfocement labled by account", + []string{accountLabel}) + + metrics.accountFloorsRequest = newCounter(cfg, reg, + "floors_account_requests", + "Count of total requests to Prebid Server that have non-zero imp.bidfloor labled by account", + []string{accountLabel}) + + metrics.rejectedBids = newCounter(cfg, reg, + "floors_partner_rejected_bids", + "Count of rejected bids due to floors enforcement per partner.", + []string{adapterLabel}) + metrics.adsCertSignTimer = newHistogram(cfg, reg, "ads_cert_sign_time", "Seconds to generate an AdsCert header", @@ -436,6 +489,43 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet metrics.Registerer = prometheus.WrapRegistererWithPrefix(metricsPrefix, reg) metrics.Registerer.MustRegister(promCollector.NewGoCollector()) + metrics.adapterDuplicateBidIDCounter = newCounter(cfg, reg, + "duplicate_bid_ids", + "Number of collisions observed for given adaptor", + []string{adapterLabel}) + + metrics.requestsDuplicateBidIDCounter = newCounterWithoutLabels(cfg, reg, + "requests_having_duplicate_bid_ids", + "Count of number of request where bid collision is detected.") + + // adpod specific metrics + metrics.podImpGenTimer = newHistogramVec(cfg, reg, + "impr_gen", + "Time taken by Ad Pod Impression Generator in seconds", []string{podAlgorithm, podNoOfImpressions}, + // 200 µS, 250 µS, 275 µS, 300 µS + //[]float64{0.000200000, 0.000250000, 0.000275000, 0.000300000}) + // 100 µS, 200 µS, 300 µS, 400 µS, 500 µS, 600 µS, + []float64{0.000100000, 0.000200000, 0.000300000, 0.000400000, 0.000500000, 0.000600000}) + + metrics.podCombGenTimer = newHistogramVec(cfg, reg, + "comb_gen", + "Time taken by Ad Pod Combination Generator in seconds", []string{podAlgorithm, podTotalCombinations}, + // 200 µS, 250 µS, 275 µS, 300 µS + //[]float64{0.000200000, 0.000250000, 0.000275000, 0.000300000}) + []float64{0.000100000, 0.000200000, 0.000300000, 0.000400000, 0.000500000, 0.000600000}) + + metrics.podCompExclTimer = newHistogramVec(cfg, reg, + "comp_excl", + "Time taken by Ad Pod Compititve Exclusion in seconds", []string{podAlgorithm, podNoOfResponseBids}, + // 200 µS, 250 µS, 275 µS, 300 µS + //[]float64{0.000200000, 0.000250000, 0.000275000, 0.000300000}) + []float64{0.000100000, 0.000200000, 0.000300000, 0.000400000, 0.000500000, 0.000600000}) + + metrics.adapterVideoBidDuration = newHistogramVec(cfg, reg, + "adapter_vidbid_dur", + "Video Ad durations returned by the bidder", []string{adapterLabel}, + []float64{4, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120}) + preloadLabelValues(&metrics, syncerKeys) return &metrics @@ -550,6 +640,22 @@ func (m *Metrics) RecordStoredResponse(pubId string) { } } +func (m *Metrics) RecordRejectedBidsForAccount(pubId string) { + if pubId != metrics.PublisherUnknown { + m.accountRejectedBid.With(prometheus.Labels{ + accountLabel: pubId, + }).Inc() + } +} + +func (m *Metrics) RecordFloorsRequestForAccount(pubId string) { + if pubId != metrics.PublisherUnknown { + m.accountFloorsRequest.With(prometheus.Labels{ + accountLabel: pubId, + }).Inc() + } +} + func (m *Metrics) RecordImps(labels metrics.ImpLabels) { m.impressions.With(prometheus.Labels{ isBannerLabel: strconv.FormatBool(labels.BannerImps), @@ -640,6 +746,14 @@ func (m *Metrics) RecordAdapterRequest(labels metrics.AdapterLabels) { } } +func (m *Metrics) RecordRejectedBidsForBidder(Adapter openrtb_ext.BidderName) { + if m.rejectedBids != nil { + m.rejectedBids.With(prometheus.Labels{ + adapterLabel: string(Adapter), + }).Inc() + } +} + // Keeps track of created and reused connections to adapter bidders and the time from the // connection request, to the connection creation, or reuse from the pool across all engines func (m *Metrics) RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) { @@ -666,8 +780,11 @@ func (m *Metrics) RecordDNSTime(dnsLookupTime time.Duration) { m.dnsLookupTimer.Observe(dnsLookupTime.Seconds()) } -func (m *Metrics) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) { - m.tlsHandhakeTimer.Observe(tlsHandshakeTime.Seconds()) +func (m *Metrics) RecordTLSHandshakeTime(adapterName openrtb_ext.BidderName, tlsHandshakeTime time.Duration) { + // m.tlsHandhakeTimer.Observe(tlsHandshakeTime.Seconds()) + m.tlsHandhakeTimer.With(prometheus.Labels{ + adapterLabel: string(adapterName), + }).Observe(tlsHandshakeTime.Seconds()) } func (m *Metrics) RecordAdapterPanic(labels metrics.AdapterLabels) { diff --git a/metrics/prometheus/prometheus_ow.go b/metrics/prometheus/prometheus_ow.go new file mode 100644 index 00000000000..243fb144e47 --- /dev/null +++ b/metrics/prometheus/prometheus_ow.go @@ -0,0 +1,73 @@ +package prometheusmetrics + +import ( + "strconv" + "time" + + "github.com/prebid/prebid-server/metrics" + "github.com/prometheus/client_golang/prometheus" +) + +// RecordAdapterDuplicateBidID captures the bid.ID collisions when adaptor +// gives the bid response with multiple bids containing same bid.ID +// ensure collisions value is greater than 1. This function will not give any error +// if collisions = 1 is passed +func (m *Metrics) RecordAdapterDuplicateBidID(adaptor string, collisions int) { + m.adapterDuplicateBidIDCounter.With(prometheus.Labels{ + adapterLabel: adaptor, + }).Add(float64(collisions)) +} + +// RecordRequestHavingDuplicateBidID keeps count of request when duplicate bid.id is +// detected in partner's response +func (m *Metrics) RecordRequestHavingDuplicateBidID() { + m.requestsDuplicateBidIDCounter.Inc() +} + +// pod specific metrics + +// recordAlgoTime is common method which handles algorithm time performance +func recordAlgoTime(timer *prometheus.HistogramVec, labels metrics.PodLabels, elapsedTime time.Duration) { + + pmLabels := prometheus.Labels{ + podAlgorithm: labels.AlgorithmName, + } + + if labels.NoOfImpressions != nil { + pmLabels[podNoOfImpressions] = strconv.Itoa(*labels.NoOfImpressions) + } + if labels.NoOfCombinations != nil { + pmLabels[podTotalCombinations] = strconv.Itoa(*labels.NoOfCombinations) + } + if labels.NoOfResponseBids != nil { + pmLabels[podNoOfResponseBids] = strconv.Itoa(*labels.NoOfResponseBids) + } + + timer.With(pmLabels).Observe(elapsedTime.Seconds()) +} + +// RecordPodImpGenTime records number of impressions generated and time taken +// by underneath algorithm to generate them +func (m *Metrics) RecordPodImpGenTime(labels metrics.PodLabels, start time.Time) { + elapsedTime := time.Since(start) + recordAlgoTime(m.podImpGenTimer, labels, elapsedTime) +} + +// RecordPodCombGenTime records number of combinations generated and time taken +// by underneath algorithm to generate them +func (m *Metrics) RecordPodCombGenTime(labels metrics.PodLabels, elapsedTime time.Duration) { + recordAlgoTime(m.podCombGenTimer, labels, elapsedTime) +} + +// RecordPodCompititveExclusionTime records number of combinations comsumed for forming +// final ad pod response and time taken by underneath algorithm to generate them +func (m *Metrics) RecordPodCompititveExclusionTime(labels metrics.PodLabels, elapsedTime time.Duration) { + recordAlgoTime(m.podCompExclTimer, labels, elapsedTime) +} + +//RecordAdapterVideoBidDuration records actual ad duration (>0) returned by the bidder +func (m *Metrics) RecordAdapterVideoBidDuration(labels metrics.AdapterLabels, videoBidDuration int) { + if videoBidDuration > 0 { + m.adapterVideoBidDuration.With(prometheus.Labels{adapterLabel: string(labels.Adapter)}).Observe(float64(videoBidDuration)) + } +} diff --git a/metrics/prometheus/prometheus_test.go b/metrics/prometheus/prometheus_test.go index 84516b4d460..ab043de9082 100644 --- a/metrics/prometheus/prometheus_test.go +++ b/metrics/prometheus/prometheus_test.go @@ -2,6 +2,7 @@ package prometheusmetrics import ( "fmt" + "strconv" "testing" "time" @@ -1324,35 +1325,57 @@ func TestRecordDNSTime(t *testing.T) { } func TestRecordTLSHandshakeTime(t *testing.T) { - testCases := []struct { - description string + type testIn struct { + adapterName openrtb_ext.BidderName tLSHandshakeDuration time.Duration - expectedDuration float64 - expectedCount uint64 + } + + type testOut struct { + expectedDuration float64 + expectedCount uint64 + } + + testCases := []struct { + description string + in testIn + out testOut }{ { - description: "Five second DNS lookup time", - tLSHandshakeDuration: time.Second * 5, - expectedDuration: 5, - expectedCount: 1, + description: "Five second DNS lookup time", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + tLSHandshakeDuration: time.Second * 5, + }, + out: testOut{ + expectedDuration: 5, + expectedCount: 1, + }, }, { - description: "Zero DNS lookup time", - tLSHandshakeDuration: 0, - expectedDuration: 0, - expectedCount: 1, + description: "Zero DNS lookup time", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + tLSHandshakeDuration: 0, + }, + out: testOut{ + expectedDuration: 0, + expectedCount: 1, + }, }, } for i, test := range testCases { pm := createMetricsForTesting() - pm.RecordTLSHandshakeTime(test.tLSHandshakeDuration) + assertDesciptions := []string{ + fmt.Sprintf("[%d] Incorrect number of histogram entries. Desc: %s", i+1, test.description), + fmt.Sprintf("[%d] Incorrect number of histogram cumulative values. Desc: %s", i+1, test.description), + } - m := dto.Metric{} - pm.tlsHandhakeTimer.Write(&m) - histogram := *m.GetHistogram() + pm.RecordTLSHandshakeTime(test.in.adapterName, test.in.tLSHandshakeDuration) - assert.Equal(t, test.expectedCount, histogram.GetSampleCount(), "[%d] Incorrect number of histogram entries. Desc: %s\n", i, test.description) - assert.Equal(t, test.expectedDuration, histogram.GetSampleSum(), "[%d] Incorrect number of histogram cumulative values. Desc: %s\n", i, test.description) + // Assert TLS Handshake time + histogram := getHistogramFromHistogramVec(pm.tlsHandhakeTimer, adapterLabel, string(test.in.adapterName)) + assert.Equal(t, test.out.expectedCount, histogram.GetSampleCount(), assertDesciptions[0]) + assert.Equal(t, test.out.expectedDuration, histogram.GetSampleSum(), assertDesciptions[1]) } } @@ -1482,6 +1505,7 @@ func TestDisabledMetrics(t *testing.T) { assert.Nil(t, prometheusMetrics.adapterReusedConnections, "Counter Vector adapterReusedConnections should be nil") assert.Nil(t, prometheusMetrics.adapterCreatedConnections, "Counter Vector adapterCreatedConnections should be nil") assert.Nil(t, prometheusMetrics.adapterConnectionWaitTime, "Counter Vector adapterConnectionWaitTime should be nil") + assert.Nil(t, prometheusMetrics.tlsHandhakeTimer, "Counter Vector tlsHandhakeTimer should be nil") assert.Nil(t, prometheusMetrics.adapterGDPRBlockedRequests, "Counter Vector adapterGDPRBlockedRequests should be nil") } @@ -1563,6 +1587,187 @@ func TestRecordRequestPrivacy(t *testing.T) { }) } +// TestRecordRequestDuplicateBidID checks RecordRequestDuplicateBidID +func TestRecordRequestDuplicateBidID(t *testing.T) { + m := createMetricsForTesting() + m.RecordRequestHavingDuplicateBidID() + // verify total no of requests which detected collision + assertCounterValue(t, "request cnt having duplicate bid.id", "request cnt having duplicate bid.id", m.requestsDuplicateBidIDCounter, float64(1)) +} + +// TestRecordAdapterDuplicateBidID checks RecordAdapterDuplicateBidID +func TestRecordAdapterDuplicateBidID(t *testing.T) { + type collisions struct { + simulate int // no of bids to be simulate with same bid.id + expect int // no of collisions expected to be recorded by metrics engine for given bidder + } + type bidderCollisions = map[string]collisions + testCases := []struct { + scenario string + bidderCollisions bidderCollisions // represents no of collisions detected for bid.id at bidder level for given request + expectCollisions int + }{ + {scenario: "invalid collision value", bidderCollisions: map[string]collisions{"bidder-1": {simulate: -1, expect: 0}}}, + {scenario: "no collision", bidderCollisions: map[string]collisions{"bidder-1": {simulate: 0, expect: 0}}}, + {scenario: "one collision", bidderCollisions: map[string]collisions{"bidder-1": {simulate: 1, expect: 1}}}, + {scenario: "multiple collisions", bidderCollisions: map[string]collisions{"bidder-1": {simulate: 2, expect: 2}}}, + {scenario: "multiple bidders", bidderCollisions: map[string]collisions{"bidder-1": {simulate: 2, expect: 2}, "bidder-2": {simulate: 4, expect: 4}}}, + {scenario: "multiple bidders with bidder-1 no collision", bidderCollisions: map[string]collisions{"bidder-1": {simulate: 0, expect: 0}, + "bidder-2": {simulate: 4, expect: 4}}}, + } + + for _, testcase := range testCases { + m := createMetricsForTesting() + for bidder, collisions := range testcase.bidderCollisions { + for collision := 1; collision <= collisions.simulate; collision++ { + m.RecordAdapterDuplicateBidID(bidder, 1) + } + assertCounterVecValue(t, testcase.scenario, testcase.scenario, m.adapterDuplicateBidIDCounter, float64(collisions.expect), prometheus.Labels{ + adapterLabel: bidder, + }) + } + } +} + +func TestRecordPodImpGenTime(t *testing.T) { + impressions := 4 + testAlgorithmMetrics(t, impressions, func(m *Metrics) dto.Histogram { + m.RecordPodImpGenTime(metrics.PodLabels{AlgorithmName: "sample_imp_algo", NoOfImpressions: &impressions}, time.Now()) + return getHistogramFromHistogramVec(m.podImpGenTimer, podNoOfImpressions, strconv.Itoa(impressions)) + }) +} + +func TestRecordPodCombGenTime(t *testing.T) { + combinations := 5 + testAlgorithmMetrics(t, combinations, func(m *Metrics) dto.Histogram { + m.RecordPodCombGenTime(metrics.PodLabels{AlgorithmName: "sample_comb_algo", NoOfCombinations: &combinations}, time.Since(time.Now())) + return getHistogramFromHistogramVec(m.podCombGenTimer, podTotalCombinations, strconv.Itoa(combinations)) + }) +} + +func TestRecordPodCompetitiveExclusionTime(t *testing.T) { + totalBids := 8 + testAlgorithmMetrics(t, totalBids, func(m *Metrics) dto.Histogram { + m.RecordPodCompititveExclusionTime(metrics.PodLabels{AlgorithmName: "sample_comt_excl_algo", NoOfResponseBids: &totalBids}, time.Since(time.Now())) + return getHistogramFromHistogramVec(m.podCompExclTimer, podNoOfResponseBids, strconv.Itoa(totalBids)) + }) +} + +func TestRecordAdapterVideoBidDuration(t *testing.T) { + + testCases := []struct { + description string + bidderAdDurations map[string][]int + expectedSum map[string]int + expectedCount map[string]int + expectedBuckets map[string]map[int]int // cumulative + }{ + { + description: "single bidder multiple ad durations", + bidderAdDurations: map[string][]int{ + "bidder_1": {5, 10, 11, 32}, + }, + expectedSum: map[string]int{"bidder_1": 58}, + expectedCount: map[string]int{"bidder_1": 4}, + expectedBuckets: map[string]map[int]int{ + "bidder_1": {5: 1, 10: 2, 15: 3, 35: 4}, // Upper bound : cumulative number + }, + }, + { + description: "multiple bidders multiple ad durations", + bidderAdDurations: map[string][]int{ + "bidder_1": {5, 10, 11, 32, 39}, + "bidder_2": {25, 30}, + }, + expectedSum: map[string]int{"bidder_1": 97, "bidder_2": 55}, + expectedCount: map[string]int{"bidder_1": 5, "bidder_2": 2}, + expectedBuckets: map[string]map[int]int{ + "bidder_1": {5: 1, 10: 2, 15: 3, 35: 4, 40: 5}, + "bidder_2": {25: 1, 30: 2}, + }, + }, + { + description: "bidder with 0 ad durations", + bidderAdDurations: map[string][]int{ + "bidder_1": {5, 0, 0, 27}, + }, + expectedSum: map[string]int{"bidder_1": 32}, + expectedCount: map[string]int{"bidder_1": 2}, // must exclude 2 observations having 0 durations + expectedBuckets: map[string]map[int]int{ + "bidder_1": {5: 1, 30: 2}, + }, + }, + { + description: "bidder with similar durations", + bidderAdDurations: map[string][]int{ + "bidder_1": {23, 23, 23}, + }, + expectedSum: map[string]int{"bidder_1": 69}, + expectedCount: map[string]int{"bidder_1": 3}, // + expectedBuckets: map[string]map[int]int{ + "bidder_1": {25: 3}, + }, + }, + { + description: "bidder with ad durations >= 60", + bidderAdDurations: map[string][]int{ + "bidder_1": {33, 60, 93, 90, 90, 120}, + }, + expectedSum: map[string]int{"bidder_1": 486}, + expectedCount: map[string]int{"bidder_1": 6}, // + expectedBuckets: map[string]map[int]int{ + "bidder_1": {35: 1, 60: 2, 120: 6}, + }, + }, + } + + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + m := createMetricsForTesting() + for adapterName, adDurations := range test.bidderAdDurations { + for _, adDuration := range adDurations { + m.RecordAdapterVideoBidDuration(metrics.AdapterLabels{ + Adapter: openrtb_ext.BidderName(adapterName), + }, adDuration) + } + result := getHistogramFromHistogramVec(m.adapterVideoBidDuration, adapterLabel, adapterName) + for bucketDuration, durationCnt := range test.expectedBuckets[adapterName] { + validBucket := false + for _, bucket := range result.GetBucket() { + if int(bucket.GetUpperBound()) == bucketDuration { + validBucket = true + assert.Equal(t, uint64(durationCnt), bucket.GetCumulativeCount()) + break + } + } + if !validBucket { + assert.Fail(t, "Invalid expected bucket = "+strconv.Itoa(bucketDuration)) + } + } + expectedCount := test.expectedCount[adapterName] + expectedSum := test.expectedSum[adapterName] + assertHistogram(t, "adapter_vidbid_dur", result, uint64(expectedCount), float64(expectedSum)) + } + }) + } +} + +func testAlgorithmMetrics(t *testing.T, input int, f func(m *Metrics) dto.Histogram) { + // test input + adRequests := 2 + m := createMetricsForTesting() + var result dto.Histogram + for req := 1; req <= adRequests; req++ { + result = f(m) + } + + // assert observations + assert.Equal(t, uint64(adRequests), result.GetSampleCount(), "ad requests : count") + for _, bucket := range result.Bucket { + assert.Equal(t, uint64(adRequests), bucket.GetCumulativeCount(), "total observations") + } +} + func assertCounterValue(t *testing.T, description, name string, counter prometheus.Counter, expected float64) { m := dto.Metric{} counter.Write(&m) diff --git a/openrtb_ext/adpod.go b/openrtb_ext/adpod.go new file mode 100644 index 00000000000..c3bcc1aca21 --- /dev/null +++ b/openrtb_ext/adpod.go @@ -0,0 +1,326 @@ +package openrtb_ext + +import ( + "encoding/json" + "errors" + "strings" +) + +const ( + //BidderOWPrebidCTV for prebid adpod response + BidderOWPrebidCTV BidderName = "prebid_ctv" +) + +var ( + errInvalidAdPodMinDuration = errors.New("imp.video.minduration must be number positive number") + errInvalidAdPodMaxDuration = errors.New("imp.video.maxduration must be number positive non zero number") + errInvalidAdPodDuration = errors.New("imp.video.minduration must be less than imp.video.maxduration") + errInvalidCrossPodAdvertiserExclusionPercent = errors.New("request.ext.adpod.crosspodexcladv must be a number between 0 and 100") + errInvalidCrossPodIABCategoryExclusionPercent = errors.New("request.ext.adpod.crosspodexcliabcat must be a number between 0 and 100") + errInvalidIABCategoryExclusionWindow = errors.New("request.ext.adpod.excliabcatwindow must be postive number") + errInvalidAdvertiserExclusionWindow = errors.New("request.ext.adpod.excladvwindow must be postive number") + errInvalidVideoLengthMatching = errors.New("request.ext.adpod.videolengthmatching must be exact|roundup") + errInvalidAdPodOffset = errors.New("request.imp.video.ext.offset must be postive number") + errInvalidMinAds = errors.New("%key%.ext.adpod.minads must be positive number") + errInvalidMaxAds = errors.New("%key%.ext.adpod.maxads must be positive number") + errInvalidMinDuration = errors.New("%key%.ext.adpod.adminduration must be positive number") + errInvalidMaxDuration = errors.New("%key%.ext.adpod.admaxduration must be positive number") + errInvalidAdvertiserExclusionPercent = errors.New("%key%.ext.adpod.excladv must be number between 0 and 100") + errInvalidIABCategoryExclusionPercent = errors.New("%key%.ext.adpod.excliabcat must be number between 0 and 100") + errInvalidMinMaxAds = errors.New("%key%.ext.adpod.minads must be less than %key%.ext.adpod.maxads") + errInvalidMinMaxDuration = errors.New("%key%.ext.adpod.adminduration must be less than %key%.ext.adpod.admaxduration") + errInvalidMinMaxDurationRange = errors.New("adpod duration checks for adminduration,admaxduration,minads,maxads are not in video minduration and maxduration duration range") +) + +type OWVideoLengthMatchingPolicy = string + +const ( + OWExactVideoLengthsMatching OWVideoLengthMatchingPolicy = `exact` + OWRoundupVideoLengthMatching OWVideoLengthMatchingPolicy = `roundup` +) + +// ExtCTVBid defines the contract for bidresponse.seatbid.bid[i].ext +type ExtOWBid struct { + ExtBid + AdPod *BidAdPodExt `json:"adpod,omitempty"` + SKAdNetwork json.RawMessage `json:"skadn,omitempty"` +} + +// BidAdPodExt defines the prebid adpod response in bidresponse.ext.adpod parameter +type BidAdPodExt struct { + ReasonCode *int `json:"aprc,omitempty"` + RefBids []string `json:"refbids,omitempty"` //change refbids to bids name +} + +// ExtOWRequest defines the contract for bidrequest.ext +type ExtOWRequest struct { + ExtRequest + AdPod *ExtRequestAdPod `json:"adpod,omitempty"` +} + +//ExtVideoAdPod structure to accept video specific more parameters like adpod +type ExtVideoAdPod struct { + Offset *int `json:"offset,omitempty"` // Minutes from start where this ad is intended to show + AdPod *VideoAdPod `json:"adpod,omitempty"` +} + +//ExtRequestAdPod holds AdPod specific extension parameters at request level +type ExtRequestAdPod struct { + VideoAdPod + CrossPodAdvertiserExclusionPercent *int `json:"crosspodexcladv,omitempty"` //Percent Value - Across multiple impression there will be no ads from same advertiser. Note: These cross pod rule % values can not be more restrictive than per pod + CrossPodIABCategoryExclusionPercent *int `json:"crosspodexcliabcat,omitempty"` //Percent Value - Across multiple impression there will be no ads from same advertiser + IABCategoryExclusionWindow *int `json:"excliabcatwindow,omitempty"` //Duration in minute between pods where exclusive IAB rule needs to be applied + AdvertiserExclusionWindow *int `json:"excladvwindow,omitempty"` //Duration in minute between pods where exclusive advertiser rule needs to be applied + VideoLengths []int `json:"videolengths,omitempty"` //Range of ad durations allowed in the response + VideoLengthMatching OWVideoLengthMatchingPolicy `json:"videolengthmatching,omitempty"` //Flag indicating exact ad duration requirement. (default)empty/exact/round. +} + +//VideoAdPod holds Video AdPod specific extension parameters at impression level +type VideoAdPod struct { + MinAds *int `json:"minads,omitempty"` //Default 1 if not specified + MaxAds *int `json:"maxads,omitempty"` //Default 1 if not specified + MinDuration *int `json:"adminduration,omitempty"` // (adpod.adminduration * adpod.minads) should be greater than or equal to video.minduration + MaxDuration *int `json:"admaxduration,omitempty"` // (adpod.admaxduration * adpod.maxads) should be less than or equal to video.maxduration + video.maxextended + AdvertiserExclusionPercent *int `json:"excladv,omitempty"` // Percent value 0 means none of the ads can be from same advertiser 100 means can have all same advertisers + IABCategoryExclusionPercent *int `json:"excliabcat,omitempty"` // Percent value 0 means all ads should be of different IAB categories. +} + +/* +//UnmarshalJSON will unmarshal extension into ExtVideoAdPod object +func (ext *ExtVideoAdPod) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, ext) +} + +//UnmarshalJSON will unmarshal extension into ExtRequestAdPod object +func (ext *ExtRequestAdPod) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, ext) +} +*/ +//getRequestAdPodError will return request level error message +func getRequestAdPodError(err error) error { + return errors.New(strings.Replace(err.Error(), "%key%", "req.ext", -1)) +} + +//getVideoAdPodError will return video adpod level error message +func getVideoAdPodError(err error) error { + return errors.New(strings.Replace(err.Error(), "%key%", "imp.video.ext", -1)) +} + +func getIntPtr(v int) *int { + return &v +} + +//Validate will validate AdPod object +func (pod *VideoAdPod) Validate() (err []error) { + if nil != pod.MinAds && *pod.MinAds <= 0 { + err = append(err, errInvalidMinAds) + } + + if nil != pod.MaxAds && *pod.MaxAds <= 0 { + err = append(err, errInvalidMaxAds) + } + + if nil != pod.MinDuration && *pod.MinDuration <= 0 { + err = append(err, errInvalidMinDuration) + } + + if nil != pod.MaxDuration && *pod.MaxDuration <= 0 { + err = append(err, errInvalidMaxDuration) + } + + if nil != pod.AdvertiserExclusionPercent && (*pod.AdvertiserExclusionPercent < 0 || *pod.AdvertiserExclusionPercent > 100) { + err = append(err, errInvalidAdvertiserExclusionPercent) + } + + if nil != pod.IABCategoryExclusionPercent && (*pod.IABCategoryExclusionPercent < 0 || *pod.IABCategoryExclusionPercent > 100) { + err = append(err, errInvalidIABCategoryExclusionPercent) + } + + if nil != pod.MinAds && nil != pod.MaxAds && *pod.MinAds > *pod.MaxAds { + err = append(err, errInvalidMinMaxAds) + } + + if nil != pod.MinDuration && nil != pod.MaxDuration && *pod.MinDuration > *pod.MaxDuration { + err = append(err, errInvalidMinMaxDuration) + } + + return +} + +//Validate will validate ExtRequestAdPod object +func (ext *ExtRequestAdPod) Validate() (err []error) { + if nil == ext { + return + } + + if nil != ext.CrossPodAdvertiserExclusionPercent && + (*ext.CrossPodAdvertiserExclusionPercent < 0 || *ext.CrossPodAdvertiserExclusionPercent > 100) { + err = append(err, errInvalidCrossPodAdvertiserExclusionPercent) + } + + if nil != ext.CrossPodIABCategoryExclusionPercent && + (*ext.CrossPodIABCategoryExclusionPercent < 0 || *ext.CrossPodIABCategoryExclusionPercent > 100) { + err = append(err, errInvalidCrossPodIABCategoryExclusionPercent) + } + + if nil != ext.IABCategoryExclusionWindow && *ext.IABCategoryExclusionWindow < 0 { + err = append(err, errInvalidIABCategoryExclusionWindow) + } + + if nil != ext.AdvertiserExclusionWindow && *ext.AdvertiserExclusionWindow < 0 { + err = append(err, errInvalidAdvertiserExclusionWindow) + } + + if len(ext.VideoLengthMatching) > 0 && !(OWExactVideoLengthsMatching == ext.VideoLengthMatching || OWRoundupVideoLengthMatching == ext.VideoLengthMatching) { + err = append(err, errInvalidVideoLengthMatching) + } + + if errL := ext.VideoAdPod.Validate(); nil != errL { + for _, errr := range errL { + err = append(err, getRequestAdPodError(errr)) + } + } + + return +} + +//Validate will validate video extension object +func (ext *ExtVideoAdPod) Validate() (err []error) { + if nil != ext.Offset && *ext.Offset < 0 { + err = append(err, errInvalidAdPodOffset) + } + + if nil != ext.AdPod { + if errL := ext.AdPod.Validate(); nil != errL { + for _, errr := range errL { + err = append(err, getRequestAdPodError(errr)) + } + } + } + + return +} + +//SetDefaultValue will set default values if not present +func (pod *VideoAdPod) SetDefaultValue() { + //pod.MinAds setting default value + if nil == pod.MinAds { + pod.MinAds = getIntPtr(1) + } + + //pod.MaxAds setting default value + if nil == pod.MaxAds { + pod.MaxAds = getIntPtr(3) + } + + //pod.AdvertiserExclusionPercent setting default value + if nil == pod.AdvertiserExclusionPercent { + pod.AdvertiserExclusionPercent = getIntPtr(100) + } + + //pod.IABCategoryExclusionPercent setting default value + if nil == pod.IABCategoryExclusionPercent { + pod.IABCategoryExclusionPercent = getIntPtr(100) + } +} + +//SetDefaultValue will set default values if not present +func (ext *ExtRequestAdPod) SetDefaultValue() { + //ext.VideoAdPod setting default value + ext.VideoAdPod.SetDefaultValue() + + //ext.CrossPodAdvertiserExclusionPercent setting default value + if nil == ext.CrossPodAdvertiserExclusionPercent { + ext.CrossPodAdvertiserExclusionPercent = getIntPtr(100) + } + + //ext.CrossPodIABCategoryExclusionPercent setting default value + if nil == ext.CrossPodIABCategoryExclusionPercent { + ext.CrossPodIABCategoryExclusionPercent = getIntPtr(100) + } + + //ext.IABCategoryExclusionWindow setting default value + if nil == ext.IABCategoryExclusionWindow { + ext.IABCategoryExclusionWindow = getIntPtr(0) + } + + //ext.AdvertiserExclusionWindow setting default value + if nil == ext.AdvertiserExclusionWindow { + ext.AdvertiserExclusionWindow = getIntPtr(0) + } +} + +//SetDefaultValue will set default values if not present +func (ext *ExtVideoAdPod) SetDefaultValue() { + //ext.Offset setting default values + if nil == ext.Offset { + ext.Offset = getIntPtr(0) + } + + //ext.AdPod setting default values + if nil == ext.AdPod { + ext.AdPod = &VideoAdPod{} + } + ext.AdPod.SetDefaultValue() +} + +//SetDefaultAdDuration will set default pod ad slot durations +func (pod *VideoAdPod) SetDefaultAdDurations(podMinDuration, podMaxDuration int64) { + //pod.MinDuration setting default adminduration + if nil == pod.MinDuration { + duration := int(podMinDuration / 2) + pod.MinDuration = &duration + } + + //pod.MaxDuration setting default admaxduration + if nil == pod.MaxDuration { + duration := int(podMaxDuration / 2) + pod.MaxDuration = &duration + } +} + +//Merge VideoAdPod Values +func (pod *VideoAdPod) Merge(parent *VideoAdPod) { + //pod.MinAds setting default value + if nil == pod.MinAds { + pod.MinAds = parent.MinAds + } + + //pod.MaxAds setting default value + if nil == pod.MaxAds { + pod.MaxAds = parent.MaxAds + } + + //pod.AdvertiserExclusionPercent setting default value + if nil == pod.AdvertiserExclusionPercent { + pod.AdvertiserExclusionPercent = parent.AdvertiserExclusionPercent + } + + //pod.IABCategoryExclusionPercent setting default value + if nil == pod.IABCategoryExclusionPercent { + pod.IABCategoryExclusionPercent = parent.IABCategoryExclusionPercent + } +} + +//ValidateAdPodDurations will validate adpod min,max durations +func (pod *VideoAdPod) ValidateAdPodDurations(minDuration, maxDuration, maxExtended int64) (err []error) { + if minDuration < 0 { + err = append(err, errInvalidAdPodMinDuration) + } + + if maxDuration <= 0 { + err = append(err, errInvalidAdPodMaxDuration) + } + + if minDuration > maxDuration { + err = append(err, errInvalidAdPodDuration) + } + + if pod.MinAds != nil && pod.MinDuration != nil && pod.MaxDuration != nil && pod.MaxAds != nil { + if ((*pod.MinAds * *pod.MinDuration) <= int(maxDuration)) && (int(minDuration) <= (*pod.MaxAds * *pod.MaxDuration)) { + } else { + err = append(err, errInvalidMinMaxDurationRange) + } + } + return +} diff --git a/openrtb_ext/adpod_test.go b/openrtb_ext/adpod_test.go new file mode 100644 index 00000000000..da5f98b45bc --- /dev/null +++ b/openrtb_ext/adpod_test.go @@ -0,0 +1,315 @@ +package openrtb_ext + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestVideoAdPod_Validate(t *testing.T) { + type fields struct { + MinAds *int + MaxAds *int + MinDuration *int + MaxDuration *int + AdvertiserExclusionPercent *int + IABCategoryExclusionPercent *int + } + tests := []struct { + name string + fields fields + wantErr []error + }{ + { + name: "ErrInvalidMinAds", + fields: fields{ + MinAds: getIntPtr(-1), + }, + wantErr: []error{errInvalidMinAds}, + }, + { + name: "ZeroMinAds", + fields: fields{ + MinAds: getIntPtr(0), + }, + wantErr: []error{errInvalidMinAds}, + }, + { + name: "ErrInvalidMaxAds", + fields: fields{ + MaxAds: getIntPtr(-1), + }, + wantErr: []error{errInvalidMaxAds}, + }, + { + name: "ZeroMaxAds", + fields: fields{ + MaxAds: getIntPtr(0), + }, + wantErr: []error{errInvalidMaxAds}, + }, + { + name: "ErrInvalidMinDuration", + fields: fields{ + MinDuration: getIntPtr(-1), + }, + wantErr: []error{errInvalidMinDuration}, + }, + { + name: "ZeroMinDuration", + fields: fields{ + MinDuration: getIntPtr(0), + }, + wantErr: []error{errInvalidMinDuration}, + }, + { + name: "ErrInvalidMaxDuration", + fields: fields{ + MaxDuration: getIntPtr(-1), + }, + wantErr: []error{errInvalidMaxDuration}, + }, + { + name: "ZeroMaxDuration", + fields: fields{ + MaxDuration: getIntPtr(0), + }, + wantErr: []error{errInvalidMaxDuration}, + }, + { + name: "ErrInvalidAdvertiserExclusionPercent_NegativeValue", + fields: fields{ + AdvertiserExclusionPercent: getIntPtr(-1), + }, + wantErr: []error{errInvalidAdvertiserExclusionPercent}, + }, + { + name: "ErrInvalidAdvertiserExclusionPercent_InvalidRange", + fields: fields{ + AdvertiserExclusionPercent: getIntPtr(-1), + }, + wantErr: []error{errInvalidAdvertiserExclusionPercent}, + }, + { + name: "ErrInvalidIABCategoryExclusionPercent_Negative", + fields: fields{ + IABCategoryExclusionPercent: getIntPtr(-1), + }, + wantErr: []error{errInvalidIABCategoryExclusionPercent}, + }, + { + name: "ErrInvalidIABCategoryExclusionPercent_InvalidRange", + fields: fields{ + IABCategoryExclusionPercent: getIntPtr(101), + }, + wantErr: []error{errInvalidIABCategoryExclusionPercent}, + }, + { + name: "ErrInvalidMinMaxAds", + fields: fields{ + MinAds: getIntPtr(5), + MaxAds: getIntPtr(2), + }, + wantErr: []error{errInvalidMinMaxAds}, + }, + { + name: "ErrInvalidMinMaxDuration", + fields: fields{ + MinDuration: getIntPtr(5), + MaxDuration: getIntPtr(2), + }, + wantErr: []error{errInvalidMinMaxDuration}, + }, + { + name: "Valid", + fields: fields{ + MinAds: getIntPtr(3), + MaxAds: getIntPtr(4), + MinDuration: getIntPtr(20), + MaxDuration: getIntPtr(30), + AdvertiserExclusionPercent: getIntPtr(100), + IABCategoryExclusionPercent: getIntPtr(100), + }, + wantErr: nil, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pod := &VideoAdPod{ + MinAds: tt.fields.MinAds, + MaxAds: tt.fields.MaxAds, + MinDuration: tt.fields.MinDuration, + MaxDuration: tt.fields.MaxDuration, + AdvertiserExclusionPercent: tt.fields.AdvertiserExclusionPercent, + IABCategoryExclusionPercent: tt.fields.IABCategoryExclusionPercent, + } + + actualErr := pod.Validate() + assert.Equal(t, tt.wantErr, actualErr) + }) + } +} + +func TestExtRequestAdPod_Validate(t *testing.T) { + type fields struct { + VideoAdPod VideoAdPod + CrossPodAdvertiserExclusionPercent *int + CrossPodIABCategoryExclusionPercent *int + IABCategoryExclusionWindow *int + AdvertiserExclusionWindow *int + VideoLengthMatching string + } + tests := []struct { + name string + fields fields + wantErr []error + }{ + { + name: "ErrInvalidCrossPodAdvertiserExclusionPercent_Negative", + fields: fields{ + CrossPodAdvertiserExclusionPercent: getIntPtr(-1), + }, + wantErr: []error{errInvalidCrossPodAdvertiserExclusionPercent}, + }, + { + name: "ErrInvalidCrossPodAdvertiserExclusionPercent_InvalidRange", + fields: fields{ + CrossPodAdvertiserExclusionPercent: getIntPtr(101), + }, + wantErr: []error{errInvalidCrossPodAdvertiserExclusionPercent}, + }, + { + name: "ErrInvalidCrossPodIABCategoryExclusionPercent_Negative", + fields: fields{ + CrossPodIABCategoryExclusionPercent: getIntPtr(-1), + }, + wantErr: []error{errInvalidCrossPodIABCategoryExclusionPercent}, + }, + { + name: "ErrInvalidCrossPodIABCategoryExclusionPercent_InvalidRange", + fields: fields{ + CrossPodIABCategoryExclusionPercent: getIntPtr(101), + }, + wantErr: []error{errInvalidCrossPodIABCategoryExclusionPercent}, + }, + { + name: "ErrInvalidIABCategoryExclusionWindow", + fields: fields{ + IABCategoryExclusionWindow: getIntPtr(-1), + }, + wantErr: []error{errInvalidIABCategoryExclusionWindow}, + }, + { + name: "ErrInvalidAdvertiserExclusionWindow", + fields: fields{ + AdvertiserExclusionWindow: getIntPtr(-1), + }, + wantErr: []error{errInvalidAdvertiserExclusionWindow}, + }, + { + name: "ErrInvalidVideoLengthMatching", + fields: fields{ + VideoLengthMatching: "invalid", + }, + wantErr: []error{errInvalidVideoLengthMatching}, + }, + { + name: "InvalidAdPod", + fields: fields{ + VideoAdPod: VideoAdPod{ + MinAds: getIntPtr(-1), + }, + }, + wantErr: []error{getRequestAdPodError(errInvalidMinAds)}, + }, + { + name: "Valid", + fields: fields{ + CrossPodAdvertiserExclusionPercent: getIntPtr(100), + CrossPodIABCategoryExclusionPercent: getIntPtr(0), + IABCategoryExclusionWindow: getIntPtr(10), + AdvertiserExclusionWindow: getIntPtr(10), + VideoAdPod: VideoAdPod{ + MinAds: getIntPtr(3), + MaxAds: getIntPtr(4), + MinDuration: getIntPtr(20), + MaxDuration: getIntPtr(30), + AdvertiserExclusionPercent: getIntPtr(100), + IABCategoryExclusionPercent: getIntPtr(100), + }, + }, + wantErr: nil, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ext := &ExtRequestAdPod{ + VideoAdPod: tt.fields.VideoAdPod, + CrossPodAdvertiserExclusionPercent: tt.fields.CrossPodAdvertiserExclusionPercent, + CrossPodIABCategoryExclusionPercent: tt.fields.CrossPodIABCategoryExclusionPercent, + IABCategoryExclusionWindow: tt.fields.IABCategoryExclusionWindow, + AdvertiserExclusionWindow: tt.fields.AdvertiserExclusionWindow, + VideoLengthMatching: tt.fields.VideoLengthMatching, + } + actualErr := ext.Validate() + assert.Equal(t, tt.wantErr, actualErr) + }) + } +} + +func TestExtVideoAdPod_Validate(t *testing.T) { + type fields struct { + Offset *int + AdPod *VideoAdPod + } + tests := []struct { + name string + fields fields + wantErr []error + }{ + { + name: "ErrInvalidAdPodOffset", + fields: fields{ + Offset: getIntPtr(-1), + }, + wantErr: []error{errInvalidAdPodOffset}, + }, + { + name: "InvalidAdPod", + fields: fields{ + AdPod: &VideoAdPod{ + MinAds: getIntPtr(-1), + }, + }, + wantErr: []error{getRequestAdPodError(errInvalidMinAds)}, + }, + { + name: "Valid", + fields: fields{ + Offset: getIntPtr(10), + AdPod: &VideoAdPod{ + MinAds: getIntPtr(3), + MaxAds: getIntPtr(4), + MinDuration: getIntPtr(20), + MaxDuration: getIntPtr(30), + AdvertiserExclusionPercent: getIntPtr(100), + IABCategoryExclusionPercent: getIntPtr(100), + }, + }, + wantErr: nil, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ext := &ExtVideoAdPod{ + Offset: tt.fields.Offset, + AdPod: tt.fields.AdPod, + } + actualErr := ext.Validate() + assert.Equal(t, tt.wantErr, actualErr) + }) + } +} diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go index 61c720fe023..c3f1c61ba6d 100644 --- a/openrtb_ext/bid.go +++ b/openrtb_ext/bid.go @@ -7,7 +7,8 @@ import ( // ExtBid defines the contract for bidresponse.seatbid.bid[i].ext type ExtBid struct { - Prebid *ExtBidPrebid `json:"prebid,omitempty"` + Prebid *ExtBidPrebid `json:"prebid,omitempty"` + Bidder json.RawMessage `json:"bidder,omitempty"` } // ExtBidPrebid defines the contract for bidresponse.seatbid.bid[i].ext.prebid @@ -61,6 +62,7 @@ type ExtBidPrebidMeta struct { type ExtBidPrebidVideo struct { Duration int `json:"duration"` PrimaryCategory string `json:"primary_category"` + VASTTagID string `json:"vasttagid"` } // ExtBidPrebidEvents defines the contract for bidresponse.seatbid.bid[i].ext.prebid.events @@ -175,4 +177,5 @@ const ( OriginalBidCpmKey = "origbidcpm" OriginalBidCurKey = "origbidcur" Passthrough = "passthrough" + OriginalBidCpmUsdKey = "origbidcpmusd" ) diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 4ddc21c8977..b64a19826c1 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -36,6 +36,7 @@ const ( BidderReservedPrebid BidderName = "prebid" // Reserved for Prebid Server configuration. BidderReservedSKAdN BidderName = "skadn" // Reserved for Apple's SKAdNetwork OpenRTB extension. BidderReservedTID BidderName = "tid" // Reserved for Per-Impression Transactions IDs for Multi-Impression Bid Requests. + BidderReservedCommerce BidderName = "commerce" ) // IsBidderNameReserved returns true if the specified name is a case insensitive match for a reserved bidder name. @@ -214,6 +215,7 @@ const ( BidderSonobi BidderName = "sonobi" BidderSovrn BidderName = "sovrn" BidderSspBC BidderName = "sspBC" + BidderSpotX BidderName = "spotx" BidderStreamkey BidderName = "streamkey" BidderStroeerCore BidderName = "stroeerCore" BidderSynacormedia BidderName = "synacormedia" @@ -227,6 +229,7 @@ const ( BidderUnicorn BidderName = "unicorn" BidderUnruly BidderName = "unruly" BidderValueImpression BidderName = "valueimpression" + BidderVASTBidder BidderName = "vastbidder" BidderVerizonMedia BidderName = "verizonmedia" BidderVideoByte BidderName = "videobyte" BidderVidoomy BidderName = "vidoomy" @@ -240,6 +243,10 @@ const ( BidderYieldone BidderName = "yieldone" BidderYSSP BidderName = "yssp" BidderZeroClickFraud BidderName = "zeroclickfraud" + BidderKoddi BidderName = "koddi" + BidderAdButtler BidderName = "adbuttler" + BidderCriteoRetail BidderName = "criteoretail" + ) // CoreBidderNames returns a slice of all core bidders. @@ -378,6 +385,7 @@ func CoreBidderNames() []BidderName { BidderSonobi, BidderSovrn, BidderSspBC, + BidderSpotX, BidderStreamkey, BidderStroeerCore, BidderSynacormedia, @@ -391,6 +399,7 @@ func CoreBidderNames() []BidderName { BidderUnicorn, BidderUnruly, BidderValueImpression, + BidderVASTBidder, BidderVerizonMedia, BidderVideoByte, BidderVidoomy, @@ -404,6 +413,9 @@ func CoreBidderNames() []BidderName { BidderYieldone, BidderYSSP, BidderZeroClickFraud, + BidderKoddi, + BidderAdButtler, + BidderCriteoRetail, } } @@ -525,3 +537,4 @@ func (validator *bidderParamValidator) Validate(name BidderName, ext json.RawMes func (validator *bidderParamValidator) Schema(name BidderName) string { return validator.schemaContents[name] } + diff --git a/openrtb_ext/device.go b/openrtb_ext/device.go index 8c5b36733b9..f5b0556980b 100644 --- a/openrtb_ext/device.go +++ b/openrtb_ext/device.go @@ -34,6 +34,14 @@ type ExtDevice struct { // Description: // Prebid extensions for the Device object. Prebid ExtDevicePrebid `json:"prebid"` + + // Attribute: + // ifa_type + // Type: + // string; optional + // Description: + // Contains source who generated ifa value + IFAType string `json:"ifa_type,omitempty"` } // IOSAppTrackingStatus describes the values for iOS app tracking authorization status. diff --git a/openrtb_ext/floors.go b/openrtb_ext/floors.go new file mode 100644 index 00000000000..413b2d54974 --- /dev/null +++ b/openrtb_ext/floors.go @@ -0,0 +1,55 @@ +package openrtb_ext + +// PriceFloorRules defines the contract for bidrequest.ext.prebid.floors +type PriceFloorRules struct { + FloorMin float64 `json:"floormin,omitempty"` + FloorMinCur string `json:"floormincur,omitempty"` + SkipRate int `json:"skiprate,omitempty"` + Location *PriceFloorEndpoint `json:"location,omitempty"` + Data *PriceFloorData `json:"data,omitempty"` + Enforcement *PriceFloorEnforcement `json:"enforcement,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Skipped *bool `json:"skipped,omitempty"` +} + +type PriceFloorEndpoint struct { + URL string `json:"url,omitempty"` +} + +type PriceFloorData struct { + Currency string `json:"currency,omitempty"` + SkipRate int `json:"skiprate,omitempty"` + FloorsSchemaVersion string `json:"floorsschemaversion,omitempty"` + ModelTimestamp int `json:"modeltimestamp,omitempty"` + ModelGroups []PriceFloorModelGroup `json:"modelgroups,omitempty"` +} + +type PriceFloorModelGroup struct { + Currency string `json:"currency,omitempty"` + ModelWeight int `json:"modelweight,omitempty"` + DebugWeight int `json:"debugweight,omitempty"` // Added for Debug purpose, shall be removed + ModelVersion string `json:"modelversion,omitempty"` + SkipRate int `json:"skiprate,omitempty"` + Schema PriceFloorSchema `json:"schema,omitempty"` + Values map[string]float64 `json:"values,omitempty"` + Default float64 `json:"default,omitempty"` +} +type PriceFloorSchema struct { + Fields []string `json:"fields,omitempty"` + Delimiter string `json:"delimiter,omitempty"` +} + +type PriceFloorEnforcement struct { + EnforcePBS *bool `json:"enforcepbs,omitempty"` + FloorDeals *bool `json:"floordeals,omitempty"` + BidAdjustment bool `json:"bidadjustment,omitempty"` + EnforceRate int `json:"enforcerate,omitempty"` +} + +// GetEnabled will check if floors is enabled in request +func (Floors *PriceFloorRules) GetEnabled() bool { + if Floors != nil && Floors.Enabled != nil && !*Floors.Enabled { + return *Floors.Enabled + } + return true +} diff --git a/openrtb_ext/imp_commerce.go b/openrtb_ext/imp_commerce.go new file mode 100644 index 00000000000..cf293f2bab3 --- /dev/null +++ b/openrtb_ext/imp_commerce.go @@ -0,0 +1,80 @@ +package openrtb_ext + +// ExtImpFilteringSubCategory - Impression Filtering SubCategory Extension +type ExtImpFilteringSubCategory struct { + Name string `json:"name,omitempty"` + Value []string `json:"value,omitempty"` +} + +// ExtImpPreferred - Impression Preferred Extension +type ExtImpPreferred struct { + ProductID string `json:"pid,omitempty"` + Rating float64 `json:"rating,omitempty"` +} + +// ExtImpFiltering - Impression Filtering Extension +type ExtImpFiltering struct { + Category []string `json:"category,omitempty"` + Brand []string `json:"brand,omitempty"` + SubCategory []*ExtImpFilteringSubCategory `json:"subcategory,omitempty"` +} + +// ExtImpTargeting - Impression Targeting Extension +type ExtImpTargeting struct { + Name string `json:"name,omitempty"` + Value interface{} `json:"value,omitempty"` +} + +type ExtCustomConfig struct { + Key string `json:"key,omitempty"` + Value string `json:"value,omitempty"` + Type int `json:"type,omitempty"` +} + +// ImpExtensionCommerce - Impression Commerce Extension +type CommerceParams struct { + SlotsRequested int `json:"slots_requested,omitempty"` + TestRequest bool `json:"test_request,omitempty"` + SearchTerm string `json:"search_term,omitempty"` + SearchType string `json:"search_type,omitempty"` + Preferred []*ExtImpPreferred `json:"preferred,omitempty"` + Filtering *ExtImpFiltering `json:"filtering,omitempty"` + Targeting []*ExtImpTargeting `json:"targeting,omitempty"` +} + +// ImpExtensionCommerce - Impression Commerce Extension +type ExtImpCommerce struct { + ComParams *CommerceParams `json:"commerce,omitempty"` + Bidder *ExtBidderCommerce `json:"bidder,omitempty"` +} +// UserExtensionCommerce - User Commerce Extension +type ExtUserCommerce struct { + IsAuthenticated bool `json:"is_authenticated,omitempty"` + Consent string `json:"consent,omitempty"` +} + +// SiteExtensionCommerce - Site Commerce Extension +type ExtSiteCommerce struct { + Page string `json:"page_name,omitempty"` +} + +// AppExtensionCommerce - App Commerce Extension +type ExtAppCommerce struct { + Page string `json:"page_name,omitempty"` +} + +type ExtBidderCommerce struct { + PrebidBidderName string `json:"prebidname,omitempty"` + BidderCode string `json:"biddercode,omitempty"` + CustomConfig []*ExtCustomConfig `json:"config,omitempty"` +} + +type ExtBidCommerce struct { + ProductId string `json:"productid,omitempty"` + ClickUrl string `json:"curl,omitempty"` + ConversionUrl string `json:"purl,omitempty"` + ClickPrice float64 `json:"clickprice,omitempty"` + Rate float64 `json:"rate,omitempty"` + ProductDetails map[string]interface{} `json:"productdetails,omitempty"` +} + diff --git a/openrtb_ext/imp_pubmatic.go b/openrtb_ext/imp_pubmatic.go index db96ca518c6..149885c8ece 100644 --- a/openrtb_ext/imp_pubmatic.go +++ b/openrtb_ext/imp_pubmatic.go @@ -11,8 +11,8 @@ import "encoding/json" type ExtImpPubmatic struct { PublisherId string `json:"publisherId"` AdSlot string `json:"adSlot"` - Dctr string `json:"dctr"` - PmZoneID string `json:"pmzoneid"` + Dctr string `json:"dctr,omitempty"` + PmZoneID string `json:"pmzoneid,omitempty"` WrapExt json.RawMessage `json:"wrapper,omitempty"` Keywords []*ExtImpPubmaticKeyVal `json:"keywords,omitempty"` Kadfloor string `json:"kadfloor,omitempty"` diff --git a/openrtb_ext/imp_spotx.go b/openrtb_ext/imp_spotx.go new file mode 100644 index 00000000000..ee209b78f6c --- /dev/null +++ b/openrtb_ext/imp_spotx.go @@ -0,0 +1,10 @@ +package openrtb_ext + +type ExtImpSpotX struct { + ChannelID string `json:"channel_id"` + AdUnit string `json:"ad_unit"` + Secure bool `json:"secure,omitempty"` + AdVolume float64 `json:"ad_volume,omitempty"` + PriceFloor int `json:"price_floor,omitempty"` + HideSkin bool `json:"hide_skin,omitempty"` +} diff --git a/openrtb_ext/imp_vastbidder.go b/openrtb_ext/imp_vastbidder.go new file mode 100644 index 00000000000..2923c2dd8d7 --- /dev/null +++ b/openrtb_ext/imp_vastbidder.go @@ -0,0 +1,18 @@ +package openrtb_ext + +// ExtImpVASTBidder defines the contract for bidrequest.imp[i].ext.vastbidder +type ExtImpVASTBidder struct { + Tags []*ExtImpVASTBidderTag `json:"tags,omitempty"` + Parser string `json:"parser,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Cookies map[string]string `json:"cookies,omitempty"` +} + +// ExtImpVASTBidderTag defines the contract for bidrequest.imp[i].ext.pubmatic.tags[i] +type ExtImpVASTBidderTag struct { + TagID string `json:"tagid"` + URL string `json:"url"` + Duration int `json:"dur"` + Price float64 `json:"price"` + Params map[string]interface{} `json:"params,omitempty"` +} diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index b3807c5d893..210f9b787ce 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -22,6 +22,8 @@ const GPIDKey = "gpid" // TIDKey reserved for Per-Impression Transactions IDs for Multi-Impression Bid Requests. const TIDKey = "tid" +const CommerceParamKey = "commerce" + // NativeExchangeSpecificLowerBound defines the lower threshold of exchange specific types for native ads. There is no upper bound. const NativeExchangeSpecificLowerBound = 500 @@ -58,6 +60,13 @@ type ExtRequestPrebid struct { // passing of personally identifiable information doesn't constitute a sale per CCPA law. // The array may contain a single sstar ('*') entry to represent all bidders. NoSale []string `json:"nosale,omitempty"` + + // Macros specifies list of custom macros along with the values. This is used while forming + // the tracker URLs, where PBS will replace the Custom Macro with its value with url-encoding + Macros map[string]string `json:"macros,omitempty"` + + Transparency *TransparencyExt `json:"transparency,omitempty"` + Floors *PriceFloorRules `json:"floors,omitempty"` } // Experiment defines if experimental features are available for the request @@ -70,6 +79,15 @@ type AdsCert struct { Enabled bool `json:"enabled,omitempty"` } +type TransparencyRule struct { + Include bool `json:"include,omitempty"` + Keys []string `json:"keys,omitempty"` +} + +type TransparencyExt struct { + Content map[string]TransparencyRule `json:"content,omitempty"` +} + type BidderConfig struct { Bidders []string `json:"bidders,omitempty"` Config *Config `json:"config,omitempty"` @@ -151,6 +169,7 @@ type ExtIncludeBrandCategory struct { Publisher string `json:"publisher"` WithCategory bool `json:"withcategory"` TranslateCategories *bool `json:"translatecategories,omitempty"` + SkipDedup bool `json:"skipdedup"` } // Make an unmarshaller that will set a default PriceGranularity @@ -180,6 +199,7 @@ func (ert *ExtRequestTargeting) UnmarshalJSON(b []byte) error { // PriceGranularity defines the allowed values for bidrequest.ext.prebid.targeting.pricegranularity type PriceGranularity struct { + Test bool `json:"test,omitempty"` Precision int `json:"precision,omitempty"` Ranges []GranularityRange `json:"ranges,omitempty"` } @@ -259,6 +279,10 @@ func PriceGranularityFromString(gran string) PriceGranularity { return priceGranularityAuto case "dense": return priceGranularityDense + case "ow-ctv-med": + return priceGranularityOWCTVMed + case "testpg": + return priceGranularityTestPG } // Return empty if not matched return PriceGranularity{} @@ -330,6 +354,23 @@ var priceGranularityAuto = PriceGranularity{ }, } +var priceGranularityOWCTVMed = PriceGranularity{ + Precision: 2, + Ranges: []GranularityRange{{ + Min: 0, + Max: 100, + Increment: 0.5}}, +} + +var priceGranularityTestPG = PriceGranularity{ + Test: true, + Precision: 2, + Ranges: []GranularityRange{{ + Min: 0, + Max: 50, + Increment: 50}}, +} + // ExtRequestPrebidData defines Prebid's First Party Data (FPD) and related bid request options. type ExtRequestPrebidData struct { EidPermissions []ExtRequestPrebidDataEidPermission `json:"eidpermissions"` diff --git a/openrtb_ext/request_test.go b/openrtb_ext/request_test.go index 98a2e1645a0..fb17f57ff16 100644 --- a/openrtb_ext/request_test.go +++ b/openrtb_ext/request_test.go @@ -46,6 +46,20 @@ func TestExtRequestTargeting(t *testing.T) { t.Errorf("ext3 expected Price granularity \"medium\", found \"%v\"", extRequest.Prebid.Targeting.PriceGranularity) } } + + extRequest = &ExtRequest{} + err = json.Unmarshal([]byte(ext4), extRequest) + if err != nil { + t.Errorf("ext4 Unmarshall failure: %s", err.Error()) + } + if extRequest.Prebid.Targeting == nil { + t.Error("ext4 Targeting is nil") + } else { + pgOWCTVMed := PriceGranularityFromString("ow-ctv-med") + if !reflect.DeepEqual(extRequest.Prebid.Targeting.PriceGranularity, pgOWCTVMed) { + t.Errorf("ext4 expected Price granularity \"ow-ctv-med\", found \"%v\"", extRequest.Prebid.Targeting.PriceGranularity) + } + } } const ext1 = `{ @@ -69,6 +83,14 @@ const ext3 = `{ } }` +const ext4 = `{ + "prebid": { + "targeting": { + "pricegranularity": "ow-ctv-med" + } + } +}` + func TestCacheIllegal(t *testing.T) { var bids ExtRequestPrebidCache if err := json.Unmarshal([]byte(`{}`), &bids); err == nil { diff --git a/openrtb_ext/response.go b/openrtb_ext/response.go index b20e741c18c..46351b98e90 100644 --- a/openrtb_ext/response.go +++ b/openrtb_ext/response.go @@ -28,6 +28,8 @@ type ExtResponseDebug struct { HttpCalls map[BidderName][]*ExtHttpCall `json:"httpcalls,omitempty"` // Request after resolution of stored requests and debug overrides ResolvedRequest json.RawMessage `json:"resolvedrequest,omitempty"` + // Request after flors signalling + UpdatedRequest json.RawMessage `json:"updatedrequest,omitempty"` } // ExtResponseSyncData defines the contract for bidresponse.ext.usersync.{bidder} @@ -62,6 +64,7 @@ type ExtHttpCall struct { RequestHeaders map[string][]string `json:"requestheaders"` ResponseBody string `json:"responsebody"` Status int `json:"status"` + Params map[string]int `json:"params,omitempty"` } // CookieStatus describes the allowed values for bidresponse.ext.usersync.{bidder}.status diff --git a/openrtb_ext/user.go b/openrtb_ext/user.go index d7c01c1f55b..885bc447c01 100644 --- a/openrtb_ext/user.go +++ b/openrtb_ext/user.go @@ -1,6 +1,8 @@ package openrtb_ext import ( + "encoding/json" + "github.com/mxmCherry/openrtb/v16/openrtb2" ) @@ -13,6 +15,8 @@ type ExtUser struct { Prebid *ExtUserPrebid `json:"prebid,omitempty"` Eids []openrtb2.EID `json:"eids,omitempty"` + + Data json.RawMessage `json:"data,omitempty"` } // ExtUserPrebid defines the contract for bidrequest.user.ext.prebid diff --git a/router/router.go b/router/router.go index f2ec00479f0..ed7a65d62b7 100644 --- a/router/router.go +++ b/router/router.go @@ -2,7 +2,6 @@ package router import ( "context" - "crypto/tls" "encoding/json" "fmt" "io/ioutil" @@ -12,28 +11,21 @@ import ( "time" analyticsConf "github.com/prebid/prebid-server/analytics/config" + + "github.com/prebid/prebid-server/usersync" + "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/currency" - "github.com/prebid/prebid-server/endpoints" - "github.com/prebid/prebid-server/endpoints/events" - infoEndpoints "github.com/prebid/prebid-server/endpoints/info" - "github.com/prebid/prebid-server/endpoints/openrtb2" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/exchange" "github.com/prebid/prebid-server/experiment/adscert" "github.com/prebid/prebid-server/gdpr" - "github.com/prebid/prebid-server/metrics" metricsConf "github.com/prebid/prebid-server/metrics/config" "github.com/prebid/prebid-server/openrtb_ext" - "github.com/prebid/prebid-server/pbs" pbc "github.com/prebid/prebid-server/prebid_cache_client" - "github.com/prebid/prebid-server/router/aspects" "github.com/prebid/prebid-server/server/ssl" storedRequestsConf "github.com/prebid/prebid-server/stored_requests/config" - "github.com/prebid/prebid-server/usersync" "github.com/prebid/prebid-server/util/sliceutil" - "github.com/prebid/prebid-server/util/uuidutil" - "github.com/prebid/prebid-server/version" "github.com/golang/glog" "github.com/julienschmidt/httprouter" @@ -112,9 +104,10 @@ type Router struct { Shutdown func() } +var schemaDirectory = "/home/http/GO_SERVER/dmhbserver/static/bidder-params" +var infoDirectory = "/home/http/GO_SERVER/dmhbserver/static/bidder-info" + func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *Router, err error) { - const schemaDirectory = "./static/bidder-params" - const infoDirectory = "./static/bidder-info" r = &Router{ Router: httprouter.New(), @@ -129,17 +122,33 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R glog.Infof("Could not read certificates file: %s \n", readCertErr.Error()) } + g_transport = getTransport(cfg, certPool) generalHttpClient := &http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - MaxConnsPerHost: cfg.Client.MaxConnsPerHost, - MaxIdleConns: cfg.Client.MaxIdleConns, - MaxIdleConnsPerHost: cfg.Client.MaxIdleConnsPerHost, - IdleConnTimeout: time.Duration(cfg.Client.IdleConnTimeout) * time.Second, - TLSClientConfig: &tls.Config{RootCAs: certPool}, - }, + Transport: g_transport, } + /* + * Add Dialer: + * Add TLSHandshakeTimeout: + * MaxConnsPerHost: Max value should be QPS + * MaxIdleConnsPerHost: + * ResponseHeaderTimeout: Max Timeout from OW End + * No Need for MaxIdleConns: + * + + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 100 * time.Millisecond, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + MaxIdleConnsPerHost: (maxIdleConnsPerHost / size), // ideal needs to be defined diff? + MaxConnsPerHost: (maxConnPerHost / size), + ResponseHeaderTimeout: responseHdrTimeout, + } + */ + cacheHttpClient := &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, @@ -164,6 +173,7 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R return nil, err } + var errs []error syncersByBidder, errs := usersync.BuildSyncers(cfg, bidderInfos) if len(errs) > 0 { return nil, errortypes.NewAggregateError("user sync", errs) @@ -180,9 +190,9 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R // Metrics engine r.MetricsEngine = metricsConf.NewMetricsEngine(cfg, openrtb_ext.CoreBidderNames(), syncerKeys) - shutdown, fetcher, ampFetcher, accounts, categoriesFetcher, videoFetcher, storedRespFetcher := storedRequestsConf.NewStoredRequests(cfg, r.MetricsEngine, generalHttpClient, r.Router) + _, fetcher, _, accounts, categoriesFetcher, videoFetcher, storedRespFetcher := storedRequestsConf.NewStoredRequests(cfg, r.MetricsEngine, generalHttpClient, r.Router) // todo(zachbadgett): better shutdown - r.Shutdown = shutdown + // r.Shutdown = shutdown pbsAnalytics := analyticsConf.NewPBSAnalytics(&cfg.Analytics) @@ -204,6 +214,14 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R gdprPermsBuilder := gdpr.NewPermissionsBuilder(cfg.GDPR, gvlVendorIDs, vendorListFetcher) tcf2CfgBuilder := gdpr.NewTCF2Config + if cfg.VendorListScheduler.Enabled { + vendorListScheduler, err := gdpr.GetVendorListScheduler(cfg.VendorListScheduler.Interval, cfg.VendorListScheduler.Timeout, generalHttpClient) + if err != nil { + glog.Fatal(err) + } + vendorListScheduler.Start() + } + cacheClient := pbc.NewClient(cacheHttpClient, &cfg.CacheURL, &cfg.ExtCacheURL, r.MetricsEngine) adapters, adaptersErrs := exchange.BuildAdapters(generalHttpClient, cfg, bidderInfos, r.MetricsEngine) @@ -217,7 +235,7 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R } theExchange := exchange.NewExchange(adapters, cacheClient, cfg, syncersByBidder, r.MetricsEngine, bidderInfos, gdprPermsBuilder, tcf2CfgBuilder, rateConvertor, categoriesFetcher, adsCertSigner) - var uuidGenerator uuidutil.UUIDRandomGenerator + /*var uuidGenerator uuidutil.UUIDRandomGenerator openrtbEndpoint, err := openrtb2.NewEndpoint(uuidGenerator, theExchange, paramsValidator, fetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders, storedRespFetcher) if err != nil { glog.Fatalf("Failed to create the openrtb2 endpoint handler. %v", err) @@ -269,7 +287,24 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R r.GET("/setuid", endpoints.NewSetUIDEndpoint(cfg, syncersByBidder, gdprPermsBuilder, tcf2CfgBuilder, pbsAnalytics, accounts, r.MetricsEngine)) r.GET("/getuids", endpoints.NewGetUIDsEndpoint(cfg.HostCookie)) r.POST("/optout", userSyncDeps.OptOut) - r.GET("/optout", userSyncDeps.OptOut) + r.GET("/optout", userSyncDeps.OptOut)*/ + + g_syncers = syncersByBidder + g_metrics = r.MetricsEngine + g_cfg = cfg + g_storedReqFetcher = &fetcher + g_accounts = &accounts + g_videoFetcher = &videoFetcher + g_storedRespFetcher = &storedRespFetcher + g_analytics = &pbsAnalytics + g_paramsValidator = ¶msValidator + g_activeBidders = activeBidders + g_disabledBidders = disabledBidders + g_defReqJSON = defReqJSON + g_cacheClient = &cacheClient + g_ex = &theExchange + g_gdprPermsBuilder = gdprPermsBuilder + g_tcf2CfgBuilder = tcf2CfgBuilder return r, nil } diff --git a/router/router_ow.go b/router/router_ow.go new file mode 100644 index 00000000000..f9026763774 --- /dev/null +++ b/router/router_ow.go @@ -0,0 +1,138 @@ +package router + +import ( + "crypto/tls" + "crypto/x509" + "net" + "net/http" + "time" + + "github.com/prebid/prebid-server/analytics" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/endpoints" + "github.com/prebid/prebid-server/endpoints/openrtb2" + "github.com/prebid/prebid-server/exchange" + "github.com/prebid/prebid-server/gdpr" + "github.com/prebid/prebid-server/metrics" + metricsConf "github.com/prebid/prebid-server/metrics/config" + "github.com/prebid/prebid-server/openrtb_ext" + pbc "github.com/prebid/prebid-server/prebid_cache_client" + "github.com/prebid/prebid-server/stored_requests" + "github.com/prebid/prebid-server/usersync" + "github.com/prebid/prebid-server/util/uuidutil" + "github.com/prometheus/client_golang/prometheus" +) + +var ( + g_syncers map[string]usersync.Syncer + g_cfg *config.Configuration + g_ex *exchange.Exchange + g_accounts *stored_requests.AccountFetcher + g_paramsValidator *openrtb_ext.BidderParamValidator + g_storedReqFetcher *stored_requests.Fetcher + g_storedRespFetcher *stored_requests.Fetcher + g_metrics metrics.MetricsEngine + g_analytics *analytics.PBSAnalyticsModule + g_disabledBidders map[string]string + g_videoFetcher *stored_requests.Fetcher + g_activeBidders map[string]openrtb_ext.BidderName + g_defReqJSON []byte + g_cacheClient *pbc.Client + g_transport *http.Transport + g_gdprPermsBuilder gdpr.PermissionsBuilder + g_tcf2CfgBuilder gdpr.TCF2ConfigBuilder +) + +func getTransport(cfg *config.Configuration, certPool *x509.CertPool) *http.Transport { + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + MaxConnsPerHost: cfg.Client.MaxConnsPerHost, + IdleConnTimeout: time.Duration(cfg.Client.IdleConnTimeout) * time.Second, + TLSClientConfig: &tls.Config{RootCAs: certPool, InsecureSkipVerify: cfg.Client.InsecureSkipVerify}, + } + + if cfg.Client.DialTimeout > 0 { + transport.Dial = (&net.Dialer{ + Timeout: time.Duration(cfg.Client.DialTimeout) * time.Millisecond, + KeepAlive: time.Duration(cfg.Client.DialKeepAlive) * time.Second, + }).Dial + } + + if cfg.Client.TLSHandshakeTimeout > 0 { + transport.TLSHandshakeTimeout = time.Duration(cfg.Client.TLSHandshakeTimeout) * time.Second + } + + if cfg.Client.ResponseHeaderTimeout > 0 { + transport.ResponseHeaderTimeout = time.Duration(cfg.Client.ResponseHeaderTimeout) * time.Second + } + + if cfg.Client.MaxIdleConns > 0 { + transport.MaxIdleConns = cfg.Client.MaxIdleConns + } + + if cfg.Client.MaxIdleConnsPerHost > 0 { + transport.MaxIdleConnsPerHost = cfg.Client.MaxIdleConnsPerHost + } + + return transport +} + +func GetCacheClient() *pbc.Client { + return g_cacheClient +} + +func GetPrebidCacheURL() string { + return g_cfg.ExternalURL +} + +//OrtbAuctionEndpointWrapper Openwrap wrapper method for calling /openrtb2/auction endpoint +func OrtbAuctionEndpointWrapper(w http.ResponseWriter, r *http.Request) error { + ortbAuctionEndpoint, err := openrtb2.NewEndpoint(uuidutil.UUIDRandomGenerator{}, *g_ex, *g_paramsValidator, *g_storedReqFetcher, *g_accounts, g_cfg, g_metrics, *g_analytics, g_disabledBidders, g_defReqJSON, g_activeBidders, *g_storedRespFetcher) + if err != nil { + return err + } + ortbAuctionEndpoint(w, r, nil) + return nil +} + +//VideoAuctionEndpointWrapper Openwrap wrapper method for calling /openrtb2/video endpoint +func VideoAuctionEndpointWrapper(w http.ResponseWriter, r *http.Request) error { + videoAuctionEndpoint, err := openrtb2.NewCTVEndpoint(*g_ex, *g_paramsValidator, *g_storedReqFetcher, *g_videoFetcher, *g_accounts, g_cfg, g_metrics, *g_analytics, g_disabledBidders, g_defReqJSON, g_activeBidders) + if err != nil { + return err + } + videoAuctionEndpoint(w, r, nil) + return nil +} + +//GetUIDSWrapper Openwrap wrapper method for calling /getuids endpoint +func GetUIDSWrapper(w http.ResponseWriter, r *http.Request) { + getUID := endpoints.NewGetUIDsEndpoint(g_cfg.HostCookie) + getUID(w, r, nil) +} + +//SetUIDSWrapper Openwrap wrapper method for calling /setuid endpoint +func SetUIDSWrapper(w http.ResponseWriter, r *http.Request) { + setUID := endpoints.NewSetUIDEndpoint(g_cfg, g_syncers, g_gdprPermsBuilder, g_tcf2CfgBuilder, *g_analytics, *g_accounts, g_metrics) + setUID(w, r, nil) +} + +//CookieSync Openwrap wrapper method for calling /cookie_sync endpoint +func CookieSync(w http.ResponseWriter, r *http.Request) { + cookiesync := endpoints.NewCookieSyncEndpoint(g_syncers, g_cfg, g_gdprPermsBuilder, g_tcf2CfgBuilder, g_metrics, *g_analytics, *g_accounts, g_activeBidders) + cookiesync.Handle(w, r, nil) +} + +//SyncerMap Returns map of bidder and its usersync info +func SyncerMap() map[string]usersync.Syncer { + return g_syncers +} + +func GetPrometheusGatherer() *prometheus.Registry { + mEngine, ok := g_metrics.(*metricsConf.DetailedMetricsEngine) + if !ok || mEngine == nil || mEngine.PrometheusMetrics == nil { + return nil + } + + return mEngine.PrometheusMetrics.Gatherer +} diff --git a/router/router_ow_test.go b/router/router_ow_test.go new file mode 100644 index 00000000000..6047b2b5ca8 --- /dev/null +++ b/router/router_ow_test.go @@ -0,0 +1,87 @@ +package router + +import ( + "testing" + + "github.com/julienschmidt/httprouter" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/currency" + "github.com/stretchr/testify/assert" +) + +func TestNew(t *testing.T) { + originalSchemaDirectory := schemaDirectory + originalinfoDirectory := infoDirectory + defer func() { + schemaDirectory = originalSchemaDirectory + infoDirectory = originalinfoDirectory + }() + schemaDirectory = "../static/bidder-params" + infoDirectory = "../static/bidder-info" + + type args struct { + cfg *config.Configuration + rateConvertor *currency.RateConverter + } + tests := []struct { + name string + args args + wantR *Router + wantErr bool + setup func() + }{ + { + name: "Happy path", + args: args{ + cfg: &config.Configuration{Adapters: map[string]config.Adapter{"pubmatic": {}}}, + rateConvertor: ¤cy.RateConverter{}, + }, + wantR: &Router{Router: &httprouter.Router{}}, + wantErr: false, + setup: func() { + g_syncers = nil + g_cfg = nil + g_ex = nil + g_accounts = nil + g_paramsValidator = nil + g_storedReqFetcher = nil + g_storedRespFetcher = nil + g_metrics = nil + g_analytics = nil + g_disabledBidders = nil + g_videoFetcher = nil + g_activeBidders = nil + g_defReqJSON = nil + g_cacheClient = nil + g_transport = nil + g_gdprPermsBuilder = nil + g_tcf2CfgBuilder = nil + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup() + _, err := New(tt.args.cfg, tt.args.rateConvertor) + assert.Equal(t, tt.wantErr, err != nil, err) + + assert.NotNil(t, g_syncers) + assert.NotNil(t, g_cfg) + assert.NotNil(t, g_ex) + assert.NotNil(t, g_accounts) + assert.NotNil(t, g_paramsValidator) + assert.NotNil(t, g_storedReqFetcher) + assert.NotNil(t, g_storedRespFetcher) + assert.NotNil(t, g_metrics) + assert.NotNil(t, g_analytics) + assert.NotNil(t, g_disabledBidders) + assert.NotNil(t, g_videoFetcher) + assert.NotNil(t, g_activeBidders) + assert.NotNil(t, g_defReqJSON) + assert.NotNil(t, g_cacheClient) + assert.NotNil(t, g_transport) + assert.NotNil(t, g_gdprPermsBuilder) + assert.NotNil(t, g_tcf2CfgBuilder) + }) + } +} diff --git a/router/router_test.go b/router/router_test.go index b4ceaff16a9..b83690fdf55 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -7,9 +7,9 @@ import ( "net/http/httptest" "testing" + _ "github.com/lib/pq" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" - "github.com/stretchr/testify/assert" ) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 640f8022f57..51f8c7a97d8 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -22,9 +22,13 @@ generate_cover_data() { for pkg in "$@"; do f="$workdir/$(echo $pkg | tr / -).cover" cover="" - if ! [[ "$pkg" =~ ^github\.com\/prebid\/prebid\-server$ ]]; then + if ! [[ "$pkg" =~ ^github\.com\/PubMatic\-OpenWrap\/prebid\-server$ ]]; then cover="-covermode=$mode -coverprofile=$f" fi + # util/task uses _test package name + if [[ "$pkg" =~ ^github\.com\/PubMatic\-OpenWrap\/prebid\-server\/util\/task$ ]]; then + cover+=" -coverpkg=github.com/prebid/prebid-server/util/task" + fi go test ${cover} "$pkg" done diff --git a/scripts/upgrade-pbs.sh b/scripts/upgrade-pbs.sh new file mode 100755 index 00000000000..c205bca08df --- /dev/null +++ b/scripts/upgrade-pbs.sh @@ -0,0 +1,262 @@ +#!/bin/bash -e + +prefix="v" +to_major=0 +to_minor=217 +to_patch=0 +upgrade_version="$prefix$to_major.$to_minor.$to_patch" + +attempt=4 + +usage=" +Script starts or continues prebid upgrade to version set in 'to_minor' variable. Workspace is at /tmp/prebid-server and /tmp/pbs-patch + + ./upgrade-pbs.sh [--restart] + + --restart Restart the upgrade (deletes /tmp/prebid-server and /tmp/pbs-patch) + -h Help + +TODO: + - paramertrize the script + - create ci branch PR + - create header-bidding PR" + +RESTART=0 +for i in "$@"; do + case $i in + --restart) + RESTART=1 + shift + ;; + -h) + echo "$usage" + exit 0 + ;; + esac +done + +# --- start --- +CHECKLOG=/tmp/pbs-patch/checkpoints.log + +trap 'clear_log' EXIT + +log () { + printf "\n$(date): $1\n" +} + +clear_log() { + major=0 + minor=0 + patch=0 + get_current_tag_version major minor patch + current_fork_at_version="$major.$minor.$patch" + + if [ "$current_fork_at_version" == "$upgrade_version" ] ; then + log "Upgraded to $current_fork_at_version" + rm -f "$CHECKLOG" + + log "Last validation before creating PR" + go_mod + checkpoint_run "./validate.sh --race 5" + go_discard + + set +e + log "Commit final go.mod and go.sum" + git commit go.mod go.sum --amend --no-edit + set -e + else + log "Exiting with failure!!!" + exit 1 + fi +} + +get_current_tag_version() { + log "get_current_tag_version $*" + + local -n _major=$1 + local -n _minor=$2 + local -n _patch=$3 + + # script will always start from start if origin/master is used. + # common_commit=$(git merge-base prebid-upstream/master origin/master) + # log "Common commit b/w prebid-upstream/master origin/master: $common_commit" + + # remove origin for master to continue from last fixed tag's rebase. + common_commit=$(git merge-base prebid-upstream/master master) + log "Common commit b/w prebid-upstream/master master: $common_commit" + + current_version=$(git tag --points-at $common_commit) + if [[ $current_version == v* ]] ; then + log "Current Version: $current_version" + else + log "Failed to detected current version. Abort." + exit 1 + # abort + # cd prebid-server; git rebase --abort;cd - + fi + + IFS='.' read -r -a _current_version <<< "$current_version" + _major=${_current_version[0]} + _minor=${_current_version[1]} + _patch=${_current_version[2]} +} + +clone_repo() { + if [ -d "/tmp/prebid-server" ]; then + log "Code already cloned. Attempting to continue the upgrade!!!" + else + log "Cloning repo at /tmp" + cd /tmp + git clone https://github.com/PubMatic-OpenWrap/prebid-server.git + cd prebid-server + + git remote add prebid-upstream https://github.com/prebid/prebid-server.git + git remote -v + git fetch --all --tags --prune + fi +} + +checkout_branch() { + set +e + git checkout tags/$_upgrade_version -b $tag_base_branch_name + # git push origin $tag_base_branch_name + + git checkout -b $upgrade_branch_name + # git push origin $upgrade_branch_name + + set -e +# if [ "$?" -ne 0 ] +# then +# log "Failed to create branch $upgrade_branch_name. Already working on it???" +# exit 1 +# fi +} + +cmd_exe() { + cmd=$* + if ! $cmd; then + log "Failure!!! creating checkpoint $cmd" + echo "$cmd" > $CHECKLOG + exit 1 + fi +} + +checkpoint_run() { + cmd=$* + if [ -f $CHECKLOG ] ; then + if grep -q "$cmd" "$CHECKLOG"; then + log "Retry this checkpoint: $cmd" + rm "$CHECKLOG" + elif grep -q "./validate.sh --race 5" "$CHECKLOG"; then + log "Special checkpoint. ./validate.sh --race 5 failed for last tag update. Hence, only fixes are expected in successfully upgraded branch. (change in func() def, wrong conflict resolve, etc)" + cmd_exe $cmd + rm "$CHECKLOG" + else + log "Skip this checkpoint: $cmd" + return + fi + fi + cmd_exe $cmd +} + +go_mod() { + go mod download all + go mod tidy + go mod tidy + go mod download all +} + +go_discard() { + # discard local changes if any. manual validate, compile, etc + # git checkout master go.mod + # git checkout master go.sum + git checkout go.mod go.sum +} + +# --- main --- + +if [ "$RESTART" -eq "1" ]; then + log "Restarting the upgrade: rm -rf /tmp/prebid-server /tmp/pbs-patch/" + rm -rf /tmp/prebid-server /tmp/pbs-patch/ + mkdir -p /tmp/pbs-patch/ +fi + +log "Final Upgrade Version: $upgrade_version" +log "Attempt: $attempt" + +checkpoint_run clone_repo +cd /tmp/prebid-server +log "At $(pwd)" + +# code merged in master +# if [ "$RESTART" -eq "1" ]; then +# # TODO: commit this in origin/master,ci and remove it from here. +# git merge --squash origin/UOE-7610-1-upgrade.sh +# git commit --no-edit +# fi + +major=0 +minor=0 +patch=0 + +get_current_tag_version major minor patch +current_fork_at_version="$major.$minor.$patch" +git diff tags/$current_fork_at_version..origin/master > /tmp/pbs-patch/current_ow_patch-$current_fork_at_version-origin_master-$attempt.diff + +((minor++)) +log "Starting with version split major:$major, minor:$minor, patch:$patch" + +# how to validate with this code +# if [ "$RESTART" -eq "1" ]; then +# # Solving go.mod and go.sum conflicts would be easy at last as we would need to only pick the OW-patch entries rather than resolving conflict for every version +# log "Using latest go.mod and go.sum. Patch OW changes at last" +# git checkout tags/$current_fork_at_version go.mod +# git checkout tags/$current_fork_at_version go.sum +# git commit go.mod go.sum -m "[upgrade-start-checkpoint] tags/$current_fork_at_version go.mod go.sum" +# fi + +log "Checking if last failure was for test case. Need this to pick correct" +go_mod +checkpoint_run "./validate.sh --race 5" +go_discard + +log "Starting upgrade loop..." +while [ "$minor" -le "$to_minor" ]; do + # _upgrade_version="$prefix$major.$minor.$patch" + _upgrade_version="$major.$minor.$patch" + ((minor++)) + + log "Starting upgrade to version $_upgrade_version" + + tag_base_branch_name=prebid_$_upgrade_version-$attempt-tag + upgrade_branch_name=prebid_$_upgrade_version-$attempt + log "Reference tag branch: $tag_base_branch_name" + log "Upgrade branch: $upgrade_branch_name" + + checkpoint_run checkout_branch + + checkpoint_run git merge master --no-edit + # Use `git commit --amend --no-edit` if you had to fix test cases, etc for wrong merge conflict resolve, etc. + log "Validating the master merge into current tag. Fix and commit changes if required. Use 'git commit --amend --no-edit' for consistency" + go_mod + checkpoint_run "./validate.sh --race 5" + go_discard + + checkpoint_run git checkout master + checkpoint_run git merge $upgrade_branch_name --no-edit + + log "Generating patch file at /tmp/pbs-patch/ for $_upgrade_version" + git diff tags/$_upgrade_version..master > /tmp/pbs-patch/new_ow_patch_$upgrade_version-master-1.diff +done + +# TODO: +# diff tags/v0.192.0..origin/master +# diff tags/v0.207.0..prebid_v0.207.0 + +# TODO: UPDATE HEADER-BIDDING GO-MOD + + +# TODO: automate go.mod conflicts +# go mod edit -replace github.com/prebid/prebid-server=./ +# go mod edit -replace github.com/mxmCherry/openrtb/v16=github.com/PubMatic-OpenWrap/openrtb/v15@v15.0.0 +# go mod edit -replace github.com/beevik/etree=github.com/PubMatic-OpenWrap/etree@latest diff --git a/static/bidder-info/adbuttler.yaml b/static/bidder-info/adbuttler.yaml new file mode 100644 index 00000000000..8c34aacb9d8 --- /dev/null +++ b/static/bidder-info/adbuttler.yaml @@ -0,0 +1,10 @@ +maintainer: + email: adbuttler@pubmatic.com +gvlVendorID: 997 +capabilities: + app: + mediaTypes: + - banner + site: + mediaTypes: + - banner diff --git a/static/bidder-info/criteoretail.yaml b/static/bidder-info/criteoretail.yaml new file mode 100644 index 00000000000..f112d74986e --- /dev/null +++ b/static/bidder-info/criteoretail.yaml @@ -0,0 +1,11 @@ +maintainer: + email: criteoretail@pubmatic.com +gvlVendorID: 997 +capabilities: + app: + mediaTypes: + - banner + site: + mediaTypes: + - banner + diff --git a/static/bidder-info/koddi.yaml b/static/bidder-info/koddi.yaml new file mode 100644 index 00000000000..2dc2037bc15 --- /dev/null +++ b/static/bidder-info/koddi.yaml @@ -0,0 +1,10 @@ +maintainer: + email: koddi@pubmatic.com +gvlVendorID: 998 +capabilities: + app: + mediaTypes: + - banner + site: + mediaTypes: + - banner diff --git a/static/bidder-info/spotx.yaml b/static/bidder-info/spotx.yaml new file mode 100644 index 00000000000..51824561022 --- /dev/null +++ b/static/bidder-info/spotx.yaml @@ -0,0 +1,10 @@ +maintainer: + email: "teameighties@spotx.tv" +gvlVendorID: 165 +capabilities: + app: + mediaTypes: + - video + site: + mediaTypes: + - video diff --git a/static/bidder-info/vastbidder.yaml b/static/bidder-info/vastbidder.yaml new file mode 100644 index 00000000000..b8eb41d4e49 --- /dev/null +++ b/static/bidder-info/vastbidder.yaml @@ -0,0 +1,9 @@ +maintainer: + email: "UOEDev@pubmatic.com" +capabilities: + app: + mediaTypes: + - video + site: + mediaTypes: + - video diff --git a/static/bidder-params/adbuttler.json b/static/bidder-params/adbuttler.json new file mode 100644 index 00000000000..e77ffd92ea9 --- /dev/null +++ b/static/bidder-params/adbuttler.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdButtler Adapter Params", + "description": "A schema which validates params accepted by the AdButtler adapter", + "type": "object" +} diff --git a/static/bidder-params/criteoretail.json b/static/bidder-params/criteoretail.json new file mode 100644 index 00000000000..5e930b09f28 --- /dev/null +++ b/static/bidder-params/criteoretail.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "CriteoRetail Adapter Params", + "description": "A schema which validates params accepted by the CriteoRetail adapter", + "type": "object" +} diff --git a/static/bidder-params/koddi.json b/static/bidder-params/koddi.json new file mode 100644 index 00000000000..2efde850641 --- /dev/null +++ b/static/bidder-params/koddi.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Koddi Adapter Params", + "description": "A schema which validates params accepted by the Koddi adapter", + "type": "object" +} diff --git a/static/bidder-params/spotx.json b/static/bidder-params/spotx.json new file mode 100644 index 00000000000..13b72f2156b --- /dev/null +++ b/static/bidder-params/spotx.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "OpenX Adapter Params", + "description": "A schema which validates params accepted by the OpenX adapter", + + "type": "object", + "properties": { + "channel_id": { + "type": "string", + "minLength": 5, + "maxLength": 5, + "description": "A unique 5 digit ID that is generated by the SpotX publisher platform when a channel is created" + }, + "ad_unit": { + "type": "string", + "description": "Token that describes which ad unit to play: instream or outstream", + "enum": ["instream", "outstream"] + }, + "secure": { + "type": "boolean", + "description": "Boolean identifying whether the reqeusts should be https or not (used to override the protocol if the page isn’t secure." + }, + "ad_volume": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Value between 0 and 1 to denote the volume the ad should start at." + }, + "price_floor": { + "type": "integer", + "description": "Set the current channel price floor in real time." + }, + "hide_skin": { + "type": "boolean", + "description": "Set to true to hide the spotx skin." + } + }, + "required": ["channel_id", "ad_unit"] +} diff --git a/static/bidder-params/vastbidder.json b/static/bidder-params/vastbidder.json new file mode 100644 index 00000000000..0bef9b5fd5e --- /dev/null +++ b/static/bidder-params/vastbidder.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Tag Bidder Base Adapter", + "description": "A schema which validates params accepted by the VAST tag bidders", + + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tagid": { "type": "string" }, + "url": { "type": "string" }, + "dur": { "type": "integer" }, + "price": { "type": "number" }, + "params": { "type": "object" } + }, + "required": [ "tagid", "url", "dur" ] + } + }, + "parser": { "type": "string" }, + "headers": { "type": "object" }, + "cookies": { "type": "object" } + }, + "required": ["tags"] +} \ No newline at end of file diff --git a/usersync/chooser.go b/usersync/chooser.go index d371846364a..c3e65fee022 100644 --- a/usersync/chooser.go +++ b/usersync/chooser.go @@ -132,7 +132,11 @@ func (c standardChooser) Choose(request Request, cookie *Cookie) Result { } func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{}, syncTypeFilter SyncTypeFilter, privacy Privacy, cookie *Cookie) (Syncer, BidderEvaluation) { - syncer, exists := c.bidderSyncerLookup[bidder] + bidderName := bidder + if bidderName == "indexExchange" { + bidderName = "ix" + } + syncer, exists := c.bidderSyncerLookup[bidderName] if !exists { return nil, BidderEvaluation{Bidder: bidder, Status: StatusUnknownBidder} } diff --git a/usersync/cookie.go b/usersync/cookie.go index 5732e6c4c31..ee3eae95028 100644 --- a/usersync/cookie.go +++ b/usersync/cookie.go @@ -146,6 +146,7 @@ func (cookie *Cookie) GetUIDs() map[string]string { func (cookie *Cookie) SetCookieOnResponse(w http.ResponseWriter, setSiteCookie bool, cfg *config.HostCookie, ttl time.Duration) { httpCookie := cookie.ToHTTPCookie(ttl) var domain string = cfg.Domain + httpCookie.Secure = true if domain != "" { httpCookie.Domain = domain @@ -171,7 +172,7 @@ func (cookie *Cookie) SetCookieOnResponse(w http.ResponseWriter, setSiteCookie b } if setSiteCookie { - httpCookie.Secure = true + // httpCookie.Secure = true httpCookie.SameSite = http.SameSiteNoneMode } w.Header().Add("Set-Cookie", httpCookie.String())