From b12cfb5edcb38742e1c70e4945fd41b0db7027e6 Mon Sep 17 00:00:00 2001 From: Pubmatic-Supriya-Patil <131644110+Pubmatic-Supriya-Patil@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:37:16 +0530 Subject: [PATCH] UOE-11332: validate bids as per imp.video.protocols for all the bidders (#963) --- errortypes/code.go | 1 + exchange/exchange.go | 5 + exchange/exchange_ow.go | 53 ++ exchange/exchange_ow_test.go | 651 ++++++++++++++++++ exchange/exchange_test.go | 156 +++++ .../pubmatic/openwrap/beforevalidationhook.go | 13 +- .../openwrap/beforevalidationhook_test.go | 517 ++++++++++++++ modules/pubmatic/openwrap/models/constants.go | 1 + modules/pubmatic/openwrap/models/nbr/codes.go | 13 +- modules/pubmatic/openwrap/util.go | 15 + modules/pubmatic/openwrap/util_test.go | 83 +++ openrtb_ext/openwrap.go | 1 + 12 files changed, 1502 insertions(+), 7 deletions(-) diff --git a/errortypes/code.go b/errortypes/code.go index b6f4aa33728..7a4017f36dc 100644 --- a/errortypes/code.go +++ b/errortypes/code.go @@ -41,6 +41,7 @@ const ( SecCookieDeprecationLenWarningCode SecBrowsingTopicsWarningCode AdpodPostFilteringWarningCode + InvalidVastVersionWarningCode ) // Coder provides an error or warning code with severity. diff --git a/exchange/exchange.go b/exchange/exchange.go index ccd5fa5a65f..3aba94ab8c7 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -426,6 +426,11 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog recordBids(ctx, e.me, r.PubID, adapterBids) recordVastVersion(e.me, adapterBids) + if requestExtPrebid.StrictVastMode { + validationErrs := filterBidsByVastVersion(adapterBids, &seatNonBid) + errs = append(errs, validationErrs...) + } + if e.priceFloorEnabled { var rejectedBids []*entities.PbsOrtbSeatBid var enforceErrs []error diff --git a/exchange/exchange_ow.go b/exchange/exchange_ow.go index fc9c52e9d72..f8b1b449983 100644 --- a/exchange/exchange_ow.go +++ b/exchange/exchange_ow.go @@ -7,6 +7,7 @@ import ( "fmt" "net/url" "regexp" + "strconv" "strings" "github.com/golang/glog" @@ -14,9 +15,11 @@ import ( "github.com/prebid/prebid-server/v2/adapters" "github.com/prebid/prebid-server/v2/config" "github.com/prebid/prebid-server/v2/currency" + "github.com/prebid/prebid-server/v2/errortypes" "github.com/prebid/prebid-server/v2/exchange/entities" "github.com/prebid/prebid-server/v2/metrics" pubmaticstats "github.com/prebid/prebid-server/v2/metrics/pubmatic_stats" + "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/ortb" "github.com/prebid/prebid-server/v2/util/jsonutil" @@ -36,6 +39,11 @@ const ( VASTTypeInLineEndTag = "" ) +var validVastVersions = map[int]bool{ + 3: true, + 4: true, +} + // VASTTagType describes the allowed values for VASTTagType type VASTTagType string @@ -378,3 +386,48 @@ func (e exchange) updateSeatNonBidsPriceThreshold(seatNonBids *openrtb_ext.NonBi } } } + +func updateSeatNonBidsInvalidVastVersion(seatNonBids *openrtb_ext.NonBidCollection, seat string, rejectedBids []*entities.PbsOrtbBid) { + for _, pbsRejBid := range rejectedBids { + nonBidParams := entities.GetNonBidParamsFromPbsOrtbBid(pbsRejBid, seat) + nonBidParams.NonBidReason = int(nbr.LossBidLostInVastVersionValidation) + seatNonBids.AddBid(openrtb_ext.NewNonBid(nonBidParams), seat) + } +} + +func filterBidsByVastVersion(adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, seatNonBid *openrtb_ext.NonBidCollection) []error { + errs := []error{} + for _, seatBid := range adapterBids { + rejectedBid := []*entities.PbsOrtbBid{} + validBids := make([]*entities.PbsOrtbBid, 0, len(seatBid.Bids)) + for _, pbsBid := range seatBid.Bids { + if pbsBid.BidType == openrtb_ext.BidTypeVideo && pbsBid.Bid.AdM != "" { + isValid, vastVersion := validateVastVersion(pbsBid.Bid.AdM) + if !isValid { + errs = append(errs, &errortypes.Warning{ + Message: fmt.Sprintf("%s Bid %s was filtered for Imp %s with Vast Version %s: Incompatible with GAM unwinding requirements", seatBid.Seat, pbsBid.Bid.ID, pbsBid.Bid.ImpID, vastVersion), + WarningCode: errortypes.InvalidVastVersionWarningCode, + }) + rejectedBid = append(rejectedBid, pbsBid) + continue + } + } + validBids = append(validBids, pbsBid) + } + updateSeatNonBidsInvalidVastVersion(seatNonBid, seatBid.Seat, rejectedBid) + seatBid.Bids = validBids + } + return errs +} + +func validateVastVersion(adM string) (bool, string) { + matches := vastVersionRegex.FindStringSubmatch(adM) + if len(matches) != 2 { + return false, "" + } + vastVersionFloat, err := strconv.ParseFloat(matches[1], 64) + if err != nil { + return false, matches[1] + } + return validVastVersions[int(vastVersionFloat)], matches[1] +} diff --git a/exchange/exchange_ow_test.go b/exchange/exchange_ow_test.go index 6a4337aad3c..2c3243bbbf4 100644 --- a/exchange/exchange_ow_test.go +++ b/exchange/exchange_ow_test.go @@ -15,9 +15,11 @@ import ( "github.com/prebid/prebid-server/v2/adapters" "github.com/prebid/prebid-server/v2/adapters/vastbidder" "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" "github.com/prebid/prebid-server/v2/exchange/entities" "github.com/prebid/prebid-server/v2/metrics" metricsConf "github.com/prebid/prebid-server/v2/metrics/config" + "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/util/ptrutil" "github.com/stretchr/testify/assert" @@ -1875,3 +1877,652 @@ func TestRecordFastXMLMetrics(t *testing.T) { }) } } + +func TestFilterBidsByVastVersion(t *testing.T) { + type args struct { + adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid + seatNonBid *openrtb_ext.NonBidCollection + } + tests := []struct { + name string + args args + want map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid + errs []error + }{ + { + name: "valid_vast_version_banner", + args: args{ + adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeBanner, + }, + }, + Seat: "bidder1", + }, + }, + seatNonBid: &openrtb_ext.NonBidCollection{}, + }, + want: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeBanner, + }, + }, + Seat: "bidder1", + }, + }, + errs: []error{}, + }, + { + name: "invalid_vast_version_banner", + args: args{ + adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeBanner, + }, + }, + Seat: "bidder1", + }, + }, + seatNonBid: &openrtb_ext.NonBidCollection{}, + }, + want: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeBanner, + }, + }, + Seat: "bidder1", + }, + }, + errs: []error{}, + }, + { + name: "valid_vast_version_video", + args: args{ + adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder1", + }, + }, + seatNonBid: &openrtb_ext.NonBidCollection{}, + }, + want: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder1", + }, + }, + errs: []error{}, + }, + { + name: "multiple_bids_valid_vast_version", + args: args{ + adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + { + Bid: &openrtb2.Bid{ + ID: "bid2", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder1", + }, + "bidder2": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + { + Bid: &openrtb2.Bid{ + ID: "bid2", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder2", + }, + }, + seatNonBid: &openrtb_ext.NonBidCollection{}, + }, + want: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + { + Bid: &openrtb2.Bid{ + ID: "bid2", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder1", + }, + "bidder2": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + { + Bid: &openrtb2.Bid{ + ID: "bid2", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder2", + }, + }, + errs: []error{}, + }, + { + name: "invalid_vast_version", + args: args{ + adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder1", + }, + }, + seatNonBid: &openrtb_ext.NonBidCollection{}, + }, + want: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{}, + Seat: "bidder1", + }, + }, + errs: []error{ + &errortypes.Warning{ + Message: "bidder1 Bid bid1 was filtered for Imp with Vast Version 2.0: Incompatible with GAM unwinding requirements", + WarningCode: errortypes.InvalidVastVersionWarningCode, + }, + }, + }, + { + name: "multiple_bids_invalid_vast_version", + args: args{ + adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + { + Bid: &openrtb2.Bid{ + ID: "bid2", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder1", + }, + "bidder2": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + { + Bid: &openrtb2.Bid{ + ID: "bid2", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder2", + }, + }, + seatNonBid: &openrtb_ext.NonBidCollection{}, + }, + want: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{}, + Seat: "bidder1", + }, + "bidder2": { + Bids: []*entities.PbsOrtbBid{}, + Seat: "bidder2", + }, + }, + errs: []error{ + &errortypes.Warning{ + Message: "bidder1 Bid bid1 was filtered for Imp with Vast Version 0: Incompatible with GAM unwinding requirements", + WarningCode: errortypes.InvalidVastVersionWarningCode, + }, + &errortypes.Warning{ + Message: "bidder1 Bid bid2 was filtered for Imp with Vast Version 1.0: Incompatible with GAM unwinding requirements", + WarningCode: errortypes.InvalidVastVersionWarningCode, + }, + &errortypes.Warning{ + Message: "bidder2 Bid bid1 was filtered for Imp with Vast Version 2.0: Incompatible with GAM unwinding requirements", + WarningCode: errortypes.InvalidVastVersionWarningCode, + }, + &errortypes.Warning{ + Message: "bidder2 Bid bid2 was filtered for Imp with Vast Version 1.0: Incompatible with GAM unwinding requirements", + WarningCode: errortypes.InvalidVastVersionWarningCode, + }, + }, + }, + { + name: "multiple_bids_valid_invalid_vast_version", + args: args{ + adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + { + Bid: &openrtb2.Bid{ + ID: "bid2", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder1", + }, + "bidder2": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + { + Bid: &openrtb2.Bid{ + ID: "bid2", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder2", + }, + }, + seatNonBid: &openrtb_ext.NonBidCollection{}, + }, + want: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder1", + }, + "bidder2": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid2", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder2", + }, + }, + errs: []error{ + &errortypes.Warning{ + Message: "bidder1 Bid bid2 was filtered for Imp with Vast Version 1.0: Incompatible with GAM unwinding requirements", + WarningCode: errortypes.InvalidVastVersionWarningCode, + }, + &errortypes.Warning{ + Message: "bidder2 Bid bid1 was filtered for Imp with Vast Version 2.0: Incompatible with GAM unwinding requirements", + WarningCode: errortypes.InvalidVastVersionWarningCode, + }, + }, + }, + { + name: "non_video_bid", + args: args{ + adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeBanner, + }, + }, + Seat: "bidder1", + }, + }, + seatNonBid: &openrtb_ext.NonBidCollection{}, + }, + want: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeBanner, + }, + }, + Seat: "bidder1", + }, + }, + errs: []error{}, + }, + { + name: "empty_adm", + args: args{ + adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: "", + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder1", + }, + }, + seatNonBid: &openrtb_ext.NonBidCollection{}, + }, + want: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: "", + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder1", + }, + }, + errs: []error{}, + }, + { + name: "invalid_vast_version_format", + args: args{ + adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{ + { + Bid: &openrtb2.Bid{ + ID: "bid1", + AdM: ``, + }, + BidType: openrtb_ext.BidTypeVideo, + }, + }, + Seat: "bidder1", + }, + }, + seatNonBid: &openrtb_ext.NonBidCollection{}, + }, + want: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ + "bidder1": { + Bids: []*entities.PbsOrtbBid{}, + Seat: "bidder1", + }, + }, + errs: []error{ + &errortypes.Warning{ + Message: "bidder1 Bid bid1 was filtered for Imp with Vast Version : Incompatible with GAM unwinding requirements", + WarningCode: errortypes.InvalidVastVersionWarningCode, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errs := filterBidsByVastVersion(tt.args.adapterBids, tt.args.seatNonBid) + want := mapToSlice(tt.want) + got := mapToSlice(tt.args.adapterBids) + assert.ElementsMatch(t, want, got) + assert.ElementsMatch(t, tt.errs, errs) + }) + } +} + +func mapToSlice(bidMap map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) []*entities.PbsOrtbSeatBid { + var result []*entities.PbsOrtbSeatBid + for _, bid := range bidMap { + result = append(result, bid) + } + return result +} + +func TestValidateVastVersion(t *testing.T) { + tests := []struct { + name string + adM string + expectedValid bool + expectedVersion string + }{ + { + name: "Valid VAST version 3", + adM: ``, + expectedValid: true, + expectedVersion: "3.0", + }, + { + name: "Valid VAST version 4", + adM: ``, + expectedValid: true, + expectedVersion: "4.0", + }, + { + name: "Invalid VAST version 2", + adM: ``, + expectedValid: false, + expectedVersion: "2.0", + }, + { + name: "Invalid VAST version 5", + adM: ``, + expectedValid: false, + expectedVersion: "5.0", + }, + { + name: "No VAST version", + adM: ``, + expectedValid: false, + expectedVersion: "", + }, + { + name: "Malformed VAST tag", + adM: `"}, + }, + }, + }}, + expected: testResults{ + bidFloorCur: "USD", + resolvedReq: `{"id":"some-request-id","imp":[{"id":"some-impression-id","bidfloor":15,"bidfloorcur":"USD","ext":{"prebid":{"bidder":{"appnexus":{"placementId":1}}}}}],"site":{"domain":"www.website.com","page":"prebid.org","ext":{"amp":0}},"test":1,"cur":["USD"],"ext":{"prebid":{"strictvastmode":true}}}`, + }, + }, + { + desc: "requestext_has_strict_vast_mode_vast_version_2", + req: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{{ + ID: "some-impression-id", + BidFloor: 15, + BidFloorCur: "USD", + Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus": {"placementId": 1}}}}`), + }}, + Site: &openrtb2.Site{ + Page: "prebid.org", + Ext: json.RawMessage(`{"amp":0}`), + Domain: "www.website.com", + }, + Test: 1, + Cur: []string{"USD"}, + Ext: json.RawMessage(`{"prebid":{"strictvastmode":true}}`), + }}, + bidderImpl: &goodSingleBidder{ + httpRequest: &adapters.RequestData{ + Method: "POST", + Uri: server.URL, + Body: []byte(`{"key":"val"}`), + Headers: http.Header{}, + }, + bidResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + BidType: openrtb_ext.BidTypeVideo, + Bid: &openrtb2.Bid{ID: "some-bid-id", AdM: ""}, + }, + }, + }}, + expected: testResults{ + bidFloorCur: "USD", + resolvedReq: `{"id":"some-request-id","imp":[{"id":"some-impression-id","bidfloor":15,"bidfloorcur":"USD","ext":{"prebid":{"bidder":{"appnexus":{"placementId":1}}}}}],"site":{"domain":"www.website.com","page":"prebid.org","ext":{"amp":0}},"test":1,"cur":["USD"],"ext":{"prebid":{"strictvastmode":true}}}`, + errMessage: "appnexus Bid some-bid-id was filtered for Imp with Vast Version 2.0: Incompatible with GAM unwinding requirements", + errCode: 10014, + }, + }, + } + + for _, test := range testCases { + auctionRequest := &AuctionRequest{ + BidRequestWrapper: test.req, + Account: config.Account{DebugAllow: true}, + UserSyncs: &emptyUsersync{}, + HookExecutor: &hookexecution.EmptyHookExecutor{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), + } + + e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{ + openrtb_ext.BidderAppnexus: AdaptBidder(test.bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, ""), + } + outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{}) + assert.Equal(t, test.expected.err, err, "Error") + actualExt := &openrtb_ext.ExtBidResponse{} + _ = jsonutil.UnmarshalValid(outBidResponse.Ext, actualExt) + if test.expected.errMessage != "" && actualExt.Warnings != nil { + assert.NotNil(t, actualExt.Warnings["prebid"], "prebid warning should be present") + assert.Equal(t, actualExt.Warnings["prebid"][0].Message, test.expected.errMessage, "Warning Message") + assert.Equal(t, actualExt.Warnings["prebid"][0].Code, test.expected.errCode, "Warning Code") + } + actualResolvedRequest, _, _, _ := jsonparser.Get(outBidResponse.Ext, "debug", "resolvedrequest") + assert.JSONEq(t, test.expected.resolvedReq, string(actualResolvedRequest), "Resolved request is incorrect") + } +} func TestReturnCreativeEndToEnd(t *testing.T) { sampleAd := "" diff --git a/modules/pubmatic/openwrap/beforevalidationhook.go b/modules/pubmatic/openwrap/beforevalidationhook.go index c500d5970a8..b16d0e8814e 100644 --- a/modules/pubmatic/openwrap/beforevalidationhook.go +++ b/modules/pubmatic/openwrap/beforevalidationhook.go @@ -719,7 +719,18 @@ func (m *OpenWrap) applyProfileChanges(rctx models.RequestCtx, bidRequest *openr bidRequest.App.StoreURL = rctx.AppLovinMax.AppStoreUrl } } - + strictVastMode := models.GetVersionLevelPropertyFromPartnerConfig(rctx.PartnerConfigMap, models.StrictVastModeKey) == models.Enabled + if strictVastMode { + if rctx.NewReqExt == nil { + rctx.NewReqExt = &models.RequestExt{} + } + rctx.NewReqExt.Prebid.StrictVastMode = strictVastMode + for i := 0; i < len(bidRequest.Imp); i++ { + if bidRequest.Imp[i].Video != nil { + bidRequest.Imp[i].Video.Protocols = UpdateImpProtocols(bidRequest.Imp[i].Video.Protocols) + } + } + } if cur, ok := rctx.PartnerConfigMap[models.VersionLevelConfigID][models.AdServerCurrency]; ok { bidRequest.Cur = append(bidRequest.Cur, cur) } diff --git a/modules/pubmatic/openwrap/beforevalidationhook_test.go b/modules/pubmatic/openwrap/beforevalidationhook_test.go index 4ea909b1918..878768573e0 100644 --- a/modules/pubmatic/openwrap/beforevalidationhook_test.go +++ b/modules/pubmatic/openwrap/beforevalidationhook_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "net/http" "sort" "testing" @@ -1840,6 +1841,522 @@ func TestOpenWrapApplyProfileChanges(t *testing.T) { }, wantErr: false, }, + { + name: "GAM_Unwinding_Enabled", + args: args{ + rctx: models.RequestCtx{ + IsTestRequest: 1, + PartnerConfigMap: map[int]map[string]string{ + -1: { + models.AdServerCurrency: "USD", + models.SChainDBKey: "1", + models.StrictVastModeKey: models.Enabled, + }, + }, + TMax: 500, + IP: "127.0.0.1", + Platform: models.PLATFORM_APP, + KADUSERCookie: &http.Cookie{ + Name: "KADUSERCOOKIE", + Value: "123456789", + }, + }, + bidRequest: &openrtb2.BidRequest{ + ID: "testID", + Test: 1, + Cur: []string{"EUR"}, + TMax: 500, + Source: &openrtb2.Source{ + TID: "testID", + }, + Imp: []openrtb2.Imp{ + { + ID: "testImp1", + Video: &openrtb2.Video{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](300), + Plcmt: 1, + Protocols: []adcom1.MediaCreativeSubtype{1, 2, 3}, + }, + }, + }, + Device: &openrtb2.Device{ + IP: "127.0.0.1", + Language: "en", + DeviceType: 1, + }, + WLang: []string{"en", "hi"}, + User: &openrtb2.User{ + CustomData: "123456789", + Ext: json.RawMessage(`{"eids":[{"source":"uidapi.com","uids":[{"id":"UID2:"},{"id":""}]},{"source":"euid.eu","uids":[{"id":""}]},{"source":"liveramp.com","uids":[{"id":"IDL:"}]}]}`), + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: "1010", + }, + Content: &openrtb2.Content{ + Language: "en", + }, + }, + }, + }, + want: &openrtb2.BidRequest{ + ID: "testID", + Test: 1, + Cur: []string{"EUR", "USD"}, + TMax: 500, + Source: &openrtb2.Source{ + TID: "testID", + }, + Imp: []openrtb2.Imp{ + { + ID: "testImp1", + Video: &openrtb2.Video{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](300), + Plcmt: 1, + Protocols: []adcom1.MediaCreativeSubtype{1, 2, 3, 6, 7, 8}, + }, + }, + }, + Device: &openrtb2.Device{ + IP: "127.0.0.1", + Language: "en", + DeviceType: 1, + }, + WLang: []string{"en", "hi"}, + User: &openrtb2.User{ + CustomData: "123456789", + Ext: json.RawMessage(`{}`), + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: "1010", + }, + Content: &openrtb2.Content{ + Language: "en", + }, + }, + Ext: json.RawMessage(`{"prebid":{"strictvastmode":true}}`), + }, + wantErr: false, + }, + { + name: "GAM_Unwinding_Enabled_Multi_Imp", + args: args{ + rctx: models.RequestCtx{ + IsTestRequest: 1, + PartnerConfigMap: map[int]map[string]string{ + -1: { + models.AdServerCurrency: "USD", + models.SChainDBKey: "1", + models.StrictVastModeKey: models.Enabled, + }, + }, + TMax: 500, + IP: "127.0.0.1", + Platform: models.PLATFORM_APP, + KADUSERCookie: &http.Cookie{ + Name: "KADUSERCOOKIE", + Value: "123456789", + }, + }, + bidRequest: &openrtb2.BidRequest{ + ID: "testID", + Test: 1, + Cur: []string{"EUR"}, + TMax: 500, + Source: &openrtb2.Source{ + TID: "testID", + }, + Imp: []openrtb2.Imp{ + { + ID: "testImp1", + Video: &openrtb2.Video{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](300), + Plcmt: 1, + Protocols: []adcom1.MediaCreativeSubtype{1, 2, 3}, + }, + }, + { + ID: "testImp2", + Video: &openrtb2.Video{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](300), + Plcmt: 1, + Protocols: []adcom1.MediaCreativeSubtype{1}, + }, + }, + }, + Device: &openrtb2.Device{ + IP: "127.0.0.1", + Language: "en", + DeviceType: 1, + }, + WLang: []string{"en", "hi"}, + User: &openrtb2.User{ + CustomData: "123456789", + Ext: json.RawMessage(`{"eids":[{"source":"uidapi.com","uids":[{"id":"UID2:"},{"id":""}]},{"source":"euid.eu","uids":[{"id":""}]},{"source":"liveramp.com","uids":[{"id":"IDL:"}]}]}`), + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: "1010", + }, + Content: &openrtb2.Content{ + Language: "en", + }, + }, + }, + }, + want: &openrtb2.BidRequest{ + ID: "testID", + Test: 1, + Cur: []string{"EUR", "USD"}, + TMax: 500, + Source: &openrtb2.Source{ + TID: "testID", + }, + Imp: []openrtb2.Imp{ + { + ID: "testImp1", + Video: &openrtb2.Video{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](300), + Plcmt: 1, + Protocols: []adcom1.MediaCreativeSubtype{1, 2, 3, 6, 7, 8}, + }, + }, + { + ID: "testImp2", + Video: &openrtb2.Video{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](300), + Plcmt: 1, + Protocols: []adcom1.MediaCreativeSubtype{1, 3, 6, 7, 8}, + }, + }, + }, + Device: &openrtb2.Device{ + IP: "127.0.0.1", + Language: "en", + DeviceType: 1, + }, + WLang: []string{"en", "hi"}, + User: &openrtb2.User{ + CustomData: "123456789", + Ext: json.RawMessage(`{}`), + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: "1010", + }, + Content: &openrtb2.Content{ + Language: "en", + }, + }, + Ext: json.RawMessage(`{"prebid":{"strictvastmode":true}}`), + }, + wantErr: false, + }, + { + name: "GAM_Unwinding_Enabled_Empty_Protocols", + args: args{ + rctx: models.RequestCtx{ + IsTestRequest: 1, + PartnerConfigMap: map[int]map[string]string{ + -1: { + models.AdServerCurrency: "USD", + models.SChainDBKey: "1", + models.StrictVastModeKey: models.Enabled, + }, + }, + TMax: 500, + IP: "127.0.0.1", + Platform: models.PLATFORM_APP, + KADUSERCookie: &http.Cookie{ + Name: "KADUSERCOOKIE", + Value: "123456789", + }, + }, + bidRequest: &openrtb2.BidRequest{ + ID: "testID", + Test: 1, + Cur: []string{"EUR"}, + TMax: 500, + Source: &openrtb2.Source{ + TID: "testID", + }, + Imp: []openrtb2.Imp{ + { + ID: "testImp1", + Video: &openrtb2.Video{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](300), + Plcmt: 1, + Protocols: []adcom1.MediaCreativeSubtype{}, + }, + }, + }, + Device: &openrtb2.Device{ + IP: "127.0.0.1", + Language: "en", + DeviceType: 1, + }, + WLang: []string{"en", "hi"}, + User: &openrtb2.User{ + CustomData: "123456789", + Ext: json.RawMessage(`{"eids":[{"source":"uidapi.com","uids":[{"id":"UID2:"},{"id":""}]},{"source":"euid.eu","uids":[{"id":""}]},{"source":"liveramp.com","uids":[{"id":"IDL:"}]}]}`), + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: "1010", + }, + Content: &openrtb2.Content{ + Language: "en", + }, + }, + }, + }, + want: &openrtb2.BidRequest{ + ID: "testID", + Test: 1, + Cur: []string{"EUR", "USD"}, + TMax: 500, + Source: &openrtb2.Source{ + TID: "testID", + }, + Imp: []openrtb2.Imp{ + { + ID: "testImp1", + Video: &openrtb2.Video{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](300), + Plcmt: 1, + Protocols: []adcom1.MediaCreativeSubtype{3, 6, 7, 8}, + }, + }, + }, + Device: &openrtb2.Device{ + IP: "127.0.0.1", + Language: "en", + DeviceType: 1, + }, + WLang: []string{"en", "hi"}, + User: &openrtb2.User{ + CustomData: "123456789", + Ext: json.RawMessage(`{}`), + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: "1010", + }, + Content: &openrtb2.Content{ + Language: "en", + }, + }, + Ext: json.RawMessage(`{"prebid":{"strictvastmode":true}}`), + }, + wantErr: false, + }, + { + name: "GAM_Unwinding_Enabled_Protocols_Not_Present", + args: args{ + rctx: models.RequestCtx{ + IsTestRequest: 1, + PartnerConfigMap: map[int]map[string]string{ + -1: { + models.AdServerCurrency: "USD", + models.SChainDBKey: "1", + models.StrictVastModeKey: models.Enabled, + }, + }, + TMax: 500, + IP: "127.0.0.1", + Platform: models.PLATFORM_APP, + KADUSERCookie: &http.Cookie{ + Name: "KADUSERCOOKIE", + Value: "123456789", + }, + }, + bidRequest: &openrtb2.BidRequest{ + ID: "testID", + Test: 1, + Cur: []string{"EUR"}, + TMax: 500, + Source: &openrtb2.Source{ + TID: "testID", + }, + Imp: []openrtb2.Imp{ + { + ID: "testImp1", + Video: &openrtb2.Video{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](300), + Plcmt: 1, + }, + }, + }, + Device: &openrtb2.Device{ + IP: "127.0.0.1", + Language: "en", + DeviceType: 1, + }, + WLang: []string{"en", "hi"}, + User: &openrtb2.User{ + CustomData: "123456789", + Ext: json.RawMessage(`{"eids":[{"source":"uidapi.com","uids":[{"id":"UID2:"},{"id":""}]},{"source":"euid.eu","uids":[{"id":""}]},{"source":"liveramp.com","uids":[{"id":"IDL:"}]}]}`), + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: "1010", + }, + Content: &openrtb2.Content{ + Language: "en", + }, + }, + }, + }, + want: &openrtb2.BidRequest{ + ID: "testID", + Test: 1, + Cur: []string{"EUR", "USD"}, + TMax: 500, + Source: &openrtb2.Source{ + TID: "testID", + }, + Imp: []openrtb2.Imp{ + { + ID: "testImp1", + Video: &openrtb2.Video{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](300), + Plcmt: 1, + Protocols: []adcom1.MediaCreativeSubtype{3, 6, 7, 8}, + }, + }, + }, + Device: &openrtb2.Device{ + IP: "127.0.0.1", + Language: "en", + DeviceType: 1, + }, + WLang: []string{"en", "hi"}, + User: &openrtb2.User{ + CustomData: "123456789", + Ext: json.RawMessage(`{}`), + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: "1010", + }, + Content: &openrtb2.Content{ + Language: "en", + }, + }, + Ext: json.RawMessage(`{"prebid":{"strictvastmode":true}}`), + }, + wantErr: false, + }, + { + name: "GAM_Unwinding_Disabled", + args: args{ + rctx: models.RequestCtx{ + IsTestRequest: 1, + PartnerConfigMap: map[int]map[string]string{ + -1: { + models.AdServerCurrency: "USD", + models.SChainDBKey: "1", + models.StrictVastModeKey: "0", + }, + }, + TMax: 500, + IP: "127.0.0.1", + Platform: models.PLATFORM_APP, + KADUSERCookie: &http.Cookie{ + Name: "KADUSERCOOKIE", + Value: "123456789", + }, + }, + bidRequest: &openrtb2.BidRequest{ + ID: "testID", + Test: 1, + Cur: []string{"EUR"}, + TMax: 500, + Source: &openrtb2.Source{ + TID: "testID", + }, + Imp: []openrtb2.Imp{ + { + ID: "testImp1", + Video: &openrtb2.Video{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](300), + Plcmt: 1, + Protocols: []adcom1.MediaCreativeSubtype{1, 2, 3}, + }, + }, + }, + Device: &openrtb2.Device{ + IP: "127.0.0.1", + Language: "en", + DeviceType: 1, + }, + WLang: []string{"en", "hi"}, + User: &openrtb2.User{ + CustomData: "123456789", + Ext: json.RawMessage(`{"eids":[{"source":"uidapi.com","uids":[{"id":"UID2:"},{"id":""}]},{"source":"euid.eu","uids":[{"id":""}]},{"source":"liveramp.com","uids":[{"id":"IDL:"}]}]}`), + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: "1010", + }, + Content: &openrtb2.Content{ + Language: "en", + }, + }, + }, + }, + want: &openrtb2.BidRequest{ + ID: "testID", + Test: 1, + Cur: []string{"EUR", "USD"}, + TMax: 500, + Source: &openrtb2.Source{ + TID: "testID", + }, + Imp: []openrtb2.Imp{ + { + ID: "testImp1", + Video: &openrtb2.Video{ + W: ptrutil.ToPtr[int64](200), + H: ptrutil.ToPtr[int64](300), + Plcmt: 1, + Protocols: []adcom1.MediaCreativeSubtype{1, 2, 3}, + }, + }, + }, + Device: &openrtb2.Device{ + IP: "127.0.0.1", + Language: "en", + DeviceType: 1, + }, + WLang: []string{"en", "hi"}, + User: &openrtb2.User{ + CustomData: "123456789", + Ext: json.RawMessage(`{}`), + }, + Site: &openrtb2.Site{ + Publisher: &openrtb2.Publisher{ + ID: "1010", + }, + Content: &openrtb2.Content{ + Language: "en", + }, + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/modules/pubmatic/openwrap/models/constants.go b/modules/pubmatic/openwrap/models/constants.go index 02812466bea..6ce0d9e277b 100755 --- a/modules/pubmatic/openwrap/models/constants.go +++ b/modules/pubmatic/openwrap/models/constants.go @@ -34,6 +34,7 @@ const ( PLATFORM_KEY = "platform" SendAllBidsKey = "sendAllBids" VastUnwrapperEnableKey = "enableVastUnwrapper" + StrictVastModeKey = "strictVastMode" VastUnwrapTrafficPercentKey = "vastUnwrapTrafficPercent" SSTimeoutKey = "ssTimeout" PWC = "awc" diff --git a/modules/pubmatic/openwrap/models/nbr/codes.go b/modules/pubmatic/openwrap/models/nbr/codes.go index df584806b78..0a238ec95f2 100644 --- a/modules/pubmatic/openwrap/models/nbr/codes.go +++ b/modules/pubmatic/openwrap/models/nbr/codes.go @@ -4,12 +4,13 @@ import "github.com/prebid/openrtb/v20/openrtb3" // vendor specific NoBidReasons (500+) const ( - LossBidLostToHigherBid openrtb3.NoBidReason = 501 // Response Rejected - Lost to Higher Bid - LossBidLostToDealBid openrtb3.NoBidReason = 502 // Response Rejected - Lost to a Bid for a Deal - RequestBlockedSlotNotMapped openrtb3.NoBidReason = 503 - RequestBlockedPartnerThrottle openrtb3.NoBidReason = 504 - RequestBlockedPartnerFiltered openrtb3.NoBidReason = 505 - LossBidLostInVastUnwrap openrtb3.NoBidReason = 506 + LossBidLostToHigherBid openrtb3.NoBidReason = 501 // Response Rejected - Lost to Higher Bid + LossBidLostToDealBid openrtb3.NoBidReason = 502 // Response Rejected - Lost to a Bid for a Deal + RequestBlockedSlotNotMapped openrtb3.NoBidReason = 503 + RequestBlockedPartnerThrottle openrtb3.NoBidReason = 504 + RequestBlockedPartnerFiltered openrtb3.NoBidReason = 505 + LossBidLostInVastUnwrap openrtb3.NoBidReason = 506 + LossBidLostInVastVersionValidation openrtb3.NoBidReason = 507 ) // Openwrap module specific codes diff --git a/modules/pubmatic/openwrap/util.go b/modules/pubmatic/openwrap/util.go index ea348f9ad98..6bbd24f6523 100644 --- a/modules/pubmatic/openwrap/util.go +++ b/modules/pubmatic/openwrap/util.go @@ -10,6 +10,8 @@ import ( "strconv" "strings" + "golang.org/x/exp/slices" // Use standard library in next prebid upgrade + "github.com/buger/jsonparser" "github.com/golang/glog" "github.com/prebid/openrtb/v20/adcom1" @@ -56,6 +58,10 @@ const ( test = "_test" ) +var ( + protocols = []adcom1.MediaCreativeSubtype{adcom1.CreativeVAST30, adcom1.CreativeVAST30Wrapper, adcom1.CreativeVAST40, adcom1.CreativeVAST40Wrapper} +) + func init() { widthRegEx = regexp.MustCompile(models.MACRO_WIDTH) heightRegEx = regexp.MustCompile(models.MACRO_HEIGHT) @@ -548,3 +554,12 @@ func UpdateUserExtWithValidValues(user *openrtb2.User) { } } } + +func UpdateImpProtocols(impProtocols []adcom1.MediaCreativeSubtype) []adcom1.MediaCreativeSubtype { + for _, protocol := range protocols { + if !slices.Contains(impProtocols, protocol) { + impProtocols = append(impProtocols, protocol) + } + } + return impProtocols +} diff --git a/modules/pubmatic/openwrap/util_test.go b/modules/pubmatic/openwrap/util_test.go index 61407b9403e..ff1a8b02b57 100644 --- a/modules/pubmatic/openwrap/util_test.go +++ b/modules/pubmatic/openwrap/util_test.go @@ -1925,3 +1925,86 @@ func TestUpdateUserExtWithValidValues(t *testing.T) { }) } } + +func TestUpdateImpProtocols(t *testing.T) { + tests := []struct { + name string + impProtocols []adcom1.MediaCreativeSubtype + want []adcom1.MediaCreativeSubtype + }{ + { + name: "Empty_Protocols", + impProtocols: []adcom1.MediaCreativeSubtype{}, + want: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST30, + adcom1.CreativeVAST30Wrapper, + adcom1.CreativeVAST40, + adcom1.CreativeVAST40Wrapper, + }, + }, + { + name: "VAST20_Protocols_Present", + impProtocols: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST20, + }, + want: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST20, + adcom1.CreativeVAST30, + adcom1.CreativeVAST30Wrapper, + adcom1.CreativeVAST40, + adcom1.CreativeVAST40Wrapper, + }, + }, + { + name: "VAST30_Protocols_Present", + impProtocols: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST30, + }, + want: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST30, + adcom1.CreativeVAST30Wrapper, + adcom1.CreativeVAST40, + adcom1.CreativeVAST40Wrapper, + }, + }, + { + name: "All_Protocols_Present", + impProtocols: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST30, + adcom1.CreativeVAST30Wrapper, + adcom1.CreativeVAST40, + adcom1.CreativeVAST40Wrapper, + }, + want: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST30, + adcom1.CreativeVAST30Wrapper, + adcom1.CreativeVAST40, + adcom1.CreativeVAST40Wrapper, + }, + }, + { + name: "Additional_Protocols_Present", + impProtocols: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST30, + adcom1.CreativeVAST30Wrapper, + adcom1.CreativeVAST40, + adcom1.CreativeVAST40Wrapper, + adcom1.CreativeVAST20, + }, + want: []adcom1.MediaCreativeSubtype{ + adcom1.CreativeVAST30, + adcom1.CreativeVAST30Wrapper, + adcom1.CreativeVAST40, + adcom1.CreativeVAST40Wrapper, + adcom1.CreativeVAST20, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := UpdateImpProtocols(tt.impProtocols) + assert.ElementsMatch(t, tt.want, got) + }) + } +} diff --git a/openrtb_ext/openwrap.go b/openrtb_ext/openwrap.go index 5371c78cf44..6c71f527150 100644 --- a/openrtb_ext/openwrap.go +++ b/openrtb_ext/openwrap.go @@ -43,6 +43,7 @@ type ExtOWRequestPrebid struct { Transparency *TransparencyExt `json:"transparency,omitempty"` KeyVal map[string]interface{} `json:"keyval,omitempty"` TrackerDisabled bool `json:"tracker_disabled,omitempty"` + StrictVastMode bool `json:"strictvastmode,omitempty"` } // ExtCTVBid defines the contract for bidresponse.seatbid.bid[i].ext