From 0b2d7e16b16bd5f18badf68df9d3d8e18d36a12d Mon Sep 17 00:00:00 2001 From: Chris Corbo Date: Mon, 15 May 2023 14:21:07 -0400 Subject: [PATCH] feat: multiimp work in ixbidadapter behind ft, pass sid signal through from bidder params --- adapters/ix/ix.go | 306 ++++++++++++++++-- adapters/ix/ix_test.go | 239 +++++++++++++- adapters/ix/ixtest/supplemental/sid.json | 257 +++++++++++++++ .../exemplary/multi-imp-requests.json | 289 +++++++++++++++++ adapters/ix/params_test.go | 1 + openrtb_ext/imp_ix.go | 1 + static/bidder-params/ix.json | 5 + 7 files changed, 1073 insertions(+), 25 deletions(-) create mode 100644 adapters/ix/ixtest/supplemental/sid.json create mode 100644 adapters/ix/ixtestmulti/exemplary/multi-imp-requests.json diff --git a/adapters/ix/ix.go b/adapters/ix/ix.go index b17d913f42d..96b2aa3010a 100644 --- a/adapters/ix/ix.go +++ b/adapters/ix/ix.go @@ -1,11 +1,13 @@ package ix import ( + "bytes" "encoding/json" "fmt" "net/http" "sort" "strings" + "time" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/config" @@ -18,15 +20,28 @@ import ( "github.com/prebid/openrtb/v19/openrtb2" ) +const ( + FtHandleMultiImpOnSingleRequest = "pbs_handle_multi_imp_on_single_req" +) + +const ExpireFtTime = 3600 //1 hour + type IxAdapter struct { - URI string - maxRequests int + URI string + maxRequests int + clientFeatureStatusMap map[string]FeatureTimestamp + featuresToRequest []string } type ExtRequest struct { - Prebid *openrtb_ext.ExtRequestPrebid `json:"prebid"` - SChain *openrtb2.SupplyChain `json:"schain,omitempty"` - IxDiag *IxDiag `json:"ixdiag,omitempty"` + Prebid *openrtb_ext.ExtRequestPrebid `json:"prebid"` + SChain *openrtb2.SupplyChain `json:"schain,omitempty"` + IxDiag *IxDiag `json:"ixdiag,omitempty"` + Features map[string]Activated `json:"features,omitempty"` +} + +type Activated struct { + Activated bool `json:"activated"` } type IxDiag struct { @@ -34,30 +49,40 @@ type IxDiag struct { PbjsV string `json:"pbjsv,omitempty"` } +type FeatureTimestamp struct { + Timestamp int64 + Activated bool +} + func (a *IxAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + if isFeatureToggleActive(a, FtHandleMultiImpOnSingleRequest) == true { + return handleMultiImpInSingleRequest(request, a) + } else { + return handleMultiImpByFlattening(request, a) + } +} + +func handleMultiImpByFlattening(request *openrtb2.BidRequest, a *IxAdapter) ([]*adapters.RequestData, []error) { nImp := len(request.Imp) if nImp > a.maxRequests { request.Imp = request.Imp[:a.maxRequests] nImp = a.maxRequests } - - errs := make([]error, 0) - - if err := BuildIxDiag(request); err != nil { - errs = append(errs, err) - } - // Multi-size banner imps are split into single-size requests. // The first size imp requests are added to the first slice. // Additional size requests are added to the second slice and are merged with the first at the end. // Preallocate the max possible size to avoid reallocating arrays. requests := make([]*adapters.RequestData, 0, a.maxRequests) multiSizeRequests := make([]*adapters.RequestData, 0, a.maxRequests-nImp) + errs := make([]error, 0, 1) + + if err := BuildIxDiag(request); err != nil { + errs = append(errs, err) + } headers := http.Header{ "Content-Type": {"application/json;charset=utf-8"}, "Accept": {"application/json"}} - imps := request.Imp for iImp := range imps { request.Imp = imps[iImp : iImp+1] @@ -67,7 +92,10 @@ func (a *IxAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters continue } } - + if err := moveSid(&request.Imp[0]); err != nil { + errs = append(errs, err) + } + requestFeatureToggles(a, request) if request.Imp[0].Banner != nil { banner := *request.Imp[0].Banner request.Imp[0].Banner = &banner @@ -100,6 +128,138 @@ func (a *IxAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters return append(requests, multiSizeRequests...), errs } +func handleMultiImpInSingleRequest(request *openrtb2.BidRequest, a *IxAdapter) ([]*adapters.RequestData, []error) { + nImp := len(request.Imp) + if nImp > a.maxRequests { + request.Imp = request.Imp[:a.maxRequests] + nImp = a.maxRequests + } + // Multi-size banner imps are split into single-size requests. + // The first size imp requests are added to the first slice. + // Additional size requests are added to the second slice and are merged with the first at the end. + requests := make([]*adapters.RequestData, 0, a.maxRequests) + multiSizeRequests := make([]*adapters.RequestData, 0, a.maxRequests-1) + errs := make([]error, 0) + + if err := BuildIxDiag(request); err != nil { + errs = append(errs, err) + } + + headers := http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}} + + siteIds := make(map[string]bool) + originalImps := make([]openrtb2.Imp, 0, len(request.Imp)) + additionalMultiSizeImps := make([]openrtb2.Imp, 0) + requestCopy := *request + for _, imp := range requestCopy.Imp { + if err := parseSiteId(&imp, siteIds); err != nil { + errs = append(errs, err) + continue + } + + if err := moveSid(&imp); err != nil { + errs = append(errs, err) + } + + if imp.Banner != nil { + bannerCopy := *imp.Banner + + if len(bannerCopy.Format) == 0 && bannerCopy.W != nil && bannerCopy.H != nil { + bannerCopy.Format = []openrtb2.Format{{W: *bannerCopy.W, H: *bannerCopy.H}} + } + + if len(bannerCopy.Format) == 1 { + bannerCopy.W = openrtb2.Int64Ptr(bannerCopy.Format[0].W) + bannerCopy.H = openrtb2.Int64Ptr(bannerCopy.Format[0].H) + } + + if len(bannerCopy.Format) > 1 { + formats := bannerCopy.Format + // Creating additional imp from multiple formats. First format is part of original imp. Rest format will be converted to individual single imp & later request. + for iFmt := range formats { + additionalBannerCopy := *imp.Banner + additionalBannerCopy.Format = formats[iFmt : iFmt+1] + additionalBannerCopy.W = openrtb2.Int64Ptr(additionalBannerCopy.Format[0].W) + additionalBannerCopy.H = openrtb2.Int64Ptr(additionalBannerCopy.Format[0].H) + if iFmt == 0 { + bannerCopy = additionalBannerCopy + } else { + imp.Banner = &additionalBannerCopy + additionalMultiSizeImps = append(additionalMultiSizeImps, imp) + } + } + } + + imp.Banner = &bannerCopy + } + originalImps = append(originalImps, imp) + } + + if requestCopy.Site != nil { + site := *requestCopy.Site + if site.Publisher == nil { + site.Publisher = &openrtb2.Publisher{} + } + if len(siteIds) == 1 { + for siteId := range siteIds { + site.Publisher.ID = siteId + } + } + requestCopy.Site = &site + } + + if len(siteIds) > 1 { + var siteIdStringBuffer bytes.Buffer + for siteId := range siteIds { + siteIdStringBuffer.WriteString(siteId) + siteIdStringBuffer.WriteString(", ") + } + a.logger.Warn(fmt.Sprintf("Multiple SiteIDs found. %s", siteIdStringBuffer.String())) + } + + requestCopy.Imp = originalImps + + requestFeatureToggles(a, &requestCopy) + + if len(requestCopy.Imp) != 0 { + if requestData, err := createRequestData(a, &requestCopy, &headers); err == nil { + requests = append(requests, requestData) + } else { + errs = append(errs, err) + } + } + + for i := range additionalMultiSizeImps { + requestCopy.Imp = additionalMultiSizeImps[i : i+1] + if requestData, err := createRequestData(a, &requestCopy, &headers); err == nil { + multiSizeRequests = append(multiSizeRequests, requestData) + } else { + errs = append(errs, err) + } + } + + return append(requests, multiSizeRequests...), errs +} + +func parseSiteId(imp *openrtb2.Imp, siteIds map[string]bool) error { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return err + } + + var ixExt openrtb_ext.ExtImpIx + if err := json.Unmarshal(bidderExt.Bidder, &ixExt); err != nil { + return err + } + + if ixExt.SiteId != "" { + siteIds[ixExt.SiteId] = true + } + return nil +} + func setSitePublisherId(request *openrtb2.BidRequest, iImp int) error { if iImp == 0 { // first impression - create a site and pub copy @@ -112,17 +272,14 @@ func setSitePublisherId(request *openrtb2.BidRequest, iImp int) error { } request.Site = &site } - var bidderExt adapters.ExtImpBidder if err := json.Unmarshal(request.Imp[0].Ext, &bidderExt); err != nil { return err } - var ixExt openrtb_ext.ExtImpIx if err := json.Unmarshal(bidderExt.Bidder, &ixExt); err != nil { return err } - request.Site.Publisher.ID = ixExt.SiteId return nil } @@ -165,6 +322,8 @@ func (a *IxAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalReque }} } + setFeatureToggles(a, &bidResponse.Ext) + // Store media type per impression in a map for later use to set in bid.ext.prebid.type // Won't work for multiple bid case with a multi-format ad unit. We expect to get type from exchange on such case. impMediaTypeReq := map[string]openrtb_ext.BidType{} @@ -180,7 +339,8 @@ func (a *IxAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalReque } } - bidderResponse := adapters.NewBidderResponseWithBidsCapacity(5) + // capacity 0 will make channel unbuffered + bidderResponse := adapters.NewBidderResponseWithBidsCapacity(0) bidderResponse.Currency = bidResponse.Cur var errs []error @@ -275,8 +435,10 @@ func getMediaTypeForBid(bid openrtb2.Bid, impMediaTypeReq map[string]openrtb_ext // Builder builds a new instance of the Ix adapter for the given bidder with the given config. func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { bidder := &IxAdapter{ - URI: config.Endpoint, - maxRequests: 20, + URI: config.Endpoint, + maxRequests: 100, + clientFeatureStatusMap: make(map[string]FeatureTimestamp), + featuresToRequest: []string{FtHandleMultiImpOnSingleRequest}, } return bidder, nil } @@ -336,18 +498,15 @@ func BuildIxDiag(request *openrtb2.BidRequest) error { } } ixdiag := &IxDiag{} - if extRequest.Prebid != nil && extRequest.Prebid.Channel != nil { ixdiag.PbjsV = extRequest.Prebid.Channel.Version } - // Slice commit hash out of version if strings.Contains(version.Ver, "-") { ixdiag.PbsV = version.Ver[:strings.Index(version.Ver, "-")] } else if version.Ver != "" { ixdiag.PbsV = version.Ver } - // Only set request.ext if ixDiag is not empty if *ixdiag != (IxDiag{}) { extRequest.IxDiag = ixdiag @@ -359,3 +518,104 @@ func BuildIxDiag(request *openrtb2.BidRequest) error { } return nil } + +// moves sid from imp[].ext.bidder.sid to imp[].ext.sid +func moveSid(imp *openrtb2.Imp) error { + if imp.Ext == nil || len(imp.Ext) == 0 { + return nil + } + + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return err + } + + var ixExt openrtb_ext.ExtImpIx + if err := json.Unmarshal(bidderExt.Bidder, &ixExt); err != nil { + return err + } + + if ixExt.Sid != "" { + var m map[string]interface{} + if err := json.Unmarshal(imp.Ext, &m); err != nil { + return err + } + m["sid"] = ixExt.Sid + ext, err := json.Marshal(m) + if err != nil { + return err + } + imp.Ext = ext + } + return nil +} + +func setFeatureToggles(a *IxAdapter, ext *json.RawMessage) { + if *ext == nil { + return + } + + var f interface{} + err := json.Unmarshal(*ext, &f) + + if err != nil { + return + } + + if features, ok := f.(map[string]interface{})["features"]; ok { + if ft, ok := features.(map[string]interface{}); ok { + for k, v := range ft { + if activated, ok := v.(map[string]interface{})["activated"]; ok { + a.clientFeatureStatusMap[k] = FeatureTimestamp{ + Activated: activated.(bool), + Timestamp: time.Now().Unix(), + } + } + } + } + } +} + +func isFeatureToggleActive(a *IxAdapter, ft string) bool { + if value, ok := a.clientFeatureStatusMap[ft]; ok { + timeNow := time.Now().Unix() + if timeNow-value.Timestamp > ExpireFtTime { + return false + } + return value.Activated + } + return false +} + +func requestFeatureToggles(a *IxAdapter, request *openrtb2.BidRequest) error { + if len(a.featuresToRequest) == 0 { + return nil + } + + extRequest := &ExtRequest{} + if request.Ext != nil { + if err := json.Unmarshal(request.Ext, &extRequest); err != nil { + return err + } + } + + for _, ft := range a.featuresToRequest { + if extRequest.Features == nil { + extRequest.Features = make(map[string]Activated) + } + + status := isFeatureToggleActive(a, ft) + + extRequest.Features[ft] = Activated{ + Activated: status, + } + + } + + extRequestJson, err := json.Marshal(extRequest) + if err != nil { + return err + } + request.Ext = extRequestJson + return nil +} diff --git a/adapters/ix/ix_test.go b/adapters/ix/ix_test.go index 0f6b856dce4..f4d653121dd 100644 --- a/adapters/ix/ix_test.go +++ b/adapters/ix/ix_test.go @@ -3,6 +3,7 @@ package ix import ( "encoding/json" "testing" + "time" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/adapters/adapterstest" @@ -21,12 +22,29 @@ func TestJsonSamples(t *testing.T) { if bidder, err := Builder(openrtb_ext.BidderIx, config.Adapter{Endpoint: endpoint}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}); err == nil { ixBidder := bidder.(*IxAdapter) ixBidder.maxRequests = 2 + ixBidder.featuresToRequest = nil adapterstest.RunJSONBidderTest(t, "ixtest", bidder) } else { t.Fatalf("Builder returned unexpected error %v", err) } } +func TestJsonForMultiImpAndSize(t *testing.T) { + if bidder, err := Builder(openrtb_ext.BidderIx, config.Adapter{Endpoint: endpoint}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}); err == nil { + ixBidder := bidder.(*IxAdapter) + ixBidder.maxRequests = 2 + ixBidder.clientFeatureStatusMap = map[string]FeatureTimestamp{ + "pbs_handle_multi_imp_on_single_req": { + Activated: true, + Timestamp: time.Now().Unix(), + }, + } + adapterstest.RunJSONBidderTest(t, "ixtestmulti", bidder) + } else { + t.Fatalf("Builder returned unexpected error %v", err) + } +} + func TestIxMakeBidsWithCategoryDuration(t *testing.T) { bidder := &IxAdapter{} @@ -44,7 +62,7 @@ func TestIxMakeBidsWithCategoryDuration(t *testing.T) { `{ "prebid": {}, "bidder": { - "siteID": 123456 + "siteID": "123456" } }`, )}, @@ -124,7 +142,7 @@ func TestIxMakeRequestWithGppString(t *testing.T) { `{ "prebid": {}, "bidder": { - "siteID": 123456 + "siteId": "123456" } }`, )}, @@ -276,3 +294,220 @@ func TestMakeRequestsErrIxDiag(t *testing.T) { _, errs := bidder.MakeRequests(req, nil) assert.Len(t, errs, 1) } + +func TestSetFeatureToggles(t *testing.T) { + testCases := []struct { + description string + ext json.RawMessage + expected map[string]FeatureTimestamp + }{ + { + description: "nil ext", + ext: nil, + expected: map[string]FeatureTimestamp{}, + }, + { + description: "Empty input", + ext: json.RawMessage(``), + expected: map[string]FeatureTimestamp{}, + }, + { + description: "valid input with one feature toggle", + ext: json.RawMessage(`{"features":{"ft_test_1":{"activated":true}}}`), + expected: map[string]FeatureTimestamp{ + "ft_test_1": { + Activated: true, + Timestamp: time.Now().Unix(), + }, + }, + }, + { + description: "valid input with two feature toggles", + ext: json.RawMessage(`{"features":{"ft_test_1":{"activated":true},"ft_test_2":{"activated":false}}}`), + expected: map[string]FeatureTimestamp{ + "ft_test_1": { + Activated: true, + Timestamp: time.Now().Unix(), + }, + "ft_test_2": { + Activated: false, + Timestamp: time.Now().Unix(), + }, + }, + }, + { + description: "input with one feature toggle, no activated key", + ext: json.RawMessage(`{"features":{"ft_test_1":{"exists":true}}}`), + expected: map[string]FeatureTimestamp{}, + }, + { + description: "features not formatted correctly", + ext: json.RawMessage(`{"features":"helloworld"}`), + expected: map[string]FeatureTimestamp{}, + }, + { + description: "no features", + ext: json.RawMessage(`{"prebid":{"test":"helloworld"}}`), + expected: map[string]FeatureTimestamp{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + bidder, _ := Builder(openrtb_ext.BidderIx, config.Adapter{Endpoint: endpoint}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + ixBidder := bidder.(*IxAdapter) + setFeatureToggles(ixBidder, &tc.ext) + assert.Equal(t, tc.expected, ixBidder.clientFeatureStatusMap) + }) + } +} + +func TestGetFeatureToggle(t *testing.T) { + clientFeatureMap := map[string]FeatureTimestamp{ + "feature1": { + Activated: true, + Timestamp: time.Now().Unix(), + }, + "feature2": { + Activated: true, + Timestamp: time.Now().Unix() - 3700, + }, + "feature3": { + Activated: false, + Timestamp: time.Now().Unix(), + }, + } + + tests := []struct { + description string + ftName string + expected bool + }{ + {"ActivatedFeature", "feature1", true}, + {"ExpiredFeature", "feature2", false}, + {"NotExpiredFeature", "feature3", false}, + {"NonExistentFeature", "nonexistent", false}, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + bidder, _ := Builder(openrtb_ext.BidderIx, config.Adapter{Endpoint: endpoint}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + ixBidder := bidder.(*IxAdapter) + ixBidder.clientFeatureStatusMap = clientFeatureMap + result := isFeatureToggleActive(ixBidder, test.ftName) + assert.Equal(t, test.expected, result) + }) + } +} + +func TestRequestFeatureToggles(t *testing.T) { + type testCase struct { + name string + inputRequest *openrtb2.BidRequest + featuresToRequest []string + expectedExt json.RawMessage + initialFeatureMap *FeatureTimestamp + } + + testCases := []testCase{ + { + name: "empty features", + inputRequest: &openrtb2.BidRequest{ID: "1"}, + featuresToRequest: []string{}, + expectedExt: json.RawMessage(nil), + }, + { + name: "no features existing internally, request feature expect false", + inputRequest: &openrtb2.BidRequest{ID: "1"}, + featuresToRequest: []string{"ft1"}, + expectedExt: json.RawMessage(`{"prebid":null,"features":{"ft1":{"activated":false}}}`), + }, + { + name: "feature exists internally and activated", + inputRequest: &openrtb2.BidRequest{ID: "1"}, + featuresToRequest: []string{"ft1"}, + expectedExt: json.RawMessage(`{"prebid":null,"features":{"ft1":{"activated":true}}}`), + initialFeatureMap: &FeatureTimestamp{Timestamp: time.Now().Unix(), Activated: true}, + }, + { + name: "feature exists internally and not activated", + inputRequest: &openrtb2.BidRequest{ID: "1"}, + featuresToRequest: []string{"ft1"}, + expectedExt: json.RawMessage(`{"prebid":null,"features":{"ft1":{"activated":false}}}`), + initialFeatureMap: &FeatureTimestamp{Timestamp: time.Now().Unix(), Activated: false}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bidder, _ := Builder(openrtb_ext.BidderIx, config.Adapter{Endpoint: endpoint}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + ixBidder := bidder.(*IxAdapter) + if tc.initialFeatureMap != nil { + ixBidder.clientFeatureStatusMap["ft1"] = *tc.initialFeatureMap + } + ixBidder.featuresToRequest = tc.featuresToRequest + requestFeatureToggles(ixBidder, tc.inputRequest) + assert.Equal(t, tc.expectedExt, tc.inputRequest.Ext) + }) + } +} + +func TestMoveSid(t *testing.T) { + testCases := []struct { + description string + imp openrtb2.Imp + expectedExt json.RawMessage + expectErr bool + }{ + { + description: "valid input with sid", + imp: openrtb2.Imp{ + Ext: json.RawMessage(`{"bidder":{"sid":"1234"}}`), + }, + expectedExt: json.RawMessage(`{"bidder":{"sid":"1234"},"sid":"1234"}`), + expectErr: false, + }, + { + description: "valid input without sid", + imp: openrtb2.Imp{ + Ext: json.RawMessage(`{"bidder":{"siteId":"1234"}}`), + }, + expectedExt: json.RawMessage(`{"bidder":{"siteId":"1234"}}`), + expectErr: false, + }, + { + description: "no ext", + imp: openrtb2.Imp{ID: "1"}, + expectedExt: nil, + expectErr: false, + }, + { + description: "malformed json", + imp: openrtb2.Imp{ + Ext: json.RawMessage(`"bidder":{"sid":"1234"}`), + }, + expectedExt: json.RawMessage(`"bidder":{"sid":"1234"}`), + expectErr: true, + }, + { + description: "malformed bidder json", + imp: openrtb2.Imp{ + Ext: json.RawMessage(`{"bidder":{"sid":1234}}`), + }, + expectedExt: json.RawMessage(`{"bidder":{"sid":1234}}`), + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + err := moveSid(&tc.imp) + assert.Equal(t, tc.expectedExt, tc.imp.Ext) + if tc.expectErr { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + }) + } +} diff --git a/adapters/ix/ixtest/supplemental/sid.json b/adapters/ix/ixtest/supplemental/sid.json new file mode 100644 index 00000000000..41afaedf524 --- /dev/null +++ b/adapters/ix/ixtest/supplemental/sid.json @@ -0,0 +1,257 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749", + "sid": "1234" + } + } + }, + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569750", + "sid": "5678" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749", + "sid": "1234" + }, + "sid": "1234" + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569750", + "sid": "5678" + }, + "sid": "5678" + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-2", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "cat": [ + "IAB9-1" + ], + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {} + } + }, + "type": "video" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-2", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "cat": [ + "IAB9-1" + ], + "ext": { + "ix": {} + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/ix/ixtestmulti/exemplary/multi-imp-requests.json b/adapters/ix/ixtestmulti/exemplary/multi-imp-requests.json new file mode 100644 index 00000000000..477e49745a7 --- /dev/null +++ b/adapters/ix/ixtestmulti/exemplary/multi-imp-requests.json @@ -0,0 +1,289 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-3", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "ext": { + "features": { + "pbs_handle_multi_imp_on_single_req": { + "activated": true + } + }, + "prebid": null + }, + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 600 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 600, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "ext": { + "features": { + "pbs_handle_multi_imp_on_single_req": { + "activated": true + } + }, + "prebid": null + }, + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-2", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "cat": [ + "IAB9-1" + ], + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 600, + "ext": { + "ix": {} + } + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-2", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "cat": [ + "IAB9-1" + ], + "ext": { + "ix": {} + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/ix/params_test.go b/adapters/ix/params_test.go index 9246a43a725..8ba937c12f4 100644 --- a/adapters/ix/params_test.go +++ b/adapters/ix/params_test.go @@ -38,6 +38,7 @@ var validParams = []string{ `{"siteID":"12345"}`, `{"siteId":"123456"}`, `{"siteid":"1234567", "size": [640,480]}`, + `{"siteId":"123456", "sid":"12345"}`, } var invalidParams = []string{ diff --git a/openrtb_ext/imp_ix.go b/openrtb_ext/imp_ix.go index 9f977fb0dcd..40c3f51867f 100644 --- a/openrtb_ext/imp_ix.go +++ b/openrtb_ext/imp_ix.go @@ -4,4 +4,5 @@ package openrtb_ext type ExtImpIx struct { SiteId string `json:"siteId"` Size []int `json:"size"` + Sid string `json:"sid"` } diff --git a/static/bidder-params/ix.json b/static/bidder-params/ix.json index a7a5cb7308a..172690cca32 100644 --- a/static/bidder-params/ix.json +++ b/static/bidder-params/ix.json @@ -27,6 +27,11 @@ "minItems": 2, "maxItems": 2, "description": "An array of two integer containing the dimension" + }, + "sid": { + "type": "string", + "minLength": 1, + "description": "Slot ID" } }, "oneOf": [