From d66338035f232aef73099ae1c257628142c62e2c Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Mon, 24 Aug 2020 13:43:02 -0700 Subject: [PATCH] Added adpod_id to request extension (#1444) * Added adpod_id to request -> ext -> appnexus and modified requests splitting based on pod * Unit test fix * Unit test fix * Minor unit test fixes * Code refactoring * Minor code and unit tests refactoring * Unit tests refactoring Co-authored-by: Veronika Solovei --- adapters/appnexus/appnexus.go | 62 ++++- adapters/appnexus/appnexus_test.go | 229 ++++++++++++++++++ .../video/simple-video.json | 132 ---------- 3 files changed, 283 insertions(+), 140 deletions(-) delete mode 100644 adapters/appnexus/appnexusplatformtest/video/simple-video.json diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go index 9bec9bf1e3b..334817ebca7 100644 --- a/adapters/appnexus/appnexus.go +++ b/adapters/appnexus/appnexus.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "math/rand" "net/http" "strconv" "strings" @@ -95,10 +96,11 @@ type appnexusBidExt struct { } type appnexusReqExtAppnexus struct { - IncludeBrandCategory *bool `json:"include_brand_category,omitempty"` - BrandCategoryUniqueness *bool `json:"brand_category_uniqueness,omitempty"` - IsAMP int `json:"is_amp,omitempty"` - HeaderBiddingSource int `json:"hb_source,omitempty"` + IncludeBrandCategory *bool `json:"include_brand_category,omitempty"` + BrandCategoryUniqueness *bool `json:"brand_category_uniqueness,omitempty"` + IsAMP int `json:"is_amp,omitempty"` + HeaderBiddingSource int `json:"hb_source,omitempty"` + AdPodId string `json:"adpod_id,omitempty"` } // Full request extension including appnexus extension object @@ -354,14 +356,56 @@ func (a *AppNexusAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *ada } reqExt.Appnexus.IsAMP = isAMP reqExt.Appnexus.HeaderBiddingSource = a.hbSource + isVIDEO + + imps := request.Imp + + // For long form requests adpod_id must be sent downstream. + // Adpod id is a unique identifier for pod + // All impressions in the same pod must have the same pod id in request extension + // For this all impressions in request should belong to the same pod + // If impressions number per pod is more than maxImpsPerReq - divide those imps to several requests but keep pod id the same + if isVIDEO == 1 { + podImps := groupByPods(imps) + + requests := make([]*adapters.RequestData, 0, len(podImps)) + for _, podImps := range podImps { + reqExt.Appnexus.AdPodId = generatePodId() + + reqs, errors := splitRequests(podImps, request, reqExt, thisURI, errs) + requests = append(requests, reqs...) + errs = append(errs, errors...) + } + return requests, errs + } + + return splitRequests(imps, request, reqExt, thisURI, errs) +} + +func generatePodId() string { + val := rand.Int63() + return fmt.Sprint(val) +} + +func groupByPods(imps []openrtb.Imp) map[string]([]openrtb.Imp) { + // find number of pods in response + podImps := make(map[string][]openrtb.Imp) + for _, imp := range imps { + pod := strings.Split(imp.ID, "_")[0] + podImps[pod] = append(podImps[pod], imp) + } + return podImps +} + +func marshalAndSetRequestExt(request *openrtb.BidRequest, requestExtension appnexusReqExt, errs []error) { var err error - request.Ext, err = json.Marshal(reqExt) + request.Ext, err = json.Marshal(requestExtension) if err != nil { errs = append(errs, err) - return nil, errs } +} + +func splitRequests(imps []openrtb.Imp, request *openrtb.BidRequest, requestExtension appnexusReqExt, uri string, errs []error) ([]*adapters.RequestData, []error) { - imps := request.Imp // Initial capacity for future array of requests, memory optimization. // Let's say there are 35 impressions and limit impressions per request equals to 10. // In this case we need to create 4 requests with 10, 10, 10 and 5 impressions. @@ -375,6 +419,8 @@ func (a *AppNexusAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *ada headers.Add("Content-Type", "application/json;charset=utf-8") headers.Add("Accept", "application/json") + marshalAndSetRequestExt(request, requestExtension, errs) + for impsLeft { endInd := startInd + maxImpsPerReq @@ -393,7 +439,7 @@ func (a *AppNexusAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *ada resArr = append(resArr, &adapters.RequestData{ Method: "POST", - Uri: thisURI, + Uri: uri, Body: reqJSON, Headers: headers, }) diff --git a/adapters/appnexus/appnexus_test.go b/adapters/appnexus/appnexus_test.go index bf49374940a..26380406624 100644 --- a/adapters/appnexus/appnexus_test.go +++ b/adapters/appnexus/appnexus_test.go @@ -4,9 +4,11 @@ import ( "bytes" "context" "encoding/json" + "github.com/stretchr/testify/assert" "io/ioutil" "net/http" "net/http/httptest" + "regexp" "testing" "time" @@ -38,6 +40,233 @@ func TestMemberQueryParam(t *testing.T) { } } +func TestVideoSinglePod(t *testing.T) { + var a AppNexusAdapter + a.URI = "http://test.com/openrtb2" + a.hbSource = 5 + + var reqInfo adapters.ExtraRequestInfo + reqInfo.PbsEntryPoint = "video" + + var req openrtb.BidRequest + req.ID = "test_id" + + reqExt := `{"prebid":{}}` + impExt := `{"bidder":{"placementId":123}}` + req.Ext = []byte(reqExt) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_2", Ext: []byte(impExt)}) + + result, err := a.MakeRequests(&req, &reqInfo) + + assert.Empty(t, err, "Errors array should be empty") + assert.Len(t, result, 1, "Only one request should be returned") + + var error error + var reqData *openrtb.BidRequest + error = json.Unmarshal(result[0].Body, &reqData) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt *appnexusReqExt + error = json.Unmarshal(reqData.Ext, &reqDataExt) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + regMatch, matchErr := regexp.Match(`[0-9]19`, []byte(reqDataExt.Appnexus.AdPodId)) + assert.NoError(t, matchErr, "Regex match error should be nil") + assert.True(t, regMatch, "AdPod id doesn't present in Appnexus extension or has incorrect format") +} + +func TestVideoSinglePodManyImps(t *testing.T) { + var a AppNexusAdapter + a.URI = "http://test.com/openrtb2" + a.hbSource = 5 + + var reqInfo adapters.ExtraRequestInfo + reqInfo.PbsEntryPoint = "video" + + var req openrtb.BidRequest + req.ID = "test_id" + + reqExt := `{"prebid":{}}` + impExt := `{"bidder":{"placementId":123}}` + req.Ext = []byte(reqExt) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_2", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_3", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_4", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_5", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_6", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_7", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_8", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_9", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_10", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_11", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_12", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_13", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_14", Ext: []byte(impExt)}) + + res, err := a.MakeRequests(&req, &reqInfo) + + assert.Empty(t, err, "Errors array should be empty") + assert.Len(t, res, 2, "Two requests should be returned") + + var error error + var reqData1 *openrtb.BidRequest + error = json.Unmarshal(res[0].Body, &reqData1) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt1 *appnexusReqExt + error = json.Unmarshal(reqData1.Ext, &reqDataExt1) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId1 := reqDataExt1.Appnexus.AdPodId + + var reqData2 *openrtb.BidRequest + error = json.Unmarshal(res[1].Body, &reqData2) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt2 *appnexusReqExt + error = json.Unmarshal(reqData2.Ext, &reqDataExt2) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId2 := reqDataExt2.Appnexus.AdPodId + + assert.Equal(t, adPodId1, adPodId2, "AdPod id is not the same for the same pod") +} + +func TestVideoTwoPods(t *testing.T) { + var a AppNexusAdapter + a.URI = "http://test.com/openrtb2" + a.hbSource = 5 + + var reqInfo adapters.ExtraRequestInfo + reqInfo.PbsEntryPoint = "video" + + var req openrtb.BidRequest + req.ID = "test_id" + + reqExt := `{"prebid":{}}` + impExt := `{"bidder":{"placementId":123}}` + req.Ext = []byte(reqExt) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_2", Ext: []byte(impExt)}) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_2", Ext: []byte(impExt)}) + + res, err := a.MakeRequests(&req, &reqInfo) + + assert.Empty(t, err, "Errors array should be empty") + assert.Len(t, res, 2, "Two request should be returned") + + var error error + var reqData1 *openrtb.BidRequest + error = json.Unmarshal(res[0].Body, &reqData1) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt1 *appnexusReqExt + error = json.Unmarshal(reqData1.Ext, &reqDataExt1) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId1 := reqDataExt1.Appnexus.AdPodId + + var reqData2 *openrtb.BidRequest + error = json.Unmarshal(res[1].Body, &reqData2) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt2 *appnexusReqExt + error = json.Unmarshal(reqData2.Ext, &reqDataExt2) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId2 := reqDataExt2.Appnexus.AdPodId + + assert.NotEqual(t, adPodId1, adPodId2, "AdPod id should be different for different pods") +} + +func TestVideoTwoPodsManyImps(t *testing.T) { + var a AppNexusAdapter + a.URI = "http://test.com/openrtb2" + a.hbSource = 5 + + var reqInfo adapters.ExtraRequestInfo + reqInfo.PbsEntryPoint = "video" + + var req openrtb.BidRequest + req.ID = "test_id" + + reqExt := `{"prebid":{}}` + impExt := `{"bidder":{"placementId":123}}` + req.Ext = []byte(reqExt) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_2", Ext: []byte(impExt)}) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_2", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_3", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_4", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_5", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_6", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_7", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_8", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_9", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_10", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_11", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_12", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_13", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_14", Ext: []byte(impExt)}) + + res, err := a.MakeRequests(&req, &reqInfo) + + assert.Empty(t, err, "Errors array should be empty") + assert.Len(t, res, 3, "Three requests should be returned") + + var error error + var reqData1 *openrtb.BidRequest + error = json.Unmarshal(res[0].Body, &reqData1) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt1 *appnexusReqExt + error = json.Unmarshal(reqData1.Ext, &reqDataExt1) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + var reqData2 *openrtb.BidRequest + error = json.Unmarshal(res[1].Body, &reqData2) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt2 *appnexusReqExt + error = json.Unmarshal(reqData2.Ext, &reqDataExt2) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + var reqData3 *openrtb.BidRequest + error = json.Unmarshal(res[2].Body, &reqData3) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt3 *appnexusReqExt + error = json.Unmarshal(reqData3.Ext, &reqDataExt3) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId1 := reqDataExt1.Appnexus.AdPodId + adPodId2 := reqDataExt2.Appnexus.AdPodId + adPodId3 := reqDataExt3.Appnexus.AdPodId + + podIds := make(map[string]int) + podIds[adPodId1] = podIds[adPodId1] + 1 + podIds[adPodId2] = podIds[adPodId2] + 1 + podIds[adPodId3] = podIds[adPodId3] + 1 + + assert.Len(t, podIds, 2, "Incorrect number of unique pod ids") +} + // ---------------------------------------------------------------------------- // Code below this line tests the legacy, non-openrtb code flow. It can be deleted after we // clean up the existing code and make everything openrtb. diff --git a/adapters/appnexus/appnexusplatformtest/video/simple-video.json b/adapters/appnexus/appnexusplatformtest/video/simple-video.json deleted file mode 100644 index 7ee192be2c1..00000000000 --- a/adapters/appnexus/appnexusplatformtest/video/simple-video.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - { - "id": "test-imp-id", - "video": { - "mimes": ["video/mp4"], - "minduration": 15, - "maxduration": 30, - "protocols": [2, 3, 5, 6, 7, 8], - "w": 940, - "h": 560 - }, - "ext": { - "bidder": { - "placement_id": 1 - } - } - } - ] - }, - - "httpCalls": [ - { - "expectedRequest": { - "uri": "http://ib.adnxs.com/openrtb2", - "body": { - "id": "test-request-id", - "ext": { - "appnexus": { - "hb_source": 9 - }, - "prebid": {} - }, - "imp": [ - { - "id": "test-imp-id", - "video": { - "mimes": ["video/mp4"], - "minduration": 15, - "maxduration": 30, - "protocols": [2, 3, 5, 6, 7, 8], - "w": 940, - "h": 560 - }, - "ext": { - "appnexus": { - "placement_id": 1 - } - } - } - ] - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "test-request-id", - "seatbid": [ - { - "seat": "958", - "bid": [{ - "id": "7706636740145184841", - "impid": "test-imp-id", - "price": 0.500000, - "adid": "29681110", - "adm": "some-test-ad", - "adomain": ["appnexus.com"], - "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", - "h": 250, - "w": 300, - "cat": ["IAB9-1"], - "ext": { - "appnexus": { - "brand_id": 9, - "brand_category_id": 9, - "auction_id": 8189378542222915032, - "bid_ad_type": 1, - "bidder_id": 2, - "ranking_price": 0.000000, - "deal_priority": 5 - } - } - }] - } - ], - "bidid": "5778926625248726496", - "cur": "USD" - } - } - } - ], - - "expectedBidResponses": [ - { - "currency": "USD", - "bids": [ - { - "bid": { - "id": "7706636740145184841", - "impid": "test-imp-id", - "price": 0.5, - "adm": "some-test-ad", - "adid": "29681110", - "adomain": ["appnexus.com"], - "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", - "w": 300, - "h": 250, - "cat": ["IAB5-3"], - "ext": { - "appnexus": { - "brand_id": 9, - "brand_category_id": 9, - "auction_id": 8189378542222915032, - "bid_ad_type": 1, - "bidder_id": 2, - "ranking_price": 0.000000, - "deal_priority": 5 - } - } - }, - "type": "video" - } - ] - } - ] - } \ No newline at end of file