From c8c00120c24efb4a6ab1a5dfd0f323a8c0b47592 Mon Sep 17 00:00:00 2001 From: Viral Vala <63396712+pm-viral-vala@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:34:05 +0530 Subject: [PATCH] OTT-1807-P1: discard emptyVAST and invalidVAST bids detected by VAST unwrap module (#933) Co-authored-by: ashishshinde-pubm <109787960+ashishshinde-pubm@users.noreply.github.com> Co-authored-by: supriya-patil Co-authored-by: dhruv.sonone --- analytics/pubmatic/logger.go | 32 + analytics/pubmatic/logger_test.go | 713 ++++++++--- go.mod | 3 +- go.sum | 4 +- hooks/hookexecution/executor.go | 4 +- hooks/hookexecution/mocks_test.go | 4 +- hooks/hookstage/rawbidderresponse.go | 4 +- .../hookstage/rawbidderresponse_mutations.go | 2 +- .../ortb2blocking/hook_raw_bidder_response.go | 4 +- modules/prebid/ortb2blocking/module_test.go | 308 +++-- modules/pubmatic/openwrap/config/config.go | 1 - .../openwrap/hook_raw_bidder_response.go | 92 +- .../openwrap/hook_raw_bidder_response_test.go | 1058 ++++++++++++++--- modules/pubmatic/openwrap/models/constants.go | 32 +- modules/pubmatic/openwrap/models/nbr/codes.go | 1 + modules/pubmatic/openwrap/unwrap/unwrap.go | 19 +- .../pubmatic/openwrap/unwrap/unwrap_test.go | 94 +- 17 files changed, 1794 insertions(+), 581 deletions(-) diff --git a/analytics/pubmatic/logger.go b/analytics/pubmatic/logger.go index 597c4320801..75364070d55 100644 --- a/analytics/pubmatic/logger.go +++ b/analytics/pubmatic/logger.go @@ -383,6 +383,9 @@ func getPartnerRecordsByImp(ao analytics.AuctionObject, rCtx *models.RequestCtx) } price := bid.Price + // If bids are rejected before setting bidExt.OriginalBidCPM, calculate the price and ocpm values based on the currency and revshare. + price = computeBidPriceForBidsRejectedBeforeSettingOCPM(rCtx, &bidExt, price, revShare, ao) + bid.Price = price if ao.Response.Cur != models.USD { if bidCtx.EN != 0 { // valid-bids + dropped-bids+ default-bids price = bidCtx.EN @@ -564,3 +567,32 @@ func getAdPodSlot(adPodConfig *models.AdPod) *AdPodSlot { return &adPodSlot } + +func GetBidPriceAfterCurrencyConversion(price float64, requestCurrencies []string, responseCurrency string, + currencyConverter func(fromCurrency string, toCurrency string, value float64) (float64, error)) float64 { + if len(requestCurrencies) == 0 { + requestCurrencies = []string{models.USD} + } + for _, requestCurrency := range requestCurrencies { + if value, err := currencyConverter(responseCurrency, requestCurrency, price); err == nil { + return value + } + } + return 0 // in case of error, send 0 value to make it consistent with prebid +} + +func computeBidPriceForBidsRejectedBeforeSettingOCPM(rCtx *models.RequestCtx, bidExt *models.BidExt, + price, revshare float64, ao analytics.AuctionObject) float64 { + if price != 0 && bidExt.OriginalBidCPM == 0 { + if len(bidExt.OriginalBidCur) == 0 { + bidExt.OriginalBidCur = models.USD + } + bidExt.OriginalBidCPM = price + price = price * models.GetBidAdjustmentValue(revshare) + if cpmUSD, err := rCtx.CurrencyConversion(bidExt.OriginalBidCur, models.USD, price); err == nil { + bidExt.OriginalBidCPMUSD = cpmUSD + } + price = GetBidPriceAfterCurrencyConversion(price, ao.RequestWrapper.Cur, bidExt.OriginalBidCur, rCtx.CurrencyConversion) + } + return price +} diff --git a/analytics/pubmatic/logger_test.go b/analytics/pubmatic/logger_test.go index b01f1f7b501..41ef74a283f 100644 --- a/analytics/pubmatic/logger_test.go +++ b/analytics/pubmatic/logger_test.go @@ -2,6 +2,7 @@ package pubmatic import ( "encoding/json" + "fmt" "net/http" "net/url" "testing" @@ -294,7 +295,8 @@ func TestGetPartnerRecordsByImp(t *testing.T) { BidCtx: map[string]models.BidCtx{ "bid-id-1": { BidExt: models.BidExt{ - ExtBid: openrtb_ext.ExtBid{}, + ExtBid: openrtb_ext.ExtBid{}, + OriginalBidCPM: 10, }, }, }, @@ -347,7 +349,8 @@ func TestGetPartnerRecordsByImp(t *testing.T) { BidCtx: map[string]models.BidCtx{ "bid-id-1": { BidExt: models.BidExt{ - ExtBid: openrtb_ext.ExtBid{}, + ExtBid: openrtb_ext.ExtBid{}, + OriginalBidCPM: 10, }, }, }, @@ -403,7 +406,8 @@ func TestGetPartnerRecordsByImp(t *testing.T) { BidCtx: map[string]models.BidCtx{ "bid-id-1": { BidExt: models.BidExt{ - ExtBid: openrtb_ext.ExtBid{}, + ExtBid: openrtb_ext.ExtBid{}, + OriginalBidCPM: 10, }, }, }, @@ -459,7 +463,8 @@ func TestGetPartnerRecordsByImp(t *testing.T) { BidCtx: map[string]models.BidCtx{ "bid-id-1": { BidExt: models.BidExt{ - ExtBid: openrtb_ext.ExtBid{}, + ExtBid: openrtb_ext.ExtBid{}, + OriginalBidCPM: 10, }, }, }, @@ -512,7 +517,8 @@ func TestGetPartnerRecordsByImp(t *testing.T) { BidCtx: map[string]models.BidCtx{ "bid-id-1": { BidExt: models.BidExt{ - ExtBid: openrtb_ext.ExtBid{}, + ExtBid: openrtb_ext.ExtBid{}, + OriginalBidCPM: 10, }, }, }, @@ -564,7 +570,8 @@ func TestGetPartnerRecordsByImp(t *testing.T) { BidCtx: map[string]models.BidCtx{ "bid-id-1": { BidExt: models.BidExt{ - ExtBid: openrtb_ext.ExtBid{}, + ExtBid: openrtb_ext.ExtBid{}, + OriginalBidCPM: 10, }, }, }, @@ -709,7 +716,8 @@ func TestGetPartnerRecordsByImpForTracker(t *testing.T) { BidCtx: map[string]models.BidCtx{ "bid-id-1": { BidExt: models.BidExt{ - ExtBid: openrtb_ext.ExtBid{}, + OriginalBidCPM: 10, + ExtBid: openrtb_ext.ExtBid{}, }, }, }, @@ -771,6 +779,7 @@ func TestGetPartnerRecordsByImpForTracker(t *testing.T) { BidCtx: map[string]models.BidCtx{ "bid-id-1": { BidExt: models.BidExt{ + OriginalBidCPM: 12, ExtBid: openrtb_ext.ExtBid{ Prebid: &openrtb_ext.ExtBidPrebid{ Floors: &openrtb_ext.ExtBidPrebidFloors{ @@ -1015,7 +1024,8 @@ func TestGetPartnerRecordsByImpForDefaultBids(t *testing.T) { BidCtx: map[string]models.BidCtx{ "bid-id-1": { BidExt: models.BidExt{ - ExtBid: openrtb_ext.ExtBid{}, + ExtBid: openrtb_ext.ExtBid{}, + OriginalBidCPM: 10, }, }, }, @@ -1287,6 +1297,7 @@ func TestGetPartnerRecordsByImpForDefaultBids(t *testing.T) { } } func TestGetPartnerRecordsByImpForSeatNonBid(t *testing.T) { + pg, _ := openrtb_ext.NewPriceGranularityFromLegacyID("med") type args struct { ao analytics.AuctionObject rCtx *models.RequestCtx @@ -1420,6 +1431,301 @@ func TestGetPartnerRecordsByImpForSeatNonBid(t *testing.T) { }, }, }, + { + name: "log rejected non-bid having bidder_response_currency EUR and request_currency USD", + args: args{ + ao: analytics.AuctionObject{ + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Cur: []string{models.USD}, + }, + }, + Response: &openrtb2.BidResponse{Cur: models.USD}, + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "appnexus", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: int(nbr.LossBidLostInVastUnwrap), + Ext: openrtb_ext.ExtNonBid{ + Prebid: openrtb_ext.ExtNonBidPrebid{ + Bid: openrtb_ext.ExtNonBidPrebidBid{ + Price: 10, + ID: "bid-id-1", + W: 10, + H: 50, + OriginalBidCur: "EUR", + }, + }, + }, + }, + }, + }, + }, + }, + rCtx: &models.RequestCtx{ + PriceGranularity: &pg, + CurrencyConversion: func(from, to string, value float64) (float64, error) { + if from == "USD" && to == "EUR" { + return value * 1.2, nil + } + if from == "EUR" && to == "USD" { + return value * 0.8, nil + } + return 0, nil + }, + ImpBidCtx: map[string]models.ImpCtx{ + "imp1": { + Bidders: map[string]models.PartnerData{ + "appnexus": { + PartnerID: 1, + PrebidBidderCode: "appnexus", + KGP: "kgp", + KGPV: "kgpv", + }, + }, + BidCtx: map[string]models.BidCtx{ + "bid-id-1": { + BidExt: models.BidExt{ + ExtBid: openrtb_ext.ExtBid{}, + Nbr: ptrutil.ToPtr(nbr.LossBidLostInVastUnwrap), + }, + }, + }, + BidFloor: 10.5, + BidFloorCur: "USD", + }, + }, + PartnerConfigMap: map[int]map[string]string{ + 1: { + "rev_share": "0", + }, + }, + WinningBids: make(models.WinningBids), + Platform: models.PLATFORM_APP, + }, + }, + partners: map[string][]PartnerRecord{ + "imp1": { + { + PartnerID: "appnexus", + BidderCode: "appnexus", + KGPV: "kgpv", + KGPSV: "kgpv", + PartnerSize: "10x50", + GrossECPM: 8, + NetECPM: 8, + BidID: "bid-id-1", + OrigBidID: "bid-id-1", + DealID: "-1", + ServerSide: 1, + OriginalCPM: 10, + OriginalCur: "EUR", + FloorValue: 10.5, + FloorRuleValue: 10.5, + PriceBucket: "8.00", + Nbr: ptrutil.ToPtr(nbr.LossBidLostInVastUnwrap), + }, + }, + }, + }, + { + name: "log rejected non-bid having bidder_response_currency EUR and request_currency USD and having 50% revshare", + args: args{ + ao: analytics.AuctionObject{ + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Cur: []string{models.USD}, + }, + }, + Response: &openrtb2.BidResponse{Cur: models.USD}, + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "appnexus", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: int(nbr.LossBidLostInVastUnwrap), + Ext: openrtb_ext.ExtNonBid{ + Prebid: openrtb_ext.ExtNonBidPrebid{ + Bid: openrtb_ext.ExtNonBidPrebidBid{ + Price: 10, + ID: "bid-id-1", + W: 10, + H: 50, + OriginalBidCur: "EUR", + }, + }, + }, + }, + }, + }, + }, + }, + rCtx: &models.RequestCtx{ + PriceGranularity: &pg, + CurrencyConversion: func(from, to string, value float64) (float64, error) { + if from == "USD" && to == "EUR" { + return value * 1.2, nil + } + if from == "EUR" && to == "USD" { + return value * 0.8, nil + } + return 0, nil + }, + ImpBidCtx: map[string]models.ImpCtx{ + "imp1": { + Bidders: map[string]models.PartnerData{ + "appnexus": { + PartnerID: 1, + PrebidBidderCode: "appnexus", + KGP: "kgp", + KGPV: "kgpv", + }, + }, + BidCtx: map[string]models.BidCtx{ + "bid-id-1": { + BidExt: models.BidExt{ + ExtBid: openrtb_ext.ExtBid{}, + Nbr: ptrutil.ToPtr(nbr.LossBidLostInVastUnwrap), + }, + }, + }, + BidFloor: 10.5, + BidFloorCur: "USD", + }, + }, + PartnerConfigMap: map[int]map[string]string{ + 1: { + "rev_share": "50", + }, + }, + WinningBids: make(models.WinningBids), + Platform: models.PLATFORM_APP, + }, + }, + partners: map[string][]PartnerRecord{ + "imp1": { + { + PartnerID: "appnexus", + BidderCode: "appnexus", + KGPV: "kgpv", + KGPSV: "kgpv", + PartnerSize: "10x50", + GrossECPM: 8, + NetECPM: 4, + PriceBucket: "4.00", + BidID: "bid-id-1", + OrigBidID: "bid-id-1", + DealID: "-1", + ServerSide: 1, + OriginalCPM: 10, + OriginalCur: "EUR", + FloorValue: 10.5, + FloorRuleValue: 10.5, + Nbr: ptrutil.ToPtr(nbr.LossBidLostInVastUnwrap), + }, + }, + }, + }, + { + name: "log rejected non-bid having response_currency USD and request_currency EUR and having 50% revshare", + args: args{ + ao: analytics.AuctionObject{ + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Cur: []string{"EUR"}, + }, + }, + Response: &openrtb2.BidResponse{Cur: "EUR"}, + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "appnexus", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: int(nbr.LossBidLostInVastUnwrap), + Ext: openrtb_ext.ExtNonBid{ + Prebid: openrtb_ext.ExtNonBidPrebid{ + Bid: openrtb_ext.ExtNonBidPrebidBid{ + Price: 10, + ID: "bid-id-1", + W: 10, + H: 50, + OriginalBidCur: models.USD, + }, + }, + }, + }, + }, + }, + }, + }, + rCtx: &models.RequestCtx{ + CurrencyConversion: func(from, to string, value float64) (float64, error) { + if from == "USD" && to == "EUR" { + return value * 1.2, nil + } + if from == "EUR" && to == "USD" { + return value * 0.8, nil + } + return value, nil + }, + ImpBidCtx: map[string]models.ImpCtx{ + "imp1": { + Bidders: map[string]models.PartnerData{ + "appnexus": { + PartnerID: 1, + PrebidBidderCode: "appnexus", + KGP: "kgp", + KGPV: "kgpv", + }, + }, + BidCtx: map[string]models.BidCtx{ + "bid-id-1": { + BidExt: models.BidExt{ + ExtBid: openrtb_ext.ExtBid{}, + Nbr: ptrutil.ToPtr(nbr.LossBidLostInVastUnwrap), + }, + }, + }, + BidFloor: 10.5, + BidFloorCur: "USD", + }, + }, + PartnerConfigMap: map[int]map[string]string{ + 1: { + "rev_share": "50", + }, + }, + WinningBids: make(models.WinningBids), + Platform: models.PLATFORM_APP, + }, + }, + partners: map[string][]PartnerRecord{ + "imp1": { + { + PartnerID: "appnexus", + BidderCode: "appnexus", + KGPV: "kgpv", + KGPSV: "kgpv", + PartnerSize: "10x50", + GrossECPM: 10, + NetECPM: 5, + BidID: "bid-id-1", + OrigBidID: "bid-id-1", + DealID: "-1", + ServerSide: 1, + OriginalCPM: 10, + OriginalCur: "USD", + FloorValue: 10.5, + FloorRuleValue: 10.5, + Nbr: ptrutil.ToPtr(nbr.LossBidLostInVastUnwrap), + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1461,6 +1767,7 @@ func TestGetPartnerRecordsByImpForSeatNonBidForFloors(t *testing.T) { FloorValue: 1, FloorCurrency: models.USD, }, + OriginalBidCPM: 10, }, }, }, @@ -1527,6 +1834,7 @@ func TestGetPartnerRecordsByImpForSeatNonBidForFloors(t *testing.T) { FloorValue: 0, FloorCurrency: models.USD, }, + OriginalBidCPM: 10, }, }, }, @@ -1593,6 +1901,7 @@ func TestGetPartnerRecordsByImpForSeatNonBidForFloors(t *testing.T) { FloorValue: 10, FloorCurrency: models.USD, }, + OriginalBidCPM: 10, }, }, }, @@ -1651,8 +1960,9 @@ func TestGetPartnerRecordsByImpForSeatNonBidForFloors(t *testing.T) { Ext: openrtb_ext.ExtNonBid{ Prebid: openrtb_ext.ExtNonBidPrebid{ Bid: openrtb_ext.ExtNonBidPrebidBid{ - Price: 10, - ID: "bid-id-1", + Price: 10, + ID: "bid-id-1", + OriginalBidCPM: 10, }, }, }, @@ -1711,8 +2021,9 @@ func TestGetPartnerRecordsByImpForSeatNonBidForFloors(t *testing.T) { Ext: openrtb_ext.ExtNonBid{ Prebid: openrtb_ext.ExtNonBidPrebid{ Bid: openrtb_ext.ExtNonBidPrebidBid{ - Price: 10, - ID: "bid-id-1", + Price: 10, + ID: "bid-id-1", + OriginalBidCPM: 10, }, }, }, @@ -1782,6 +2093,7 @@ func TestGetPartnerRecordsByImpForSeatNonBidForFloors(t *testing.T) { FloorValue: 1, FloorCurrency: "JPY", }, + OriginalBidCPM: 10, }, }, }, @@ -1988,6 +2300,7 @@ func TestGetPartnerRecordsByImpForBidIDCollisions(t *testing.T) { BidId: "uuid", }, }, + OriginalBidCPM: 10, }, }, }, @@ -2017,6 +2330,11 @@ func TestGetPartnerRecordsByImpForBidIDCollisions(t *testing.T) { name: "valid bid, but json unmarshal fails", args: args{ ao: analytics.AuctionObject{ + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Cur: []string{}, + }, + }, Response: &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{ { @@ -2046,11 +2364,13 @@ func TestGetPartnerRecordsByImpForBidIDCollisions(t *testing.T) { BidId: "uuid", }, }, + OriginalBidCPM: 10, }, }, }, }, }, + CurrencyConversion: func(from, to string, value float64) (float64, error) { return 10, nil }, }, }, partners: map[string][]PartnerRecord{ @@ -2066,6 +2386,7 @@ func TestGetPartnerRecordsByImpForBidIDCollisions(t *testing.T) { OriginalCur: models.USD, NetECPM: 10, GrossECPM: 10, + OriginalCPM: 10, DealPriority: 0, }, }, @@ -2103,7 +2424,8 @@ func TestGetPartnerRecordsByImpForBidIDCollisions(t *testing.T) { DealTierSatisfied: true, }, }, - Nbr: nbr.LossBidLostToHigherBid.Ptr(), + OriginalBidCPM: 10, + Nbr: nbr.LossBidLostToHigherBid.Ptr(), }, }, }, @@ -2195,9 +2517,10 @@ func TestGetPartnerRecordsByImpForBidIDCollisions(t *testing.T) { Ext: openrtb_ext.ExtNonBid{ Prebid: openrtb_ext.ExtNonBidPrebid{ Bid: openrtb_ext.ExtNonBidPrebidBid{ - Price: 10, - ID: "bid-id-1", - BidId: "uuid", + Price: 10, + ID: "bid-id-1", + BidId: "uuid", + OriginalBidCPM: 10, }, }, }, @@ -2263,6 +2586,7 @@ func TestGetPartnerRecordsByImpForBidIDCollisions(t *testing.T) { BidId: "uuid", }, }, + OriginalBidCPM: 10, }, }, }, @@ -2318,6 +2642,9 @@ func TestGetPartnerRecordsByImpForBidExtFailure(t *testing.T) { name: "valid bid, but bid.ext is empty", args: args{ ao: analytics.AuctionObject{ + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, Response: &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{ { @@ -2352,6 +2679,7 @@ func TestGetPartnerRecordsByImpForBidExtFailure(t *testing.T) { }, }, }, + CurrencyConversion: func(from, to string, value float64) (float64, error) { return 10, nil }, }, }, partners: map[string][]PartnerRecord{ @@ -2367,6 +2695,7 @@ func TestGetPartnerRecordsByImpForBidExtFailure(t *testing.T) { OriginalCur: models.USD, NetECPM: 10, GrossECPM: 10, + OriginalCPM: 10, DealPriority: 0, }, }, @@ -2376,6 +2705,9 @@ func TestGetPartnerRecordsByImpForBidExtFailure(t *testing.T) { name: "dropped bid, bidExt unmarshal fails", args: args{ ao: analytics.AuctionObject{ + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, Response: &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{ { @@ -2410,6 +2742,7 @@ func TestGetPartnerRecordsByImpForBidExtFailure(t *testing.T) { }, }, }, + CurrencyConversion: func(from, to string, value float64) (float64, error) { return 10, nil }, }, }, partners: map[string][]PartnerRecord{ @@ -2425,6 +2758,7 @@ func TestGetPartnerRecordsByImpForBidExtFailure(t *testing.T) { OriginalCur: models.USD, NetECPM: 10, GrossECPM: 10, + OriginalCPM: 10, DealPriority: 0, Nbr: nil, }, @@ -3246,6 +3580,9 @@ func TestGetPartnerRecordsByImpForMarketPlaceBidders(t *testing.T) { name: "overwrite marketplace bid details", args: args{ ao: analytics.AuctionObject{ + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, Response: &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{ { @@ -3294,6 +3631,7 @@ func TestGetPartnerRecordsByImpForMarketPlaceBidders(t *testing.T) { }, }, }, + CurrencyConversion: func(from, to string, value float64) (float64, error) { return value, nil }, }, }, partners: map[string][]PartnerRecord{ @@ -3309,6 +3647,7 @@ func TestGetPartnerRecordsByImpForMarketPlaceBidders(t *testing.T) { OriginalCur: models.USD, GrossECPM: 1, NetECPM: 1, + OriginalCPM: 1, KGPV: "apnx_kgpv", KGPSV: "apnx_kgpv", }, @@ -3323,6 +3662,7 @@ func TestGetPartnerRecordsByImpForMarketPlaceBidders(t *testing.T) { OriginalCur: models.USD, GrossECPM: 2, NetECPM: 2, + OriginalCPM: 2, KGPV: "pubm_kgpv", KGPSV: "pubm_kgpv", }, @@ -3337,6 +3677,7 @@ func TestGetPartnerRecordsByImpForMarketPlaceBidders(t *testing.T) { OriginalCur: models.USD, GrossECPM: 3, NetECPM: 3, + OriginalCPM: 3, KGPV: "pubm_kgpv", KGPSV: "pubm_kgpv", }, @@ -4600,129 +4941,129 @@ func TestSlotRecordsInGetLogAuctionObjectAsURL(t *testing.T) { args args want want }{ - // { - // name: "req.Imp not mapped in ImpBidCtx", - // args: args{ - // ao: analytics.AuctionObject{ - // RequestWrapper: &openrtb_ext.RequestWrapper{ - // BidRequest: &openrtb2.BidRequest{ - // Imp: []openrtb2.Imp{ - // { - // ID: "imp1", - // TagID: "tagid", - // }, - // }, - // }, - // }, - // Response: &openrtb2.BidResponse{}, - // }, - // rCtx: &models.RequestCtx{ - // Endpoint: models.EndpointV25, - // PubID: 5890, - // }, - // logInfo: false, - // forRespExt: true, - // }, - // want: want{ - // logger: ow.cfg.Endpoint + `?json={"pubid":5890,"pid":"0","pdvid":"0","sl":1,"dvc":{},"ft":0,"it":"sdk"}&pubid=5890`, - // header: http.Header{ - // models.USER_AGENT_HEADER: []string{""}, - // models.IP_HEADER: []string{""}, - // }, - // }, - // }, - // { - // name: "multi imps request", - // args: args{ - // ao: analytics.AuctionObject{ - // RequestWrapper: &openrtb_ext.RequestWrapper{ - // BidRequest: &openrtb2.BidRequest{ - // Imp: []openrtb2.Imp{ - // { - // ID: "imp_1", - // TagID: "tagid_1", - // }, - // { - // ID: "imp_2", - // TagID: "tagid_2", - // }, - // }, - // }, - // }, - // Response: &openrtb2.BidResponse{}, - // }, - // rCtx: &models.RequestCtx{ - // PubID: 5890, - // Endpoint: models.EndpointV25, - // ImpBidCtx: map[string]models.ImpCtx{ - // "imp_1": { - // SlotName: "imp_1_tagid_1", - // AdUnitName: "tagid_1", - // }, - // "imp_2": { - // AdUnitName: "tagid_2", - // SlotName: "imp_2_tagid_2", - // }, - // }, - // }, - // logInfo: false, - // forRespExt: true, - // }, - // want: want{ - // logger: ow.cfg.Endpoint + `?json={"pubid":5890,"pid":"0","pdvid":"0","sl":1,"s":[{"sid":"sid","sn":"imp_1_tagid_1","au":"tagid_1","ps":[]},{"sid":"sid","sn":"imp_2_tagid_2","au":"tagid_2","ps":[]}],"dvc":{},"ft":0,"it":"sdk"}&pubid=5890`, - // header: http.Header{ - // models.USER_AGENT_HEADER: []string{""}, - // models.IP_HEADER: []string{""}, - // }, - // }, - // }, - // { - // name: "multi imps request and one request has incomingslots", - // args: args{ - // ao: analytics.AuctionObject{ - // RequestWrapper: &openrtb_ext.RequestWrapper{ - // BidRequest: &openrtb2.BidRequest{ - // Imp: []openrtb2.Imp{ - // { - // ID: "imp_1", - // TagID: "tagid_1", - // }, - // { - // ID: "imp_2", - // TagID: "tagid_2", - // }, - // }, - // }, - // }, - // Response: &openrtb2.BidResponse{}, - // }, - // rCtx: &models.RequestCtx{ - // PubID: 5890, - // Endpoint: models.EndpointV25, - // ImpBidCtx: map[string]models.ImpCtx{ - // "imp_1": { - // IncomingSlots: []string{"0x0v", "100x200"}, - // IsRewardInventory: ptrutil.ToPtr(int8(1)), - // SlotName: "imp_1_tagid_1", - // AdUnitName: "tagid_1", - // }, - // "imp_2": { - // AdUnitName: "tagid_2", - // SlotName: "imp_2_tagid_2", - // }, - // }, - // }, - // logInfo: false, - // forRespExt: true, - // }, - // want: want{ - // logger: ow.cfg.Endpoint + `?json={"pubid":5890,"pid":"0","pdvid":"0","sl":1,"s":[{"sid":"sid","sn":"imp_1_tagid_1","sz":["0x0v","100x200"],"au":"tagid_1","ps":[],"rwrd":1},{"sid":"sid","sn":"imp_2_tagid_2","au":"tagid_2","ps":[]}],"dvc":{},"ft":0,"it":"sdk"}&pubid=5890`, - // header: http.Header{ - // models.USER_AGENT_HEADER: []string{""}, - // models.IP_HEADER: []string{""}, - // }, - // }, - // }, + { + name: "req.Imp not mapped in ImpBidCtx", + args: args{ + ao: analytics.AuctionObject{ + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp1", + TagID: "tagid", + }, + }, + }, + }, + Response: &openrtb2.BidResponse{}, + }, + rCtx: &models.RequestCtx{ + Endpoint: models.EndpointV25, + PubID: 5890, + }, + logInfo: false, + forRespExt: true, + }, + want: want{ + logger: ow.cfg.Endpoint + `?json={"pubid":5890,"pid":"0","pdvid":"0","sl":1,"dvc":{},"ft":0,"it":"sdk"}&pubid=5890`, + header: http.Header{ + models.USER_AGENT_HEADER: []string{""}, + models.IP_HEADER: []string{""}, + }, + }, + }, + { + name: "multi imps request", + args: args{ + ao: analytics.AuctionObject{ + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp_1", + TagID: "tagid_1", + }, + { + ID: "imp_2", + TagID: "tagid_2", + }, + }, + }, + }, + Response: &openrtb2.BidResponse{}, + }, + rCtx: &models.RequestCtx{ + PubID: 5890, + Endpoint: models.EndpointV25, + ImpBidCtx: map[string]models.ImpCtx{ + "imp_1": { + SlotName: "imp_1_tagid_1", + AdUnitName: "tagid_1", + }, + "imp_2": { + AdUnitName: "tagid_2", + SlotName: "imp_2_tagid_2", + }, + }, + }, + logInfo: false, + forRespExt: true, + }, + want: want{ + logger: ow.cfg.Endpoint + `?json={"pubid":5890,"pid":"0","pdvid":"0","sl":1,"s":[{"sid":"sid","sn":"imp_1_tagid_1","au":"tagid_1","ps":[]},{"sid":"sid","sn":"imp_2_tagid_2","au":"tagid_2","ps":[]}],"dvc":{},"ft":0,"it":"sdk"}&pubid=5890`, + header: http.Header{ + models.USER_AGENT_HEADER: []string{""}, + models.IP_HEADER: []string{""}, + }, + }, + }, + { + name: "multi imps request and one request has incomingslots", + args: args{ + ao: analytics.AuctionObject{ + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{ + { + ID: "imp_1", + TagID: "tagid_1", + }, + { + ID: "imp_2", + TagID: "tagid_2", + }, + }, + }, + }, + Response: &openrtb2.BidResponse{}, + }, + rCtx: &models.RequestCtx{ + PubID: 5890, + Endpoint: models.EndpointV25, + ImpBidCtx: map[string]models.ImpCtx{ + "imp_1": { + IncomingSlots: []string{"0x0v", "100x200"}, + IsRewardInventory: ptrutil.ToPtr(int8(1)), + SlotName: "imp_1_tagid_1", + AdUnitName: "tagid_1", + }, + "imp_2": { + AdUnitName: "tagid_2", + SlotName: "imp_2_tagid_2", + }, + }, + }, + logInfo: false, + forRespExt: true, + }, + want: want{ + logger: ow.cfg.Endpoint + `?json={"pubid":5890,"pid":"0","pdvid":"0","sl":1,"s":[{"sid":"sid","sn":"imp_1_tagid_1","sz":["0x0v","100x200"],"au":"tagid_1","ps":[],"rwrd":1},{"sid":"sid","sn":"imp_2_tagid_2","au":"tagid_2","ps":[]}],"dvc":{},"ft":0,"it":"sdk"}&pubid=5890`, + header: http.Header{ + models.USER_AGENT_HEADER: []string{""}, + models.IP_HEADER: []string{""}, + }, + }, + }, { name: "multi imps request and one imp has partner record", args: args{ @@ -4943,3 +5284,97 @@ func Test_getFloorValueFromUpdatedRequest(t *testing.T) { }) } } + +func TestGetBidPriceAfterCurrencyConversion(t *testing.T) { + type args struct { + price float64 + requestCurrencies []string + responseCurrency string + currencyConverter func(fromCurrency string, toCurrency string, value float64) (float64, error) + } + tests := []struct { + name string + args args + want float64 + }{ + { + name: "Single request currency - successful conversion", + args: args{ + price: 100.0, + requestCurrencies: []string{"EUR"}, + responseCurrency: "USD", + currencyConverter: func(fromCurrency string, toCurrency string, value float64) (float64, error) { + if fromCurrency == "USD" && toCurrency == "EUR" { + return 85.0, nil // Assuming conversion rate USD to EUR is 0.85 + } + return 0, fmt.Errorf("unsupported conversion") + }, + }, + want: 85.0, + }, + { + name: "Multiple request currencies - first successful conversion", + args: args{ + price: 100.0, + requestCurrencies: []string{"EUR", "GBP"}, + responseCurrency: "USD", + currencyConverter: func(fromCurrency string, toCurrency string, value float64) (float64, error) { + if fromCurrency == "USD" && toCurrency == "EUR" { + return 85.0, nil // Successful conversion to EUR + } + return 0, fmt.Errorf("unsupported conversion") + }, + }, + want: 85.0, + }, + { + name: "Multiple request currencies - second successful conversion", + args: args{ + price: 100.0, + requestCurrencies: []string{"JPY", "GBP"}, + responseCurrency: "USD", + currencyConverter: func(fromCurrency string, toCurrency string, value float64) (float64, error) { + if fromCurrency == "USD" && toCurrency == "GBP" { + return 75.0, nil // Successful conversion to GBP + } + return 0, fmt.Errorf("unsupported conversion") + }, + }, + want: 75.0, + }, + { + name: "No request currencies provided - default to USD", + args: args{ + price: 100.0, + requestCurrencies: []string{}, + responseCurrency: "USD", + currencyConverter: func(fromCurrency string, toCurrency string, value float64) (float64, error) { + if fromCurrency == "USD" && toCurrency == "USD" { + return 100.0, nil // No conversion needed + } + return 0, fmt.Errorf("unsupported conversion") + }, + }, + want: 100.0, + }, + { + name: "Conversion fails for all currencies", + args: args{ + price: 100.0, + requestCurrencies: []string{"JPY", "CNY"}, + responseCurrency: "USD", + currencyConverter: func(fromCurrency string, toCurrency string, value float64) (float64, error) { + return 0, fmt.Errorf("conversion failed") + }, + }, + want: 0.0, // Default to 0 on failure + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetBidPriceAfterCurrencyConversion(tt.args.price, tt.args.requestCurrencies, tt.args.responseCurrency, tt.args.currencyConverter) + assert.Equal(t, tt.want, got, "mismatched price") + }) + } +} diff --git a/go.mod b/go.mod index 5badc4d5deb..fdf70090dc8 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/PubMatic-OpenWrap/prebid-server/v2 go 1.20 -replace git.pubmatic.com/vastunwrap => git.pubmatic.com/PubMatic/vastunwrap v0.0.0-20240319050712-0b288cbb5a5d +replace git.pubmatic.com/vastunwrap => git.pubmatic.com/PubMatic/vastunwrap v0.0.0-20240827084017-0e392d3beb8b require ( github.com/DATA-DOG/go-sqlmock v1.5.0 @@ -91,7 +91,6 @@ require ( github.com/yudai/pp v2.0.1+incompatible // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/sys v0.18.0 // indirect - github.com/yudai/pp v2.0.1+incompatible // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 6d50b0b288f..4451dc12c04 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,8 @@ git.pubmatic.com/PubMatic/go-common v0.0.0-20240313090142-97ff3d63b7c3 h1:Ea8zwi git.pubmatic.com/PubMatic/go-common v0.0.0-20240313090142-97ff3d63b7c3/go.mod h1:c/I6IcDn4Mtq4mmw8wGJN3v0o10nIMX7VTuQnsalUw0= git.pubmatic.com/PubMatic/go-netacuity-client v0.0.0-20240104092757-5d6f15e25fe3 h1:zQUpPJOjTBGu2fIydrfRWphH7EWLlBE/Qgn64BSoccI= git.pubmatic.com/PubMatic/go-netacuity-client v0.0.0-20240104092757-5d6f15e25fe3/go.mod h1:w733mqJnHt0hLR9mIFMzyDR0D94qzc7mFHsuE0tFQho= -git.pubmatic.com/PubMatic/vastunwrap v0.0.0-20240319050712-0b288cbb5a5d h1:BgLUpJQ9Z89eDGz//voK74G/8FgjgVg2PWVbjgCJ4+A= -git.pubmatic.com/PubMatic/vastunwrap v0.0.0-20240319050712-0b288cbb5a5d/go.mod h1:kcoJf7k+xug8X8fLWmsiKhPnYP+k7RZkfUoUo5QF+KA= +git.pubmatic.com/PubMatic/vastunwrap v0.0.0-20240827084017-0e392d3beb8b h1:7AsXylZJDwq514L8KE0Id079VNfUsDEMUIYMlRYH+0Y= +git.pubmatic.com/PubMatic/vastunwrap v0.0.0-20240827084017-0e392d3beb8b/go.mod h1:kcoJf7k+xug8X8fLWmsiKhPnYP+k7RZkfUoUo5QF+KA= 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/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= diff --git a/hooks/hookexecution/executor.go b/hooks/hookexecution/executor.go index 0ba466acc8d..2249fd29d42 100644 --- a/hooks/hookexecution/executor.go +++ b/hooks/hookexecution/executor.go @@ -260,10 +260,10 @@ func (e *hookExecutor) ExecuteRawBidderResponseStage(response *adapters.BidderRe stageName := hooks.StageRawBidderResponse.String() executionCtx := e.newContext(stageName) - payload := hookstage.RawBidderResponsePayload{Bids: response.Bids, Bidder: bidder} + payload := hookstage.RawBidderResponsePayload{BidderResponse: response, Bidder: bidder} outcome, payload, contexts, reject := executeStage(executionCtx, plan, payload, handler, e.metricEngine) - response.Bids = payload.Bids + response = payload.BidderResponse outcome.Entity = entity(bidder) outcome.Stage = stageName diff --git a/hooks/hookexecution/mocks_test.go b/hooks/hookexecution/mocks_test.go index a52fdbe12a7..968b1a7540b 100644 --- a/hooks/hookexecution/mocks_test.go +++ b/hooks/hookexecution/mocks_test.go @@ -154,7 +154,7 @@ func (e mockTimeoutHook) HandleRawBidderResponseHook(_ context.Context, _ hookst time.Sleep(20 * time.Millisecond) c := hookstage.ChangeSet[hookstage.RawBidderResponsePayload]{} c.AddMutation(func(payload hookstage.RawBidderResponsePayload) (hookstage.RawBidderResponsePayload, error) { - payload.Bids[0].BidMeta = &openrtb_ext.ExtBidPrebidMeta{AdapterCode: "new-code"} + payload.BidderResponse.Bids[0].BidMeta = &openrtb_ext.ExtBidPrebidMeta{AdapterCode: "new-code"} return payload, nil }, hookstage.MutationUpdate, "bidderResponse", "bidMeta.AdapterCode") @@ -371,7 +371,7 @@ func (e mockUpdateBidderResponseHook) HandleRawBidderResponseHook(_ context.Cont c := hookstage.ChangeSet[hookstage.RawBidderResponsePayload]{} c.AddMutation( func(payload hookstage.RawBidderResponsePayload) (hookstage.RawBidderResponsePayload, error) { - payload.Bids[0].DealPriority = 10 + payload.BidderResponse.Bids[0].DealPriority = 10 return payload, nil }, hookstage.MutationUpdate, "bidderResponse", "bid.deal-priority", ) diff --git a/hooks/hookstage/rawbidderresponse.go b/hooks/hookstage/rawbidderresponse.go index 7d08a7d2e02..1f6aaaa64ef 100644 --- a/hooks/hookstage/rawbidderresponse.go +++ b/hooks/hookstage/rawbidderresponse.go @@ -25,6 +25,6 @@ type RawBidderResponse interface { // objects representing bids returned by a particular bidder. // Hooks are allowed to modify bids using mutations. type RawBidderResponsePayload struct { - Bids []*adapters.TypedBid - Bidder string + BidderResponse *adapters.BidderResponse + Bidder string } diff --git a/hooks/hookstage/rawbidderresponse_mutations.go b/hooks/hookstage/rawbidderresponse_mutations.go index efab874fa15..c5022c31218 100644 --- a/hooks/hookstage/rawbidderresponse_mutations.go +++ b/hooks/hookstage/rawbidderresponse_mutations.go @@ -33,7 +33,7 @@ func (c ChangeSetBids[T]) Update(bids []*adapters.TypedBid) { c.changeSetRawBidderResponse.changeSet.AddMutation(func(p T) (T, error) { bidderPayload, err := c.changeSetRawBidderResponse.castPayload(p) if err == nil { - bidderPayload.Bids = bids + bidderPayload.BidderResponse.Bids = bids } if payload, ok := any(bidderPayload).(T); ok { return payload, nil diff --git a/modules/prebid/ortb2blocking/hook_raw_bidder_response.go b/modules/prebid/ortb2blocking/hook_raw_bidder_response.go index 215de260b09..e1a4d9c60dd 100644 --- a/modules/prebid/ortb2blocking/hook_raw_bidder_response.go +++ b/modules/prebid/ortb2blocking/hook_raw_bidder_response.go @@ -34,7 +34,7 @@ func handleRawBidderResponseHook( // allowedBids will store all bids that have passed the attribute check allowedBids := make([]*adapters.TypedBid, 0) - for _, bid := range payload.Bids { + for _, bid := range payload.BidderResponse.Bids { failedChecksData := make(map[string]interface{}) bidMediaTypes := mediaTypesFromBid(bid) @@ -77,7 +77,7 @@ func handleRawBidderResponseHook( } changeSet := hookstage.ChangeSet[hookstage.RawBidderResponsePayload]{} - if len(payload.Bids) != len(allowedBids) { + if len(payload.BidderResponse.Bids) != len(allowedBids) { changeSet.RawBidderResponse().Bids().Update(allowedBids) result.ChangeSet = changeSet } diff --git a/modules/prebid/ortb2blocking/module_test.go b/modules/prebid/ortb2blocking/module_test.go index 8178cba15fe..053fc9bfc75 100644 --- a/modules/prebid/ortb2blocking/module_test.go +++ b/modules/prebid/ortb2blocking/module_test.go @@ -614,9 +614,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }{ { description: "Payload not changed when empty account config and empty module contexts are provided. Analytic tags have successful records", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ADomain: []string{"foo"}, ImpID: impID1}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ADomain: []string{"foo"}, ImpID: impID1}, + }, }, }}, expectedBids: []*adapters.TypedBid{ @@ -643,9 +645,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Catch error if wrong data has been passed from previous hook. Payload not changed", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ADomain: []string{"foo"}, ImpID: impID1}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ADomain: []string{"foo"}, ImpID: impID1}, + }, }, }}, expectedBids: []*adapters.TypedBid{ @@ -658,12 +662,14 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Bid blocked by badv attribute check. Payload updated. Analytic tags successfully collected", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}, ImpID: impID1}, - }, - { - Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}, ImpID: impID1}, + }, + { + Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, + }, }, }}, config: json.RawMessage(`{"attributes":{"badv":{"enforce_blocks": true}}}`), @@ -701,12 +707,14 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Bid not blocked because blocking conditions for current bidder do not exist. Payload not updated. Analytic tags successfully collected", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}, ImpID: impID1}, - }, - { - Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}, ImpID: impID1}, + }, + { + Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, + }, }, }}, config: json.RawMessage(`{"attributes":{"badv":{"enforce_blocks": true}}}`), @@ -742,12 +750,14 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Bid not blocked because enforce blocking is disabled by account config. Payload not updated. Analytic tags successfully collected", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}, ImpID: impID1}, - }, - { - Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}, ImpID: impID1}, + }, + { + Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, + }, }, }}, config: json.RawMessage(`{"attributes":{"badv":{"enforce_blocks": false}}}`), @@ -783,12 +793,14 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Bid not blocked because enforce blocking overridden for given bidder. Payload not updated. Analytic tags successfully collected", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}, ImpID: impID1}, - }, - { - Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}, ImpID: impID1}, + }, + { + Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, + }, }, }}, config: json.RawMessage(`{"attributes":{"badv":{"enforce_blocks": true, "action_overrides": {"enforce_blocks": [{"conditions": {"bidders": ["appnexus"]}, "override": false}]}}}}`), @@ -824,12 +836,14 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Bid blocked by badv attribute check (block unknown attributes). Payload updated. Analytic tags successfully collected", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", ImpID: impID1}, - }, - { - Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", ImpID: impID1}, + }, + { + Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, + }, }, }}, config: json.RawMessage(`{"attributes":{"badv":{"enforce_blocks": true, "block_unknown_adomain": true}}}`), @@ -867,12 +881,14 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Bid not blocked because block unknown overridden for given bidder. Payload not updated. Analytic tags successfully collected", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", ImpID: impID1}, - }, - { - Bid: &openrtb2.Bid{ID: "2", ImpID: impID2}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", ImpID: impID1}, + }, + { + Bid: &openrtb2.Bid{ID: "2", ImpID: impID2}, + }, }, }}, config: json.RawMessage(`{"attributes":{"badv":{"enforce_blocks": true, "block_unknown_adomain": true, "action_overrides": {"block_unknown_adomain": [{"conditions": {"bidders": ["appnexus"]}, "override": false}]}}}}`), @@ -908,12 +924,14 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Bid not blocked due to deal exception. Payload not updated. Analytic tags successfully collected", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}, ImpID: impID1, DealID: "acceptDealID"}, - }, - { - Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}, ImpID: impID1, DealID: "acceptDealID"}, + }, + { + Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, + }, }, }}, config: json.RawMessage(`{"attributes":{"badv":{"enforce_blocks": true, "action_overrides": {"allowed_adomain_for_deals": [{"conditions": {"deal_ids": ["acceptDealID"]}, "override": ["forbidden_domain"]}]}}}}`), @@ -949,9 +967,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Expect error if there is an issue processing enforce blocks overrides for badv attribute. Analytics should have error status tag", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}}, + }, }, }}, config: json.RawMessage(`{"attributes": {"badv": {"enforce_blocks": true, "action_overrides": {"enforce_blocks": [{"conditions": {}}]}}}}`), @@ -974,9 +994,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Expect error if there is an issue processing block unknown domains overrides for badv attribute. Analytics should have error status tag", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}}, + }, }, }}, config: json.RawMessage(`{"attributes": {"badv": {"enforce_blocks": true, "action_overrides": {"block_unknown_adomain": [{"conditions": {}}]}}}}`), @@ -999,9 +1021,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Expect error if deal_ids not defined in config override conditions for badv attribute. Analytics should have error status tag", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}, DealID: "acceptDealID"}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", ADomain: []string{"forbidden_domain"}, DealID: "acceptDealID"}, + }, }, }}, config: json.RawMessage(`{"attributes": {"badv": {"enforce_blocks": true, "action_overrides": {"allowed_adomain_for_deals": [{"conditions": {}}]}}}}`), @@ -1024,12 +1048,14 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Bid blocked by bcat attribute check. Payload updated. Analytic tags successfully collected", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", Cat: []string{"fishing"}, ImpID: impID1}, - }, - { - Bid: &openrtb2.Bid{ID: "2", Cat: []string{"moto"}, ImpID: impID2}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", Cat: []string{"fishing"}, ImpID: impID1}, + }, + { + Bid: &openrtb2.Bid{ID: "2", Cat: []string{"moto"}, ImpID: impID2}, + }, }, }}, config: json.RawMessage(`{"attributes":{"bcat":{"enforce_blocks": true}}}`), @@ -1067,9 +1093,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Expect error if there is an issue processing enforce blocks overrides for bcat attribute. Analytics should have error status tag", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", Cat: []string{"fishing"}, ImpID: impID1}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", Cat: []string{"fishing"}, ImpID: impID1}, + }, }, }}, config: json.RawMessage(`{"attributes": {"bcat": {"enforce_blocks": true, "action_overrides": {"enforce_blocks": [{"conditions": {}}]}}}}`), @@ -1092,9 +1120,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Expect error if there is an issue processing block unknown domains overrides for bcat attribute. Analytics should have error status tag", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", Cat: []string{"fishing"}, ImpID: impID1}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", Cat: []string{"fishing"}, ImpID: impID1}, + }, }, }}, config: json.RawMessage(`{"attributes": {"bcat": {"enforce_blocks": true, "action_overrides": {"block_unknown_adv_cat": [{"conditions": {}}]}}}}`), @@ -1117,9 +1147,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Expect error if deal_ids not defined in config override conditions for bcat attribute. Analytics should have error status tag", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", Cat: []string{"fishing"}, ImpID: impID1, DealID: "acceptDealID"}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", Cat: []string{"fishing"}, ImpID: impID1, DealID: "acceptDealID"}, + }, }, }}, config: json.RawMessage(`{"attributes": {"bcat": {"enforce_blocks": true, "action_overrides": {"allowed_adv_cat_for_deals": [{"conditions": {}}]}}}}`), @@ -1142,12 +1174,14 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Bid blocked by cattax attribute check. Payload updated. Analytic tags successfully collected", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", CatTax: 1, ImpID: impID1}, - }, - { - Bid: &openrtb2.Bid{ID: "2", CatTax: 2, ImpID: impID2}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", CatTax: 1, ImpID: impID1}, + }, + { + Bid: &openrtb2.Bid{ID: "2", CatTax: 2, ImpID: impID2}, + }, }, }}, config: json.RawMessage(`{"attributes":{"bcat":{"enforce_blocks": true}}}`), @@ -1185,12 +1219,14 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Bid blocked by cattax attribute check (the default value used if no blocking attribute passed). Payload updated. Analytic tags successfully collected", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", CatTax: 1, ImpID: impID1}, - }, - { - Bid: &openrtb2.Bid{ID: "2", CatTax: 2, ImpID: impID2}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", CatTax: 1, ImpID: impID1}, + }, + { + Bid: &openrtb2.Bid{ID: "2", CatTax: 2, ImpID: impID2}, + }, }, }}, config: json.RawMessage(`{"attributes":{"bcat":{"enforce_blocks": true}}}`), @@ -1227,12 +1263,14 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Bid blocked by bapp attribute check. Payload updated. Analytic tags successfully collected", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", Bundle: "forbidden_bundle", ImpID: impID1}, - }, - { - Bid: &openrtb2.Bid{ID: "2", Bundle: "allowed_bundle", ImpID: impID2}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", Bundle: "forbidden_bundle", ImpID: impID1}, + }, + { + Bid: &openrtb2.Bid{ID: "2", Bundle: "allowed_bundle", ImpID: impID2}, + }, }, }}, config: json.RawMessage(`{"attributes":{"bapp":{"enforce_blocks": true}}}`), @@ -1270,9 +1308,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Expect error if there is an issue processing enforce blocks overrides for bapp attribute. Analytics should have error status tag", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", Bundle: "forbidden_bundle", ImpID: impID1}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", Bundle: "forbidden_bundle", ImpID: impID1}, + }, }, }}, config: json.RawMessage(`{"attributes": {"bapp": {"enforce_blocks": true, "action_overrides": {"enforce_blocks": [{"conditions": {}}]}}}}`), @@ -1295,9 +1335,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Expect error if deal_ids not defined in config override conditions for bapp attribute. Analytics should have error status tag", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", Bundle: "forbidden_bundle", ImpID: impID1, DealID: "acceptDealID"}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", Bundle: "forbidden_bundle", ImpID: impID1, DealID: "acceptDealID"}, + }, }, }}, config: json.RawMessage(`{"attributes": {"bapp": {"enforce_blocks": true, "action_overrides": {"allowed_app_for_deals": [{"conditions": {}}]}}}}`), @@ -1320,12 +1362,14 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Bid blocked by battr attribute check. Payload updated. Analytic tags successfully collected", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", Attr: []adcom1.CreativeAttribute{1}, ImpID: impID1}, - }, - { - Bid: &openrtb2.Bid{ID: "2", Attr: []adcom1.CreativeAttribute{2}, ImpID: impID2}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", Attr: []adcom1.CreativeAttribute{1}, ImpID: impID1}, + }, + { + Bid: &openrtb2.Bid{ID: "2", Attr: []adcom1.CreativeAttribute{2}, ImpID: impID2}, + }, }, }}, config: json.RawMessage(`{"attributes":{"battr":{"enforce_blocks": true}}}`), @@ -1363,9 +1407,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Expect error if there is an issue processing enforce blocks overrides for battr attribute. Analytics should have error status tag", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", Attr: []adcom1.CreativeAttribute{1}, ImpID: impID1}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", Attr: []adcom1.CreativeAttribute{1}, ImpID: impID1}, + }, }, }}, config: json.RawMessage(`{"attributes": {"battr": {"enforce_blocks": true, "action_overrides": {"enforce_blocks": [{"conditions": {}}]}}}}`), @@ -1388,9 +1434,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Expect error if deal_ids not defined in config override conditions for battr attribute. Analytics should have error status tag", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ID: "1", Attr: []adcom1.CreativeAttribute{1}, ImpID: impID1, DealID: "acceptDealID"}, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ID: "1", Attr: []adcom1.CreativeAttribute{1}, ImpID: impID1, DealID: "acceptDealID"}, + }, }, }}, config: json.RawMessage(`{"attributes": {"battr": {"enforce_blocks": true, "action_overrides": {"allowed_banner_attr_for_deals": [{"conditions": {}}]}}}}`), @@ -1413,27 +1461,29 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, { description: "Bid blocked by multiple attribute check. Payload updated. Analytic tags successfully collected", - payload: hookstage.RawBidderResponsePayload{Bidder: bidder, Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ - ID: "1", - ADomain: []string{"forbidden_domain"}, - Cat: []string{"fishing"}, - CatTax: 1, - Bundle: "forbidden_bundle", - Attr: []adcom1.CreativeAttribute{1}, - ImpID: impID1, + payload: hookstage.RawBidderResponsePayload{Bidder: bidder, BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "1", + ADomain: []string{"forbidden_domain"}, + Cat: []string{"fishing"}, + CatTax: 1, + Bundle: "forbidden_bundle", + Attr: []adcom1.CreativeAttribute{1}, + ImpID: impID1, + }, }, - }, - { - Bid: &openrtb2.Bid{ - ID: "2", - ADomain: []string{"allowed_domain"}, - Cat: []string{"moto"}, - CatTax: 2, - Bundle: "allowed_bundle", - Attr: []adcom1.CreativeAttribute{2}, - ImpID: impID2, + { + Bid: &openrtb2.Bid{ + ID: "2", + ADomain: []string{"allowed_domain"}, + Cat: []string{"moto"}, + CatTax: 2, + Bundle: "allowed_bundle", + Attr: []adcom1.CreativeAttribute{2}, + ImpID: impID2, + }, }, }, }}, @@ -1514,7 +1564,7 @@ func TestHandleRawBidderResponseHook(t *testing.T) { assert.NoError(t, err) test.payload = newPayload } - assert.Equal(t, test.expectedBids, test.payload.Bids, "Invalid Bids returned after executing RawBidderResponse hook.") + assert.Equal(t, test.expectedBids, test.payload.BidderResponse.Bids, "Invalid Bids returned after executing RawBidderResponse hook.") // reset ChangeSet not to break hookResult assertion, we validated ChangeSet separately hookResult.ChangeSet = hookstage.ChangeSet[hookstage.RawBidderResponsePayload]{} diff --git a/modules/pubmatic/openwrap/config/config.go b/modules/pubmatic/openwrap/config/config.go index 0a634af2f62..112a50a2770 100755 --- a/modules/pubmatic/openwrap/config/config.go +++ b/modules/pubmatic/openwrap/config/config.go @@ -101,7 +101,6 @@ type PixelView struct { type FeatureToggle struct { VASTUnwrapPercent int - VASTUnwrapStatsPercent int AnalyticsThrottlingPercentage string } diff --git a/modules/pubmatic/openwrap/hook_raw_bidder_response.go b/modules/pubmatic/openwrap/hook_raw_bidder_response.go index 8bdc4f06b00..a97f80a5af4 100644 --- a/modules/pubmatic/openwrap/hook_raw_bidder_response.go +++ b/modules/pubmatic/openwrap/hook_raw_bidder_response.go @@ -2,14 +2,20 @@ package openwrap import ( "fmt" - "sync" "github.com/prebid/prebid-server/v2/adapters" "github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/models" + "github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/models/nbr" + "github.com/prebid/prebid-server/v2/openrtb_ext" "github.com/prebid/prebid-server/v2/hooks/hookstage" ) +type BidUnwrapInfo struct { + bid *adapters.TypedBid + unwrapStatus string +} + func (m OpenWrap) handleRawBidderResponseHook( miCtx hookstage.ModuleInvocationContext, payload hookstage.RawBidderResponsePayload, @@ -20,41 +26,63 @@ func (m OpenWrap) handleRawBidderResponseHook( return result, nil } - if vastRequestContext.VastUnwrapEnabled { - // Do Unwrap and Update Adm - wg := new(sync.WaitGroup) - for _, bid := range payload.Bids { - if string(bid.BidType) == models.MediaTypeVideo { - wg.Add(1) - go func(bid *adapters.TypedBid) { - defer wg.Done() - m.unwrap.Unwrap(miCtx.AccountID, payload.Bidder, bid, vastRequestContext.UA, vastRequestContext.IP, vastRequestContext.VastUnwrapStatsEnabled) - }(bid) - } - } - wg.Wait() - changeSet := hookstage.ChangeSet[hookstage.RawBidderResponsePayload]{} - changeSet.RawBidderResponse().Bids().Update(payload.Bids) - result.ChangeSet = changeSet - } else { - vastRequestContext.VastUnwrapStatsEnabled = GetRandomNumberIn1To100() <= m.cfg.Features.VASTUnwrapStatsPercent - if vastRequestContext.VastUnwrapStatsEnabled { - // Do Unwrap and Collect stats only - for _, bid := range payload.Bids { - if string(bid.BidType) == models.MediaTypeVideo { - go func(bid *adapters.TypedBid) { - m.unwrap.Unwrap(miCtx.AccountID, payload.Bidder, bid, vastRequestContext.UA, vastRequestContext.IP, vastRequestContext.VastUnwrapStatsEnabled) - }(bid) - } - } + if !vastRequestContext.VastUnwrapEnabled { + return result, nil + } + + seatNonBid := openrtb_ext.NonBidCollection{} + unwrappedBids := make([]*adapters.TypedBid, 0, len(payload.BidderResponse.Bids)) + unwrappedBidsChan := make(chan BidUnwrapInfo, len(payload.BidderResponse.Bids)) + defer close(unwrappedBidsChan) + + unwrappedBidsCnt, unwrappedSuccessBidCnt := 0, 0 + totalBidCnt := len(payload.BidderResponse.Bids) + // send bids for unwrap + for _, bid := range payload.BidderResponse.Bids { + if !isEligibleForUnwrap(bid) { + unwrappedBids = append(unwrappedBids, bid) + continue } + unwrappedBidsCnt++ + go func(bid adapters.TypedBid) { + unwrapStatus := m.unwrap.Unwrap(&bid, miCtx.AccountID, payload.Bidder, vastRequestContext.UA, vastRequestContext.IP) + unwrappedBidsChan <- BidUnwrapInfo{&bid, unwrapStatus} + }(*bid) } - if vastRequestContext.VastUnwrapEnabled || vastRequestContext.VastUnwrapStatsEnabled { - result.DebugMessages = append(result.DebugMessages, - fmt.Sprintf("For pubid:[%d] VastUnwrapEnabled: [%v] VastUnwrapStatsEnabled:[%v] ", - vastRequestContext.PubID, vastRequestContext.VastUnwrapEnabled, vastRequestContext.VastUnwrapStatsEnabled)) + // collect bids after unwrap + for i := 0; i < unwrappedBidsCnt; i++ { + unwrappedBid := <-unwrappedBidsChan + if !rejectBid(unwrappedBid.unwrapStatus) { + unwrappedSuccessBidCnt++ + unwrappedBids = append(unwrappedBids, unwrappedBid.bid) + continue + } + seatNonBid.AddBid(openrtb_ext.NewNonBid(openrtb_ext.NonBidParams{ + Bid: unwrappedBid.bid.Bid, + NonBidReason: int(nbr.LossBidLostInVastUnwrap), + DealPriority: unwrappedBid.bid.DealPriority, + BidMeta: unwrappedBid.bid.BidMeta, + BidType: unwrappedBid.bid.BidType, + BidVideo: unwrappedBid.bid.BidVideo, + OriginalBidCur: payload.BidderResponse.Currency, + }), payload.Bidder, + ) } + changeSet := hookstage.ChangeSet[hookstage.RawBidderResponsePayload]{} + changeSet.RawBidderResponse().Bids().Update(unwrappedBids) + result.ChangeSet = changeSet + result.SeatNonBid = seatNonBid + result.DebugMessages = append(result.DebugMessages, + fmt.Sprintf("For pubid:[%d] VastUnwrapEnabled: [%v] Total Input Bids: [%d] Total Bids sent for unwrapping: [%d] Total Unwrap Success: [%d]", vastRequestContext.PubID, vastRequestContext.VastUnwrapEnabled, totalBidCnt, unwrappedBidsCnt, unwrappedSuccessBidCnt)) return result, nil } + +func isEligibleForUnwrap(bid *adapters.TypedBid) bool { + return bid != nil && bid.BidType == openrtb_ext.BidTypeVideo && bid.Bid != nil && bid.Bid.AdM != "" +} + +func rejectBid(bidUnwrapStatus string) bool { + return bidUnwrapStatus == models.UnwrapEmptyVASTStatus || bidUnwrapStatus == models.UnwrapInvalidVASTStatus +} diff --git a/modules/pubmatic/openwrap/hook_raw_bidder_response_test.go b/modules/pubmatic/openwrap/hook_raw_bidder_response_test.go index 07130c3aed3..421dd4f6285 100644 --- a/modules/pubmatic/openwrap/hook_raw_bidder_response_test.go +++ b/modules/pubmatic/openwrap/hook_raw_bidder_response_test.go @@ -3,7 +3,6 @@ package openwrap import ( "fmt" "net/http" - "testing" unWrapCfg "git.pubmatic.com/vastunwrap/config" @@ -14,7 +13,9 @@ import ( "github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/config" mock_metrics "github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/metrics/mock" "github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/models" + "github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/models/nbr" "github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/unwrap" + "github.com/prebid/prebid-server/v2/openrtb_ext" "github.com/stretchr/testify/assert" ) @@ -23,7 +24,6 @@ var invalidVastXMLAdM = "PubMaticAcudeo CompatibleVAST 2.0 Instream Test 1VAST 2.0 Instream Test 1https://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&er=[ERRORCODE]https://track.dsptracker.com?p=1234&er=[ERRORCODE]https://aktrack.pubmatic.com/AdServer/AdDisplayTrackerServlet?operId=1&pubId=64195&siteId=47105&adId=1405154&adType=13&adServerId=243&kefact=1.000000&kaxefact=1.000000&kadNetFrequecy=0&kadwidth=0&kadheight=0&kadsizeid=97&kltstamp=1536933242&indirectAdId=0&adServerOptimizerId=2&ranreq=0.05969169352174375&kpbmtpfact=11.000000&dcId=1&tldId=0&passback=0&svr=ktk57&ekefact=er2bW2sDAwCra06ACbsIQySn5nqBtYsTl8fy5lupAexh37D_&ekaxefact=er2bW4EDAwB_LQpJJ23Fq0DcNC-NSAFXdpSQC8XBk_S33_Fa&ekpbmtpfact=er2bW5MDAwDJHdBnLBt5IrRuh7x0oqp_tjIALv_VvSQDAl6R&crID=m:1_x:3_y:3_p:11_va:3&lpu=ae.com&ucrid=678722001014421372&campaignId=16774&creativeId=0&pctr=0.000000&wDSPByrId=511&wDspId=27&wbId=0&wrId=0&wAdvID=3170&isRTB=1&rtbId=EBCA079F-8D7C-45B8-B733-92951F670AA1&imprId=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&oid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&pageURL=http%253A%252F%252Fowsdk-stagingams.pubmatic.com%253A8443%252Fvast-validator%252F%2523&sec=1&pmc=1https://DspImpressionTracker.com/https://mytracking.com/linear/closehttps://mytracking.com/linear/skiphttps://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=1https://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=2https://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=3https://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=4https://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=5https://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=600:00:04https://www.automationtester.inhttps://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=99https://stagingams.pubmatic.com:8443/openwrap/media/pubmatic.mp4https://stagingams.pubmatic.com:8443/openwrap/media/pubmatic.mp4https://stagingams.pubmatic.com:8443/openwrap/media/mp4-sample-3.mp4" func TestHandleRawBidderResponseHook(t *testing.T) { - ctrl := gomock.NewController(t) defer ctrl.Finish() mockMetricsEngine := mock_metrics.NewMockMetricsEngine(ctrl) @@ -33,27 +33,25 @@ func TestHandleRawBidderResponseHook(t *testing.T) { payload hookstage.RawBidderResponsePayload moduleInvocationCtx hookstage.ModuleInvocationContext isAdmUpdated bool - randomNumber int } tests := []struct { - name string - args args - wantResult hookstage.HookResult[hookstage.RawBidderResponsePayload] - setup func() - wantErr bool - mockHandler http.HandlerFunc + name string + args args + wantResult hookstage.HookResult[hookstage.RawBidderResponsePayload] + setup func() + wantSeatNonBid openrtb_ext.NonBidCollection + mockHandler http.HandlerFunc + wantBids []*adapters.TypedBid }{ - { - name: "Empty Request Context", + name: "Empty_Request_Context", args: args{ module: OpenWrap{}, }, wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{DebugMessages: []string{"error: request-ctx not found in handleRawBidderResponseHook()"}}, - wantErr: false, }, { - name: "Set Vast Unwrapper to false in request context with type video", + name: "VASTUnwrap_Disabled_Video_Bids", args: args{ module: OpenWrap{ cfg: config.Config{VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ @@ -64,32 +62,31 @@ func TestHandleRawBidderResponseHook(t *testing.T) { metricEngine: mockMetricsEngine, }, payload: hookstage.RawBidderResponsePayload{ - Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ - ID: "Bid-123", - ImpID: fmt.Sprintf("div-adunit-%d", 123), - Price: 2.1, - AdM: "
This is an Ad
", - CrID: "Cr-234", - W: 100, - H: 50, - }, - BidType: "video", - }}}, + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "
This is an Ad
", + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }}}}, moduleInvocationCtx: hookstage.ModuleInvocationContext{ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: false}}}, - randomNumber: 1, }, wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, - wantErr: false, }, { - name: "Set Vast Unwrapper to false in request context with type video, stats enabled true", + name: "VASTUnwrap_Enabled_Single_Video_Bid_Invalid_Vast_xml", args: args{ module: OpenWrap{ cfg: config.Config{ Features: config.FeatureToggle{ - VASTUnwrapStatsPercent: 2, + VASTUnwrapPercent: 50, }, VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ MaxWrapperSupport: 5, @@ -99,30 +96,93 @@ func TestHandleRawBidderResponseHook(t *testing.T) { metricEngine: mockMetricsEngine, }, payload: hookstage.RawBidderResponsePayload{ - Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ - ID: "Bid-123", - ImpID: fmt.Sprintf("div-adunit-%d", 123), - Price: 2.1, - AdM: vastXMLAdM, - CrID: "Cr-234", - W: 100, - H: 50, + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: invalidVastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", }, - BidType: "video", - }}, + }, + }, Bidder: "pubmatic", }, - moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: false}}}, - randomNumber: 1, + moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: true}}}, + }, + mockHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Add("unwrap-status", "1") + w.WriteHeader(http.StatusNoContent) + }), + wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, + setup: func() { + mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "1") + mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()) + }, + wantBids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: invalidVastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, }, + }, + { + name: "VASTUnwrap_Enabled_Single_Video_Bid", mockHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Add("unwrap-status", "0") w.Header().Add("unwrap-count", "1") w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(vastXMLAdM)) + _, _ = w.Write([]byte(inlineXMLAdM)) }), + args: args{ + module: OpenWrap{ + cfg: config.Config{ + Features: config.FeatureToggle{ + VASTUnwrapPercent: 50, + }, + VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ + MaxWrapperSupport: 5, + StatConfig: unWrapCfg.StatConfig{Endpoint: "http://10.172.141.13:8080", PublishInterval: 1}, + APPConfig: unWrapCfg.AppConfig{UnwrapDefaultTimeout: 1500}, + }}, + metricEngine: mockMetricsEngine, + }, + payload: hookstage.RawBidderResponsePayload{ + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: vastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, + }, + }, + Bidder: "pubmatic", + }, + moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: true}}}, + isAdmUpdated: true, + }, wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, setup: func() { mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "0") @@ -130,16 +190,28 @@ func TestHandleRawBidderResponseHook(t *testing.T) { mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()) mockMetricsEngine.EXPECT().RecordUnwrapRespTime("5890", "1", gomock.Any()) }, - wantErr: false, + wantBids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: inlineXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, + }, }, { - name: "Set Vast Unwrapper to true in request context with invalid vast xml", + name: "VASTUnwrap_Enabled_Multiple_Video_Bids", args: args{ module: OpenWrap{ cfg: config.Config{ Features: config.FeatureToggle{ - VASTUnwrapStatsPercent: 2, - VASTUnwrapPercent: 50, + VASTUnwrapPercent: 100, }, VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ MaxWrapperSupport: 5, @@ -149,50 +221,173 @@ func TestHandleRawBidderResponseHook(t *testing.T) { metricEngine: mockMetricsEngine, }, payload: hookstage.RawBidderResponsePayload{ - Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ - ID: "Bid-123", - ImpID: fmt.Sprintf("div-adunit-%d", 123), - Price: 2.1, - AdM: invalidVastXMLAdM, - CrID: "Cr-234", - W: 100, - H: 50, + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: vastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", }, - BidType: "video", - }, + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: vastXMLAdM, + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "video", + }}, }, Bidder: "pubmatic", }, moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: true}}}, + isAdmUpdated: true, }, mockHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Add("unwrap-status", "1") - w.WriteHeader(http.StatusNoContent) + w.Header().Add("unwrap-status", "0") + w.Header().Add("unwrap-count", "1") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(inlineXMLAdM)) }), wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, setup: func() { - mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "1") - mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()) + mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "0").Times(2) + mockMetricsEngine.EXPECT().RecordUnwrapWrapperCount("5890", "pubmatic", "1").Times(2) + mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()).Times(2) + mockMetricsEngine.EXPECT().RecordUnwrapRespTime("5890", "1", gomock.Any()).Times(2) + }, + wantBids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: inlineXMLAdM, + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "video", + }, + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: inlineXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, }, - wantErr: true, }, - { - name: "Set Vast Unwrapper to true in request context with type video", + name: "VASTUnwrap_Enabled_Video_and_Banner_Bids", + args: args{ + module: OpenWrap{ + cfg: config.Config{ + Features: config.FeatureToggle{ + VASTUnwrapPercent: 50, + }, + VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ + MaxWrapperSupport: 5, + StatConfig: unWrapCfg.StatConfig{Endpoint: "http://10.172.141.13:8080", PublishInterval: 1}, + APPConfig: unWrapCfg.AppConfig{UnwrapDefaultTimeout: 1500}, + }}, + metricEngine: mockMetricsEngine, + }, + payload: hookstage.RawBidderResponsePayload{ + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: vastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is banner creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "banner", + }}, + }, + Bidder: "pubmatic", + }, + moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: true}}}, + isAdmUpdated: true, + }, mockHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Add("unwrap-status", "0") - w.Header().Add("unwrap-count", "1") + w.Header().Add("unwrap-count", "0") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(inlineXMLAdM)) }), + wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, + setup: func() { + mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "0") + mockMetricsEngine.EXPECT().RecordUnwrapWrapperCount("5890", "pubmatic", "0") + mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()) + mockMetricsEngine.EXPECT().RecordUnwrapRespTime("5890", "0", gomock.Any()) + }, + wantBids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is banner creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "banner", + }, + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: inlineXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, + }, + }, + { + name: "VASTUnwrap_Enabled_Video_and_Native_Bids", args: args{ module: OpenWrap{ cfg: config.Config{ Features: config.FeatureToggle{ - VASTUnwrapStatsPercent: 2, - VASTUnwrapPercent: 50, + VASTUnwrapPercent: 50, }, VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ MaxWrapperSupport: 5, @@ -202,43 +397,85 @@ func TestHandleRawBidderResponseHook(t *testing.T) { metricEngine: mockMetricsEngine, }, payload: hookstage.RawBidderResponsePayload{ - Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ - ID: "Bid-123", - ImpID: fmt.Sprintf("div-adunit-%d", 123), - Price: 2.1, - AdM: vastXMLAdM, - CrID: "Cr-234", - W: 100, - H: 50, + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: vastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", }, - BidType: "video", - }, + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is native creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "native", + }}, }, Bidder: "pubmatic", }, moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: true}}}, - - isAdmUpdated: true, + isAdmUpdated: true, }, + mockHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Add("unwrap-status", "0") + w.Header().Add("unwrap-count", "0") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(inlineXMLAdM)) + }), wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, setup: func() { - mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "0").AnyTimes() - mockMetricsEngine.EXPECT().RecordUnwrapWrapperCount("5890", "pubmatic", "1").AnyTimes() - mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()).AnyTimes() - mockMetricsEngine.EXPECT().RecordUnwrapRespTime("5890", "1", gomock.Any()).AnyTimes() + mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "0") + mockMetricsEngine.EXPECT().RecordUnwrapWrapperCount("5890", "pubmatic", "0") + mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()) + mockMetricsEngine.EXPECT().RecordUnwrapRespTime("5890", "0", gomock.Any()) + }, + wantBids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is native creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "native", + }, + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: inlineXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, }, - wantErr: false, }, { - name: "Set Vast Unwrapper to true in request context for multiple bids with type video", + name: "VASTUnwrap_Enabled_Single_Video_bid_and_source_owsdk", args: args{ module: OpenWrap{ cfg: config.Config{ Features: config.FeatureToggle{ - VASTUnwrapStatsPercent: 2, - VASTUnwrapPercent: 50, + VASTUnwrapPercent: 50, }, VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ MaxWrapperSupport: 5, @@ -248,36 +485,26 @@ func TestHandleRawBidderResponseHook(t *testing.T) { metricEngine: mockMetricsEngine, }, payload: hookstage.RawBidderResponsePayload{ - Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ - ID: "Bid-123", - ImpID: fmt.Sprintf("div-adunit-%d", 123), - Price: 2.1, - AdM: vastXMLAdM, - CrID: "Cr-234", - W: 100, - H: 50, + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: vastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", }, - BidType: "video", }, - { - Bid: &openrtb2.Bid{ - ID: "Bid-456", - ImpID: fmt.Sprintf("div-adunit-%d", 123), - Price: 2.1, - AdM: vastXMLAdM, - CrID: "Cr-789", - W: 100, - H: 50, - }, - BidType: "video", - }}, + }, Bidder: "pubmatic", }, moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: true}}}, isAdmUpdated: true, - randomNumber: 10, }, mockHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Add("unwrap-status", "0") @@ -287,22 +514,33 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }), wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, setup: func() { - mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "0").AnyTimes() - mockMetricsEngine.EXPECT().RecordUnwrapWrapperCount("5890", "pubmatic", "1").AnyTimes() - mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()).AnyTimes() - mockMetricsEngine.EXPECT().RecordUnwrapRespTime("5890", "1", gomock.Any()).AnyTimes() + mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "0") + mockMetricsEngine.EXPECT().RecordUnwrapWrapperCount("5890", "pubmatic", "1") + mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()) + mockMetricsEngine.EXPECT().RecordUnwrapRespTime("5890", "1", gomock.Any()) + }, + wantBids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: inlineXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, }, - wantErr: false, }, - { - name: "Set Vast Unwrapper to true in request context for multiple bids with different type", + name: "VASTUnwrap_Enabled_Native_and_Banner_Bids", args: args{ module: OpenWrap{ cfg: config.Config{ Features: config.FeatureToggle{ - VASTUnwrapStatsPercent: 2, - VASTUnwrapPercent: 50, + VASTUnwrapPercent: 50, }, VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ MaxWrapperSupport: 5, @@ -312,60 +550,423 @@ func TestHandleRawBidderResponseHook(t *testing.T) { metricEngine: mockMetricsEngine, }, payload: hookstage.RawBidderResponsePayload{ - Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ - ID: "Bid-123", - ImpID: fmt.Sprintf("div-adunit-%d", 123), - Price: 2.1, - AdM: vastXMLAdM, - CrID: "Cr-234", - W: 100, - H: 50, + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is banner creative", + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "banner", + }, + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is native creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "native", + }}, + }, + Bidder: "pubmatic", + }, + moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: true}}}, + }, + wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, + wantBids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is banner creative", + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "banner", + }, + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is native creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "native", + }, + }, + }, + { + name: "bid_with_InvalidVAST_should_be_discarded_and_should_be_present_in_seatNonBid", + args: args{ + module: OpenWrap{ + cfg: config.Config{ + Features: config.FeatureToggle{ + VASTUnwrapPercent: 50, + }, + VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ + MaxWrapperSupport: 5, + StatConfig: unWrapCfg.StatConfig{Endpoint: "http://10.172.141.13:8080", PublishInterval: 1}, + APPConfig: unWrapCfg.AppConfig{UnwrapDefaultTimeout: 1500}, + }}, + metricEngine: mockMetricsEngine, + }, + payload: hookstage.RawBidderResponsePayload{ + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: invalidVastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", }, - BidType: "video", }, - { - Bid: &openrtb2.Bid{ - ID: "Bid-456", - ImpID: fmt.Sprintf("div-adunit-%d", 123), + }, + Bidder: "pubmatic", + }, + moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: true}}}, + }, + mockHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Add("unwrap-status", models.UnwrapInvalidVASTStatus) + w.WriteHeader(http.StatusNoContent) + }), + wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, + setup: func() { + mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", models.UnwrapInvalidVASTStatus) + mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()) + }, + wantBids: []*adapters.TypedBid{}, + wantSeatNonBid: func() openrtb_ext.NonBidCollection { + seatNonBid := openrtb_ext.NonBidCollection{} + seatNonBid.AddBid(openrtb_ext.NonBid{ + ImpId: fmt.Sprintf("div-adunit-%d", 123), + StatusCode: int(nbr.LossBidLostInVastUnwrap), + Ext: openrtb_ext.ExtNonBid{ + Prebid: openrtb_ext.ExtNonBidPrebid{ + Bid: openrtb_ext.ExtNonBidPrebidBid{ Price: 2.1, - AdM: "This is banner creative", - CrID: "Cr-789", + ID: "Bid-123", W: 100, H: 50, + Type: openrtb_ext.BidTypeVideo, }, - BidType: "banner", + }, + }, + }, "pubmatic") + return seatNonBid + }(), + }, + { + name: "bid_with_EmptyVAST_should_be_discarded_and_should_be_present_in_seatNonBid", + args: args{ + module: OpenWrap{ + cfg: config.Config{ + Features: config.FeatureToggle{ + VASTUnwrapPercent: 50, + }, + VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ + MaxWrapperSupport: 5, + StatConfig: unWrapCfg.StatConfig{Endpoint: "http://10.172.141.13:8080", PublishInterval: 1}, + APPConfig: unWrapCfg.AppConfig{UnwrapDefaultTimeout: 1500}, }}, + metricEngine: mockMetricsEngine, + }, + payload: hookstage.RawBidderResponsePayload{ + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: invalidVastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, + }, + }, Bidder: "pubmatic", }, moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: true}}}, - - isAdmUpdated: true, }, mockHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Add("unwrap-status", "0") - w.Header().Add("unwrap-count", "0") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(inlineXMLAdM)) + w.Header().Add("unwrap-status", models.UnwrapEmptyVASTStatus) + w.WriteHeader(http.StatusNoContent) }), wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, setup: func() { - mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "0").AnyTimes() - mockMetricsEngine.EXPECT().RecordUnwrapWrapperCount("5890", "pubmatic", "0").AnyTimes() - mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()).AnyTimes() - mockMetricsEngine.EXPECT().RecordUnwrapRespTime("5890", "0", gomock.Any()).AnyTimes() + mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", models.UnwrapEmptyVASTStatus) + mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()) + }, + wantBids: []*adapters.TypedBid{}, + wantSeatNonBid: func() openrtb_ext.NonBidCollection { + seatNonBid := openrtb_ext.NonBidCollection{} + seatNonBid.AddBid(openrtb_ext.NonBid{ + ImpId: fmt.Sprintf("div-adunit-%d", 123), + StatusCode: int(nbr.LossBidLostInVastUnwrap), + Ext: openrtb_ext.ExtNonBid{ + Prebid: openrtb_ext.ExtNonBidPrebid{ + Bid: openrtb_ext.ExtNonBidPrebidBid{ + Price: 2.1, + ID: "Bid-123", + W: 100, + H: 50, + Type: openrtb_ext.BidTypeVideo, + }, + }, + }, + }, "pubmatic") + return seatNonBid + }(), + }, + { + name: "VASTUnwrap_Disabled_Video_Bids_Valid_XML", + args: args{ + module: OpenWrap{ + cfg: config.Config{ + Features: config.FeatureToggle{}, + VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ + MaxWrapperSupport: 5, + StatConfig: unWrapCfg.StatConfig{Endpoint: "http://10.172.141.13:8080", PublishInterval: 1}, + APPConfig: unWrapCfg.AppConfig{UnwrapDefaultTimeout: 1500}, + }}, + metricEngine: mockMetricsEngine, + }, + payload: hookstage.RawBidderResponsePayload{ + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: vastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }}}, + Bidder: "pubmatic", + }, + moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: false}}}, + }, + wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, + wantBids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: vastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, + }, + }, + { + name: "VASTUnwrap_Disabled_Video_and_Banner_Bids", + args: args{ + module: OpenWrap{ + cfg: config.Config{ + Features: config.FeatureToggle{}, + VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ + MaxWrapperSupport: 5, + StatConfig: unWrapCfg.StatConfig{Endpoint: "http://10.172.141.13:8080", PublishInterval: 1}, + APPConfig: unWrapCfg.AppConfig{UnwrapDefaultTimeout: 1500}, + }}, + metricEngine: mockMetricsEngine, + }, + payload: hookstage.RawBidderResponsePayload{ + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: vastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is banner creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "banner", + }}, + }, + Bidder: "pubmatic", + }, + moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: false}}}, + }, + wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, + wantBids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: vastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is banner creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "banner", + }, }, - wantErr: false, }, { - name: "Set Vast Unwrapper to true in request context with type video and source owsdk", + name: "VASTUnwrap_Disabled_Banner_Bids", + args: args{ + module: OpenWrap{ + cfg: config.Config{ + Features: config.FeatureToggle{}, + VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ + MaxWrapperSupport: 5, + StatConfig: unWrapCfg.StatConfig{Endpoint: "http://10.172.141.13:8080", PublishInterval: 1}, + APPConfig: unWrapCfg.AppConfig{UnwrapDefaultTimeout: 1500}, + }}, + metricEngine: mockMetricsEngine, + }, + payload: hookstage.RawBidderResponsePayload{ + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is banner creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "banner", + }}, + }, + Bidder: "pubmatic", + }, + moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: false}}}, + }, + wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, + wantBids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is banner creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "banner", + }, + }, + }, + { + name: "VASTUnwrap_Enabled_Banner_Bids", + args: args{ + module: OpenWrap{ + cfg: config.Config{ + Features: config.FeatureToggle{}, + VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ + MaxWrapperSupport: 5, + StatConfig: unWrapCfg.StatConfig{Endpoint: "http://10.172.141.13:8080", PublishInterval: 1}, + APPConfig: unWrapCfg.AppConfig{UnwrapDefaultTimeout: 1500}, + }}, + metricEngine: mockMetricsEngine, + }, + payload: hookstage.RawBidderResponsePayload{ + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is banner creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "banner", + }}, + }, + Bidder: "pubmatic", + }, + moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: true}}}, + }, + wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, + wantBids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is banner creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "banner", + }, + }, + }, + { + name: "VASTUnwrap_Enabled_Invalid_Video_and_Banner_Bids", args: args{ module: OpenWrap{ cfg: config.Config{ Features: config.FeatureToggle{ - VASTUnwrapStatsPercent: 2, - VASTUnwrapPercent: 50, + VASTUnwrapPercent: 50, }, VastUnwrapCfg: unWrapCfg.VastUnWrapCfg{ MaxWrapperSupport: 5, @@ -375,40 +976,73 @@ func TestHandleRawBidderResponseHook(t *testing.T) { metricEngine: mockMetricsEngine, }, payload: hookstage.RawBidderResponsePayload{ - Bids: []*adapters.TypedBid{ - { - Bid: &openrtb2.Bid{ - ID: "Bid-123", - ImpID: fmt.Sprintf("div-adunit-%d", 123), - Price: 2.1, - AdM: vastXMLAdM, - CrID: "Cr-234", - W: 100, - H: 50, + BidderResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: invalidVastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is banner creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "banner", }, - BidType: "video", }, }, Bidder: "pubmatic", }, moduleInvocationCtx: hookstage.ModuleInvocationContext{AccountID: "5890", ModuleContext: hookstage.ModuleContext{models.RequestContext: models.RequestCtx{VastUnwrapEnabled: true}}}, - - isAdmUpdated: true, }, mockHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Add("unwrap-status", "0") - w.Header().Add("unwrap-count", "1") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(inlineXMLAdM)) + w.Header().Add("unwrap-status", "1") + w.WriteHeader(http.StatusNoContent) }), wantResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{Reject: false}, setup: func() { - mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "0").AnyTimes() - mockMetricsEngine.EXPECT().RecordUnwrapWrapperCount("5890", "pubmatic", "1").AnyTimes() - mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()).AnyTimes() - mockMetricsEngine.EXPECT().RecordUnwrapRespTime("5890", "1", gomock.Any()).AnyTimes() + mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "1") + mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()) + }, + wantBids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{ + ID: "Bid-456", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: "This is banner creative", + CrID: "Cr-789", + W: 100, + H: 50, + }, + BidType: "banner", + }, + { + Bid: &openrtb2.Bid{ + ID: "Bid-123", + ImpID: fmt.Sprintf("div-adunit-%d", 123), + Price: 2.1, + AdM: invalidVastXMLAdM, + CrID: "Cr-234", + W: 100, + H: 50, + }, + BidType: "video", + }, }, - wantErr: false, }, } for _, tt := range tests { @@ -417,19 +1051,65 @@ func TestHandleRawBidderResponseHook(t *testing.T) { tt.setup() } - GetRandomNumberIn1To100 = func() int { - return tt.args.randomNumber - } - m := tt.args.module m.unwrap = unwrap.NewUnwrap("http://localhost:8001/unwrap", 200, tt.mockHandler, m.metricEngine) - _, err := m.handleRawBidderResponseHook(tt.args.moduleInvocationCtx, tt.args.payload) - if !assert.NoError(t, err, tt.wantErr) { - return - } + hookResult, _ := m.handleRawBidderResponseHook(tt.args.moduleInvocationCtx, tt.args.payload) if tt.args.moduleInvocationCtx.ModuleContext != nil && tt.args.isAdmUpdated { - assert.Equal(t, inlineXMLAdM, tt.args.payload.Bids[0].Bid.AdM, "AdM is not updated correctly after executing RawBidderResponse hook.") + assert.Equal(t, inlineXMLAdM, tt.args.payload.BidderResponse.Bids[0].Bid.AdM, "AdM is not updated correctly after executing RawBidderResponse hook.") + } + for _, mut := range hookResult.ChangeSet.Mutations() { + newPayload, err := mut.Apply(tt.args.payload) + assert.NoError(t, err) + tt.args.payload = newPayload + } + if tt.wantBids != nil { + assert.ElementsMatch(t, tt.wantBids, tt.args.payload.BidderResponse.Bids, "Mismatched response bids") } + + assert.Equal(t, tt.wantSeatNonBid, hookResult.SeatNonBid, "mismatched seatNonBids") + }) + } +} + +func TestIsEligibleForUnwrap(t *testing.T) { + type args struct { + bid *adapters.TypedBid + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Bid is nil", + args: args{bid: nil}, + want: false, + }, + { + name: "Bid.Bid is nil", + args: args{bid: &adapters.TypedBid{Bid: nil}}, + want: false, + }, + { + name: "AdM is empty", + args: args{bid: &adapters.TypedBid{Bid: &openrtb2.Bid{AdM: ""}}}, + want: false, + }, + { + name: "BidType is not video", + args: args{bid: &adapters.TypedBid{Bid: &openrtb2.Bid{AdM: "some_adm"}, BidType: openrtb_ext.BidTypeBanner}}, + want: false, + }, + { + name: "Bid is eligible for unwrap", + args: args{bid: &adapters.TypedBid{Bid: &openrtb2.Bid{AdM: "some_adm"}, BidType: openrtb_ext.BidTypeVideo}}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isEligibleForUnwrap(tt.args.bid) + assert.Equal(t, tt.want, got) }) } } diff --git a/modules/pubmatic/openwrap/models/constants.go b/modules/pubmatic/openwrap/models/constants.go index d55973c7bf0..7b49843fff7 100755 --- a/modules/pubmatic/openwrap/models/constants.go +++ b/modules/pubmatic/openwrap/models/constants.go @@ -374,21 +374,23 @@ const ( Enabled = "1" // VAST Unwrap - RequestContext = "rctx" - UnwrapCount = "unwrap-count" - UnwrapStatus = "unwrap-status" - Timeout = "Timeout" - UnwrapSucessStatus = "0" - UnwrapTimeout = "unwrap-timeout" - MediaTypeVideo = "video" - ProfileId = "profileID" - VersionId = "versionID" - DisplayId = "DisplayID" - XUserIP = "X-Forwarded-For" - XUserAgent = "X-Device-User-Agent" - CreativeID = "unwrap-ucrid" - PubID = "pub_id" - ImpressionID = "imr_id" + RequestContext = "rctx" + UnwrapCount = "unwrap-count" + UnwrapStatus = "unwrap-status" + Timeout = "Timeout" + UnwrapSucessStatus = "0" + UnwrapEmptyVASTStatus = "4" + UnwrapInvalidVASTStatus = "6" + UnwrapTimeout = "unwrap-timeout" + MediaTypeVideo = "video" + ProfileId = "profileID" + VersionId = "versionID" + DisplayId = "DisplayID" + XUserIP = "X-Forwarded-For" + XUserAgent = "X-Device-User-Agent" + CreativeID = "unwrap-ucrid" + PubID = "pub_id" + ImpressionID = "imr_id" //Constants for new SDK reporting ProfileTypeKey = "type" diff --git a/modules/pubmatic/openwrap/models/nbr/codes.go b/modules/pubmatic/openwrap/models/nbr/codes.go index 7eafb8a5d09..fa54481d8d1 100644 --- a/modules/pubmatic/openwrap/models/nbr/codes.go +++ b/modules/pubmatic/openwrap/models/nbr/codes.go @@ -9,6 +9,7 @@ const ( RequestBlockedSlotNotMapped openrtb3.NoBidReason = 503 RequestBlockedPartnerThrottle openrtb3.NoBidReason = 504 RequestBlockedPartnerFiltered openrtb3.NoBidReason = 505 + LossBidLostInVastUnwrap openrtb3.NoBidReason = 506 ) // Openwrap module specific codes diff --git a/modules/pubmatic/openwrap/unwrap/unwrap.go b/modules/pubmatic/openwrap/unwrap/unwrap.go index ef002929116..24c5b24ba38 100644 --- a/modules/pubmatic/openwrap/unwrap/unwrap.go +++ b/modules/pubmatic/openwrap/unwrap/unwrap.go @@ -40,21 +40,17 @@ func NewUnwrap(Endpoint string, DefaultTime int, handler http.HandlerFunc, Metri } -func (uw Unwrap) Unwrap(accountID, bidder string, bid *adapters.TypedBid, userAgent, ip string, isStatsEnabled bool) { +func (uw Unwrap) Unwrap(bid *adapters.TypedBid, accountID, bidder, userAgent, ip string) (unwrapStatus string) { startTime := time.Now() var wrapperCnt int64 - var respStatus string - if bid == nil || bid.Bid == nil || bid.Bid.AdM == "" { - return - } defer func() { if r := recover(); r != nil { glog.Errorf("AdM:[%s] Error:[%v] stacktrace:[%s]", bid.Bid.AdM, r, string(debug.Stack())) } respTime := time.Since(startTime) uw.metricEngine.RecordUnwrapRequestTime(accountID, bidder, respTime) - uw.metricEngine.RecordUnwrapRequestStatus(accountID, bidder, respStatus) - if respStatus == "0" { + uw.metricEngine.RecordUnwrapRequestStatus(accountID, bidder, unwrapStatus) + if unwrapStatus == "0" { uw.metricEngine.RecordUnwrapWrapperCount(accountID, bidder, strconv.Itoa(int(wrapperCnt))) uw.metricEngine.RecordUnwrapRespTime(accountID, strconv.Itoa(int(wrapperCnt)), respTime) } @@ -76,13 +72,14 @@ func (uw Unwrap) Unwrap(accountID, bidder string, bid *adapters.TypedBid, userAg httpReq.Header = headers httpResp := NewCustomRecorder() uw.unwrapRequest(httpResp, httpReq) - respStatus = httpResp.Header().Get(models.UnwrapStatus) + unwrapStatus = httpResp.Header().Get(models.UnwrapStatus) wrapperCnt, _ = strconv.ParseInt(httpResp.Header().Get(models.UnwrapCount), 10, 0) - if !isStatsEnabled && httpResp.Code == http.StatusOK && respStatus == models.UnwrapSucessStatus { + if httpResp.Code == http.StatusOK && unwrapStatus == models.UnwrapSucessStatus { respBody := httpResp.Body.Bytes() bid.Bid.AdM = string(respBody) } - glog.V(models.LogLevelDebug).Infof("[VAST_UNWRAPPER] pubid:[%v] bidder:[%v] impid:[%v] bidid:[%v] status_code:[%v] wrapper_cnt:[%v] httpRespCode= [%v] statsEnabled:[%v]", - accountID, bidder, bid.Bid.ImpID, bid.Bid.ID, respStatus, wrapperCnt, httpResp.Code, isStatsEnabled) + glog.V(models.LogLevelDebug).Infof("[VAST_UNWRAPPER] pubid:[%v] bidder:[%v] impid:[%v] bidid:[%v] status_code:[%v] wrapper_cnt:[%v] httpRespCode= [%v]", + accountID, bidder, bid.Bid.ImpID, bid.Bid.ID, unwrapStatus, wrapperCnt, httpResp.Code) + return unwrapStatus } diff --git a/modules/pubmatic/openwrap/unwrap/unwrap_test.go b/modules/pubmatic/openwrap/unwrap/unwrap_test.go index a7f3e7be050..763ba071c8c 100644 --- a/modules/pubmatic/openwrap/unwrap/unwrap_test.go +++ b/modules/pubmatic/openwrap/unwrap/unwrap_test.go @@ -8,7 +8,6 @@ import ( "github.com/golang/mock/gomock" "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/adapters" - metrics "github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/metrics" mock_metrics "github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/metrics/mock" "github.com/stretchr/testify/assert" ) @@ -18,35 +17,32 @@ var invalidVastXMLAdM = "PubMaticAcudeo CompatibleVAST 2.0 Instream Test 1VAST 2.0 Instream Test 1https://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&er=[ERRORCODE]https://track.dsptracker.com?p=1234&er=[ERRORCODE]https://aktrack.pubmatic.com/AdServer/AdDisplayTrackerServlet?operId=1&pubId=64195&siteId=47105&adId=1405154&adType=13&adServerId=243&kefact=1.000000&kaxefact=1.000000&kadNetFrequecy=0&kadwidth=0&kadheight=0&kadsizeid=97&kltstamp=1536933242&indirectAdId=0&adServerOptimizerId=2&ranreq=0.05969169352174375&kpbmtpfact=11.000000&dcId=1&tldId=0&passback=0&svr=ktk57&ekefact=er2bW2sDAwCra06ACbsIQySn5nqBtYsTl8fy5lupAexh37D_&ekaxefact=er2bW4EDAwB_LQpJJ23Fq0DcNC-NSAFXdpSQC8XBk_S33_Fa&ekpbmtpfact=er2bW5MDAwDJHdBnLBt5IrRuh7x0oqp_tjIALv_VvSQDAl6R&crID=m:1_x:3_y:3_p:11_va:3&lpu=ae.com&ucrid=678722001014421372&campaignId=16774&creativeId=0&pctr=0.000000&wDSPByrId=511&wDspId=27&wbId=0&wrId=0&wAdvID=3170&isRTB=1&rtbId=EBCA079F-8D7C-45B8-B733-92951F670AA1&imprId=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&oid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&pageURL=http%253A%252F%252Fowsdk-stagingams.pubmatic.com%253A8443%252Fvast-validator%252F%2523&sec=1&pmc=1https://DspImpressionTracker.com/https://mytracking.com/linear/closehttps://mytracking.com/linear/skiphttps://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=1https://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=2https://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=3https://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=4https://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=5https://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=600:00:04https://www.automationtester.inhttps://aktrack.pubmatic.com/track?operId=7&p=64195&s=47105&a=1405154&wa=243&ts=1536933242&wc=16774&crId=m:1_x:3_y:3_p:11_va:3&ucrid=678722001014421372&impid=24D9FEDA-C97D-4DF7-B747-BD3CFF5AC7B5&advertiser_id=3170&ecpm=1.000000&e=99https://stagingams.pubmatic.com:8443/openwrap/media/pubmatic.mp4https://stagingams.pubmatic.com:8443/openwrap/media/pubmatic.mp4https://stagingams.pubmatic.com:8443/openwrap/media/mp4-sample-3.mp4" func TestUnwrap_Unwrap(t *testing.T) { - ctrl := gomock.NewController(t) defer ctrl.Finish() mockMetricsEngine := mock_metrics.NewMockMetricsEngine(ctrl) type fields struct { - endpoint string - defaultTime int - metricEngine metrics.MetricsEngine - unwrapRequest http.HandlerFunc + endpoint string } type args struct { - accountID string - bidder string - bid *adapters.TypedBid - userAgent string - ip string - isStatsEnabled bool + accountID string + bidder string + bid *adapters.TypedBid + userAgent string + ip string } tests := []struct { - name string - fields fields - args args - setup func() - mockHandler http.HandlerFunc - expectedAdm string + name string + fields fields + args args + setup func() + mockHandler http.HandlerFunc + expectedAdm string + expectedUnwrapStatus string }{ { - name: "Stats enabled only", + name: "Unwrap enabled with valid adm", + fields: fields{endpoint: "http://localhost:8001/unwrap"}, args: args{ accountID: "5890", bidder: "pubmatic", @@ -55,9 +51,8 @@ func TestUnwrap_Unwrap(t *testing.T) { AdM: vastXMLAdM, }, }, - userAgent: "UA", - ip: "10.12.13.14", - isStatsEnabled: true, + userAgent: "UA", + ip: "10.12.13.14", }, setup: func() { mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "0") @@ -69,40 +64,39 @@ func TestUnwrap_Unwrap(t *testing.T) { w.Header().Add("unwrap-status", "0") w.Header().Add("unwrap-count", "1") w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(vastXMLAdM)) + _, _ = w.Write([]byte(inlineXMLAdM)) }), - expectedAdm: vastXMLAdM, + expectedAdm: inlineXMLAdM, + expectedUnwrapStatus: "0", }, { - name: "Unwrap enabled with valid adm", + name: "Unwrap enabled with invalid adm", + fields: fields{endpoint: "http://localhost:8001/unwrap"}, args: args{ accountID: "5890", bidder: "pubmatic", bid: &adapters.TypedBid{ Bid: &openrtb2.Bid{ - AdM: vastXMLAdM, + AdM: invalidVastXMLAdM, }, }, - userAgent: "UA", - ip: "10.12.13.14", - isStatsEnabled: false, + userAgent: "UA", + ip: "10.12.13.14", }, setup: func() { - mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "0") - mockMetricsEngine.EXPECT().RecordUnwrapWrapperCount("5890", "pubmatic", "1") + mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "1") mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()) - mockMetricsEngine.EXPECT().RecordUnwrapRespTime("5890", "1", gomock.Any()) }, mockHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Add("unwrap-status", "0") - w.Header().Add("unwrap-count", "1") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(inlineXMLAdM)) + w.Header().Add("unwrap-status", "1") + w.WriteHeader(http.StatusNoContent) }), - expectedAdm: inlineXMLAdM, + expectedAdm: invalidVastXMLAdM, + expectedUnwrapStatus: "1", }, { - name: "Unwrap enabled with invalid adm", + name: "Error while forming the HTTPRequest for unwrap process", + fields: fields{endpoint: ":"}, args: args{ accountID: "5890", bidder: "pubmatic", @@ -111,33 +105,29 @@ func TestUnwrap_Unwrap(t *testing.T) { AdM: invalidVastXMLAdM, }, }, - userAgent: "UA", - ip: "10.12.13.14", - isStatsEnabled: false, + userAgent: "UA", + ip: "10.12.13.14", }, setup: func() { - mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "1") + mockMetricsEngine.EXPECT().RecordUnwrapRequestStatus("5890", "pubmatic", "") mockMetricsEngine.EXPECT().RecordUnwrapRequestTime("5890", "pubmatic", gomock.Any()) }, - mockHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Add("unwrap-status", "1") - w.WriteHeader(http.StatusNoContent) - }), - expectedAdm: invalidVastXMLAdM, + mockHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {}), + expectedAdm: invalidVastXMLAdM, + expectedUnwrapStatus: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.setup != nil { tt.setup() } - uw := NewUnwrap("http://localhost:8001/unwrap", 200, tt.mockHandler, mockMetricsEngine) - uw.Unwrap(tt.args.accountID, tt.args.bidder, tt.args.bid, tt.args.userAgent, tt.args.ip, tt.args.isStatsEnabled) - if !tt.args.isStatsEnabled && strings.Compare(tt.args.bid.Bid.AdM, tt.expectedAdm) != 0 { + uw := NewUnwrap(tt.fields.endpoint, 200, tt.mockHandler, mockMetricsEngine) + unwrapStatus := uw.Unwrap(tt.args.bid, tt.args.accountID, tt.args.bidder, tt.args.userAgent, tt.args.ip) + if strings.Compare(tt.args.bid.Bid.AdM, tt.expectedAdm) != 0 { assert.Equal(t, inlineXMLAdM, tt.args.bid.Bid.AdM, "AdM is not updated correctly after unwrap ") - } + assert.Equal(t, tt.expectedUnwrapStatus, unwrapStatus, "mismatched unwrap status") }) } }