diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample_appendbiddernames.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_appendbiddernames.json new file mode 100644 index 00000000000..4850fe91652 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_appendbiddernames.json @@ -0,0 +1,86 @@ +{ + "description": "Video endpoint valid request with AppendBidderNames.", + "requestPayload": { + "appendbiddernames": true, + "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22", + "podconfig": { + "durationrangesec": [ + 30 + ], + "requireexactduration": true, + "pods": [{ + "podid": 1, + "adpoddurationsec": 180, + "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6" + }, + { + "podid": 2, + "adpoddurationsec": 150, + "configid": "8b452b41-2681-4a20-9086-6f16ffad7773" + } + ] + }, + "site": { + "page": "prebid.com" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "yob": 1991, + "gender": "F", + "keywords": "Hotels, Travelling", + "ext": { + "prebid": { + "buyeruids": { + "appnexus": "unique_id_an", + "rubicon": "unique_id_rubi" + } + } + } + }, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", + "ip": "123.145.167.10", + "devicetype": 1, + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", + "lmt": 44, + "os": "mac os", + "w": 640, + "h": 480, + "didsha1": "didsha1", + "didmd5": "didmd5", + "dpidsha1": "dpidsha1", + "dpidmd5": "dpidmd5", + "macsha1": "macsha1", + "macmd5": "macmd5" + }, + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "" + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, 3, 5, 6 + ] + }, + "content": { + "episode": 6, + "title": "episodeName", + "series": "TvName", + "season": "season3", + "len": 900, + "livestream": 0 + }, + "cacheconfig": { + "ttl": 42 + } + } +} \ No newline at end of file diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index ab5634c7853..e277e362a28 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -624,6 +624,7 @@ func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, erro IncludeBrandCategory: inclBrandCat, DurationRangeSec: durationRangeSec, IncludeBidderKeys: true, + AppendBidderNames: videoRequest.AppendBidderNames, } vastXml := openrtb_ext.ExtRequestPrebidCacheVAST{} diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 27741b22dcd..33411c87555 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -1149,6 +1149,48 @@ func TestCCPA(t *testing.T) { } } +func TestVideoEndpointAppendBidderNames(t *testing.T) { + ex := &mockExchangeAppendBidderNames{} + reqData, err := ioutil.ReadFile("sample-requests/video/video_valid_sample_appendbiddernames.json") + if err != nil { + t.Fatalf("Failed to fetch a valid request: %v", err) + } + reqBody := string(getRequestPayload(t, reqData)) + req := httptest.NewRequest("POST", "/openrtb2/video", strings.NewReader(reqBody)) + recorder := httptest.NewRecorder() + + deps := mockDepsAppendBidderNames(t, ex) + deps.VideoAuctionEndpoint(recorder, req, nil) + + if ex.lastRequest == nil { + t.Fatalf("The request never made it into the Exchange.") + } + + var extData openrtb_ext.ExtRequest + json.Unmarshal(ex.lastRequest.Ext, &extData) + assert.True(t, extData.Prebid.Targeting.AppendBidderNames, "Request ext incorrect: AppendBidderNames should be true ") + + respBytes := recorder.Body.Bytes() + resp := &openrtb_ext.BidResponseVideo{} + if err := json.Unmarshal(respBytes, resp); err != nil { + t.Fatalf("Unable to unmarshal response.") + } + + assert.Len(t, ex.lastRequest.Imp, 11, "Incorrect number of impressions in request") + assert.Equal(t, string(ex.lastRequest.Site.Page), "prebid.com", "Incorrect site page in request") + assert.Equal(t, ex.lastRequest.Site.Content.Series, "TvName", "Incorrect site content series in request") + + assert.Len(t, resp.AdPods, 5, "Incorrect number of Ad Pods in response") + assert.Len(t, resp.AdPods[0].Targeting, 4, "Incorrect Targeting data in response") + assert.Len(t, resp.AdPods[1].Targeting, 3, "Incorrect Targeting data in response") + assert.Len(t, resp.AdPods[2].Targeting, 5, "Incorrect Targeting data in response") + assert.Len(t, resp.AdPods[3].Targeting, 1, "Incorrect Targeting data in response") + assert.Len(t, resp.AdPods[4].Targeting, 3, "Incorrect Targeting data in response") + + assert.Equal(t, resp.AdPods[4].Targeting[0].HbPbCatDur, "20.00_395_30s_appnexus", "Incorrect number of Ad Pods in response") + +} + func TestFormatTargetingKey(t *testing.T) { res := formatTargetingKey(openrtb_ext.HbCategoryDurationKey, "appnexus") assert.Equal(t, "hb_pb_cat_dur_appnex", res, "Tergeting key constructed incorrectly") @@ -1229,6 +1271,30 @@ func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps { return deps } +func mockDepsAppendBidderNames(t *testing.T, ex *mockExchangeAppendBidderNames) *endpointDeps { + theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + deps := &endpointDeps{ + ex, + newParamsValidator(t), + &mockVideoStoredReqFetcher{}, + &mockVideoStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: maxSize}, + theMetrics, + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BidderMap, + ex.cache, + regexp.MustCompile(`[<>]`), + hardcodedResponseIPValidator{response: true}, + } + + return deps +} + func mockDepsNoBids(t *testing.T, ex *mockExchangeVideoNoBids) *endpointDeps { theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) edep := &endpointDeps{ @@ -1311,6 +1377,42 @@ func (m *mockExchangeVideo) HoldAuction(ctx context.Context, bidRequest *openrtb }, nil } +type mockExchangeAppendBidderNames struct { + lastRequest *openrtb.BidRequest + cache *mockCacheClient +} + +func (m *mockExchangeAppendBidderNames) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, account *config.Account, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { + m.lastRequest = bidRequest + if debugLog != nil && debugLog.Enabled { + m.cache.called = true + } + ext := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"20.00","hb_pb_cat_dur_appnex":"20.00_395_30s_appnexus","hb_size":"1x1", "hb_uuid_appnexus":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"},"type":"video"},"bidder":{"appnexus":{"brand_id":1,"auction_id":7840037870526938650,"bidder_id":2,"bid_ad_type":1,"creative_info":{"video":{"duration":30,"mimes":["video\/mp4"]}}}}}`) + return &openrtb.BidResponse{ + SeatBid: []openrtb.SeatBid{{ + Seat: "appnexus", + Bid: []openrtb.Bid{ + {ID: "01", ImpID: "1_0", Ext: ext}, + {ID: "02", ImpID: "1_1", Ext: ext}, + {ID: "03", ImpID: "1_2", Ext: ext}, + {ID: "04", ImpID: "1_3", Ext: ext}, + {ID: "05", ImpID: "2_0", Ext: ext}, + {ID: "06", ImpID: "2_1", Ext: ext}, + {ID: "07", ImpID: "2_2", Ext: ext}, + {ID: "08", ImpID: "3_0", Ext: ext}, + {ID: "09", ImpID: "3_1", Ext: ext}, + {ID: "10", ImpID: "3_2", Ext: ext}, + {ID: "11", ImpID: "3_3", Ext: ext}, + {ID: "12", ImpID: "3_5", Ext: ext}, + {ID: "13", ImpID: "4_0", Ext: ext}, + {ID: "14", ImpID: "5_0", Ext: ext}, + {ID: "15", ImpID: "5_1", Ext: ext}, + {ID: "16", ImpID: "5_2", Ext: ext}, + }, + }}, + }, nil +} + type mockExchangeVideoNoBids struct { lastRequest *openrtb.BidRequest cache *mockCacheClient diff --git a/exchange/exchange.go b/exchange/exchange.go index 62fb9a2c952..64b0043c129 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -535,6 +535,7 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques //If ext.prebid.targeting.includebrandcategory is present in ext then competitive exclusion feature is on. var includeBrandCategory = brandCatExt != nil //if not present - category will no be appended + appendBidderNames := requestExt.Prebid.Targeting.AppendBidderNames var primaryAdServer string var publisher string @@ -630,6 +631,10 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques dupeKey = categoryDuration } + if appendBidderNames { + categoryDuration = fmt.Sprintf("%s_%s", categoryDuration, bidderName.String()) + } + if dupe, ok := dedupe[dupeKey]; ok { dupeBidPrice, err := strconv.ParseFloat(dupe.bidPrice, 64) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 2b91f1b07ca..99c351c40f4 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -1888,6 +1888,114 @@ func TestNoCategoryDedupe(t *testing.T) { } +func TestCategoryMappingBidderName(t *testing.T) { + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + requestExt := newExtRequest() + requestExt.Prebid.Targeting.AppendBidderNames = true + + targData := &targetData{ + priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, + includeWinners: true, + } + + requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30} + + adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + + cats1 := []string{"IAB1-1"} + cats2 := []string{"IAB1-2"} + bid1 := openrtb.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} + bid2 := openrtb.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 10.0000, Cat: cats2, W: 1, H: 1} + + bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + + innerBids1 := []*pbsOrtbBid{ + &bid1_1, + } + innerBids2 := []*pbsOrtbBid{ + &bid1_2, + } + + seatBid1 := pbsOrtbSeatBid{innerBids1, "USD", nil, nil} + bidderName1 := openrtb_ext.BidderName("bidder1") + + seatBid2 := pbsOrtbSeatBid{innerBids2, "USD", nil, nil} + bidderName2 := openrtb_ext.BidderName("bidder2") + + adapterBids[bidderName1] = &seatBid1 + adapterBids[bidderName2] = &seatBid2 + + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData) + + assert.NoError(t, err, "Category mapping error should be empty") + assert.Empty(t, rejections, "There should be 0 bid rejection messages") + assert.Equal(t, "10.00_VideoGames_30s_bidder1", bidCategory["bid_id1"], "Category mapping doesn't match") + assert.Equal(t, "10.00_HomeDecor_30s_bidder2", bidCategory["bid_id2"], "Category mapping doesn't match") + assert.Len(t, adapterBids[bidderName1].bids, 1, "Bidders number doesn't match") + assert.Len(t, adapterBids[bidderName2].bids, 1, "Bidders number doesn't match") + assert.Len(t, bidCategory, 2, "Bidders category mapping doesn't match") +} + +func TestCategoryMappingBidderNameNoCategories(t *testing.T) { + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + requestExt := newExtRequestNoBrandCat() + requestExt.Prebid.Targeting.AppendBidderNames = true + + targData := &targetData{ + priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, + includeWinners: true, + } + + requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30} + + adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + + cats1 := []string{"IAB1-1"} + cats2 := []string{"IAB1-2"} + bid1 := openrtb.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} + bid2 := openrtb.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 12.0000, Cat: cats2, W: 1, H: 1} + + bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + + innerBids1 := []*pbsOrtbBid{ + &bid1_1, + } + innerBids2 := []*pbsOrtbBid{ + &bid1_2, + } + + seatBid1 := pbsOrtbSeatBid{innerBids1, "USD", nil, nil} + bidderName1 := openrtb_ext.BidderName("bidder1") + + seatBid2 := pbsOrtbSeatBid{innerBids2, "USD", nil, nil} + bidderName2 := openrtb_ext.BidderName("bidder2") + + adapterBids[bidderName1] = &seatBid1 + adapterBids[bidderName2] = &seatBid2 + + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData) + + assert.NoError(t, err, "Category mapping error should be empty") + assert.Empty(t, rejections, "There should be 0 bid rejection messages") + assert.Equal(t, "10.00_30s_bidder1", bidCategory["bid_id1"], "Category mapping doesn't match") + assert.Equal(t, "12.00_30s_bidder2", bidCategory["bid_id2"], "Category mapping doesn't match") + assert.Len(t, adapterBids[bidderName1].bids, 1, "Bidders number doesn't match") + assert.Len(t, adapterBids[bidderName2].bids, 1, "Bidders number doesn't match") + assert.Len(t, bidCategory, 2, "Bidders category mapping doesn't match") +} + func TestBidRejectionErrors(t *testing.T) { categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") if error != nil { diff --git a/exchange/exchangetest/append-bidder-names.json b/exchange/exchangetest/append-bidder-names.json new file mode 100644 index 00000000000..1247b9f0261 --- /dev/null +++ b/exchange/exchangetest/append-bidder-names.json @@ -0,0 +1,222 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + }, + { + "id": "imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + } + ], + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + }, + "appendbiddernames": true + } + } + } + }, + "usersyncs": { + "appnexus": "123" + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [ + { + "ortbBid": { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + }, + "bidType": "video", + "bidVideo": { + "duration": 30, + "PrimaryCategory": "" + } + }, + { + "ortbBid": { + "id": "apn-other-bid", + "impid": "imp-id-2", + "price": 0.6, + "w": 300, + "h": 500, + "crid": "creative-3", + "cat": [ + "IAB1-2" + ] + }, + "bidType": "video" + } + ] + } + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "ext": { + "prebid": { + "type": "video", + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "0.20", + "hb_pb_appnexus": "0.20", + "hb_pb_cat_dur": "0.20_VideoGames_0s_appnexus", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s_appnexus", + "hb_size": "200x250", + "hb_size_appnexus": "200x250" + } + } + } + }, + { + "cat": [ + "IAB1-2" + ], + "crid": "creative-3", + "ext": { + "prebid": { + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "0.50", + "hb_pb_appnexus": "0.50", + "hb_pb_cat_dur": "0.50_HomeDecor_0s_appnexus", + "hb_pb_cat_dur_appnex": "0.50_HomeDecor_0s_appnexus", + "hb_size": "300x500", + "hb_size_appnexus": "300x500" + }, + "type": "video" + } + }, + "h": 500, + "id": "apn-other-bid", + "impid": "imp-id-2", + "price": 0.6, + "w": 300 + } + ] + } + ] + }, + "ext": { + "debug": { + "httpcalls": { + "appnexus": null + }, + "resolvedrequest": { + "id": "some-request-id", + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + }, + { + "id": "imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + } + ], + "site": { + "page": "test.somepage.com" + }, + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + }, + "appendbiddernames": true + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/openrtb_ext/bid_request_video.go b/openrtb_ext/bid_request_video.go index 18865108433..09edf51d0db 100644 --- a/openrtb_ext/bid_request_video.go +++ b/openrtb_ext/bid_request_video.go @@ -144,6 +144,13 @@ type BidRequestVideo struct { // Description: // Indicates that the response should update key to include prefix and tier SupportDeals bool `json:"supportdeals,omitempty"` + + // Attribute: + // appendbiddernames + // Type: + // boolean, optional + // Flag indicating if the bidder name will be added to the hb_pb_cat_dur. Default is false. + AppendBidderNames bool `json:"appendbiddernames,omitempty"` } type PodConfig struct { diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index 177e907ed71..08df7c59c80 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -101,6 +101,7 @@ type ExtRequestTargeting struct { IncludeBrandCategory *ExtIncludeBrandCategory `json:"includebrandcategory"` IncludeFormat bool `json:"includeformat"` DurationRangeSec []int `json:"durationrangesec"` + AppendBidderNames bool `json:"appendbiddernames,omitempty"` } type ExtIncludeBrandCategory struct {