From 28aa6b2e7456e6b19972849e80d2b79fd225d3cb Mon Sep 17 00:00:00 2001 From: Cameron Rice <37162584+camrice@users.noreply.github.com> Date: Mon, 11 Nov 2019 07:54:26 -0800 Subject: [PATCH] Adding translatecategories flag to includebrandcategory (#1098) * Making IAB category translation optional with translatecategories boolean in request * Updating exchange unit tests to remove extra bids * Updates from code review comments * Removed comment about default TranslateCategories value * Changed translateCat to translateCategories in tests * Combined helper functions in exchange_test related to TranslateCategories --- endpoints/openrtb2/video_auction.go | 7 +- endpoints/openrtb2/video_auction_test.go | 12 +- exchange/exchange.go | 37 ++++--- exchange/exchange_test.go | 133 ++++++++++++++++++++++- openrtb_ext/bid_request_video.go | 8 ++ openrtb_ext/request.go | 7 +- 6 files changed, 180 insertions(+), 24 deletions(-) diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 237663eb8bc..61f4f3e739f 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -508,9 +508,10 @@ func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, erro var inclBrandCat *openrtb_ext.ExtIncludeBrandCategory if videoRequest.IncludeBrandCategory != nil { inclBrandCat = &openrtb_ext.ExtIncludeBrandCategory{ - PrimaryAdServer: videoRequest.IncludeBrandCategory.PrimaryAdserver, - Publisher: videoRequest.IncludeBrandCategory.Publisher, - WithCategory: true, + PrimaryAdServer: videoRequest.IncludeBrandCategory.PrimaryAdserver, + Publisher: videoRequest.IncludeBrandCategory.Publisher, + WithCategory: true, + TranslateCategories: videoRequest.IncludeBrandCategory.TranslateCategories, } } else { inclBrandCat = &openrtb_ext.ExtIncludeBrandCategory{ diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 1caba27c33a..85285a3032a 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -106,10 +106,12 @@ func TestCreateBidExtension(t *testing.T) { Increment: 0.1, }) + translateCategories := true videoRequest := openrtb_ext.BidRequestVideo{ IncludeBrandCategory: &openrtb_ext.IncludeBrandCategory{ - PrimaryAdserver: 1, - Publisher: "", + PrimaryAdserver: 1, + Publisher: "", + TranslateCategories: &translateCategories, }, PodConfig: openrtb_ext.PodConfig{ DurationRangeSec: durationRange, @@ -138,10 +140,12 @@ func TestCreateBidExtensionExactDurTrueNoPriceRange(t *testing.T) { durationRange = append(durationRange, 15) durationRange = append(durationRange, 30) + translateCategories := false videoRequest := openrtb_ext.BidRequestVideo{ IncludeBrandCategory: &openrtb_ext.IncludeBrandCategory{ - PrimaryAdserver: 1, - Publisher: "", + PrimaryAdserver: 1, + Publisher: "", + TranslateCategories: &translateCategories, }, PodConfig: openrtb_ext.PodConfig{ DurationRangeSec: durationRange, diff --git a/exchange/exchange.go b/exchange/exchange.go index 16e9e199f08..ec6fdd80d59 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -355,14 +355,21 @@ func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest var primaryAdServer string var publisher string var err error + var translateCategories = true if includeBrandCategory && brandCatExt.WithCategory { - //if ext.prebid.targeting.includebrandcategory present but primaryadserver/publisher not present then error out the request right away. - primaryAdServer, err = getPrimaryAdServer(brandCatExt.PrimaryAdServer) //1-Freewheel 2-DFP - if err != nil { - return res, seatBids, err + if brandCatExt.TranslateCategories != nil { + translateCategories = *brandCatExt.TranslateCategories + } + //if translateCategories is set to false, ignore checking primaryAdServer and publisher + if translateCategories { + //if ext.prebid.targeting.includebrandcategory present but primaryadserver/publisher not present then error out the request right away. + primaryAdServer, err = getPrimaryAdServer(brandCatExt.PrimaryAdServer) //1-Freewheel 2-DFP + if err != nil { + return res, seatBids, err + } + publisher = brandCatExt.Publisher } - publisher = brandCatExt.Publisher } seatBidsToRemove := make([]openrtb_ext.BidderName, 0) @@ -387,15 +394,19 @@ func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest bidsToRemove = append(bidsToRemove, bidInd) continue } - //if unique IAB category is present then translate it to the adserver category based on mapping file - category, err = categoriesFetcher.FetchCategories(ctx, primaryAdServer, publisher, bidIabCat[0]) - if err != nil || category == "" { - //TODO: add metrics - //if mapping required but no mapping file is found then discard the bid - bidsToRemove = append(bidsToRemove, bidInd) - continue + if translateCategories { + //if unique IAB category is present then translate it to the adserver category based on mapping file + category, err = categoriesFetcher.FetchCategories(ctx, primaryAdServer, publisher, bidIabCat[0]) + if err != nil || category == "" { + //TODO: add metrics + //if mapping required but no mapping file is found then discard the bid + bidsToRemove = append(bidsToRemove, bidInd) + continue + } + } else { + //category translation is disabled, continue with IAB category + category = bidIabCat[0] } - } // TODO: consider should we remove bids with zero duration here? diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index b8d789ea35e..39497d61b14 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -526,7 +526,8 @@ func newExtRequest() openrtb_ext.ExtRequest { }, } - brandCat := openrtb_ext.ExtIncludeBrandCategory{PrimaryAdServer: 1, WithCategory: true} + translateCategories := true + brandCat := openrtb_ext.ExtIncludeBrandCategory{PrimaryAdServer: 1, WithCategory: true, TranslateCategories: &translateCategories} reqExt := openrtb_ext.ExtRequestTargeting{ PriceGranularity: priceGran, @@ -676,6 +677,136 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { assert.Equal(t, 4, len(bidCategory), "Bidders category mapping doesn't match") } +func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + requestExt := newExtRequestTranslateCategories(nil) + + targData := &targetData{ + priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, + includeWinners: true, + } + + requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50} + + adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + + cats1 := []string{"IAB1-3"} + cats2 := []string{"IAB1-4"} + cats3 := []string{"IAB1-1000"} + 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: 20.0000, Cat: cats2, W: 1, H: 1} + bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} + + bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} + bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}} + bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} + + innerBids := []*pbsOrtbBid{ + &bid1_1, + &bid1_2, + &bid1_3, + } + + seatBid := pbsOrtbSeatBid{innerBids, "USD", nil, nil} + bidderName1 := openrtb_ext.BidderName("appnexus") + + adapterBids[bidderName1] = &seatBid + + bidCategory, adapterBids, err := applyCategoryMapping(nil, requestExt, adapterBids, categoriesFetcher, targData) + + assert.Equal(t, nil, err, "Category mapping error should be empty") + assert.Equal(t, "10.00_Electronics_30s", bidCategory["bid_id1"], "Category mapping doesn't match") + assert.Equal(t, "20.00_Sports_50s", bidCategory["bid_id2"], "Category mapping doesn't match") + assert.Equal(t, 2, len(adapterBids[bidderName1].bids), "Bidders number doesn't match") + assert.Equal(t, 2, len(bidCategory), "Bidders category mapping doesn't match") +} + +func newExtRequestTranslateCategories(translateCategories *bool) openrtb_ext.ExtRequest { + priceGran := openrtb_ext.PriceGranularity{ + Precision: 2, + Ranges: []openrtb_ext.GranularityRange{ + { + Min: 0.0, + Max: 20.0, + Increment: 2.0, + }, + }, + } + + brandCat := openrtb_ext.ExtIncludeBrandCategory{WithCategory: true, PrimaryAdServer: 1} + if translateCategories != nil { + brandCat.TranslateCategories = translateCategories + } + + reqExt := openrtb_ext.ExtRequestTargeting{ + PriceGranularity: priceGran, + IncludeWinners: true, + IncludeBrandCategory: &brandCat, + } + + return openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Targeting: &reqExt, + }, + } +} + +func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + translateCategories := false + requestExt := newExtRequestTranslateCategories(&translateCategories) + + targData := &targetData{ + priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, + includeWinners: true, + } + + requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50} + + adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + + cats1 := []string{"IAB1-3"} + cats2 := []string{"IAB1-4"} + cats3 := []string{"IAB1-1000"} + 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: 20.0000, Cat: cats2, W: 1, H: 1} + bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} + + bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} + bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}} + bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} + + innerBids := []*pbsOrtbBid{ + &bid1_1, + &bid1_2, + &bid1_3, + } + + seatBid := pbsOrtbSeatBid{innerBids, "USD", nil, nil} + bidderName1 := openrtb_ext.BidderName("appnexus") + + adapterBids[bidderName1] = &seatBid + + bidCategory, adapterBids, err := applyCategoryMapping(nil, requestExt, adapterBids, categoriesFetcher, targData) + + assert.Equal(t, nil, err, "Category mapping error should be empty") + assert.Equal(t, "10.00_IAB1-3_30s", bidCategory["bid_id1"], "Category should not be translated") + assert.Equal(t, "20.00_IAB1-4_50s", bidCategory["bid_id2"], "Category should not be translated") + assert.Equal(t, "20.00_IAB1-1000_30s", bidCategory["bid_id3"], "Bid should not be rejected") + assert.Equal(t, 3, len(adapterBids[bidderName1].bids), "Bidders number doesn't match") + assert.Equal(t, 3, len(bidCategory), "Bidders category mapping doesn't match") +} + func TestCategoryDedupe(t *testing.T) { categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") diff --git a/openrtb_ext/bid_request_video.go b/openrtb_ext/bid_request_video.go index 3ca6508b9a1..43b780fcacb 100644 --- a/openrtb_ext/bid_request_video.go +++ b/openrtb_ext/bid_request_video.go @@ -191,6 +191,14 @@ type IncludeBrandCategory struct { // string; optional // Identifier for the Publisher Publisher string `json:"publisher"` + + // Attribute: + // translatecategories + // Type: + // *bool; optional + // Description: + // Indicates if IAB categories should be translated to adserver category + TranslateCategories *bool `json:"translatecategories,omitempty"` } type Cacheconfig struct { diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index 43f69a21c51..9226ff294d5 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -57,9 +57,10 @@ type ExtRequestTargeting struct { } type ExtIncludeBrandCategory struct { - PrimaryAdServer int `json:"primaryadserver"` - Publisher string `json:"publisher"` - WithCategory bool `json:"withcategory"` + PrimaryAdServer int `json:"primaryadserver"` + Publisher string `json:"publisher"` + WithCategory bool `json:"withcategory"` + TranslateCategories *bool `json:"translatecategories,omitempty"` } // Make an unmarshaller that will set a default PriceGranularity