diff --git a/analytics/pubstack/pubstack_module_test.go b/analytics/pubstack/pubstack_module_test.go index e2b4e118c6..c07a7c6c7b 100644 --- a/analytics/pubstack/pubstack_module_test.go +++ b/analytics/pubstack/pubstack_module_test.go @@ -99,7 +99,7 @@ func TestNewModuleSuccess(t *testing.T) { { ImpId: "123", StatusCode: 34, - Ext: &openrtb_ext.NonBidExt{Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{}}}, + Ext: openrtb_ext.ExtNonBid{Prebid: openrtb_ext.ExtNonBidPrebid{Bid: openrtb_ext.ExtNonBidPrebidBid{}}}, }, }, }, diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index b596ec793c..21b7b01d7d 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -117,6 +117,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h // We can respect timeouts more accurately if we note the *real* start time, and use it // to compute the auction timeout. start := time.Now() + seatNonBid := &openrtb_ext.SeatNonBidBuilder{} hookExecutor := hookexecution.NewHookExecutor(deps.hookExecutionPlanBuilder, hookexecution.EndpointAmp, deps.metricsEngine) @@ -138,6 +139,12 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h activityControl := privacy.ActivityControl{} defer func() { + // if AmpObject.AuctionResponse is nil then collect nonbids from all stage outcomes and set it in the AmpObject.SeatNonBid + // Nil AmpObject.AuctionResponse indicates the occurrence of a fatal error. + if ao.AuctionResponse == nil { + seatNonBid.Append(getNonBidsFromStageOutcomes(hookExecutor.GetOutcomes())) + ao.SeatNonBid = seatNonBid.Get() + } deps.metricsEngine.RecordRequest(labels) deps.metricsEngine.RecordRequestTime(labels, time.Since(start)) deps.analytics.LogAmpObject(&ao, activityControl) @@ -165,7 +172,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h // Process reject after parsing amp request, so we can use reqWrapper. // There is no body for AMP requests, so we pass a nil body and ignore the return value. if rejectErr != nil { - labels, ao = rejectAmpRequest(*rejectErr, w, hookExecutor, reqWrapper, nil, labels, ao, nil) + labels, ao = rejectAmpRequest(*rejectErr, w, hookExecutor, reqWrapper, nil, labels, ao, nil, *seatNonBid) return } @@ -286,8 +293,9 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h var response *openrtb2.BidResponse if auctionResponse != nil { response = auctionResponse.BidResponse + seatNonBid.Append(auctionResponse.SeatNonBid) + } - ao.SeatNonBid = auctionResponse.GetSeatNonBid() ao.AuctionResponse = response rejectErr, isRejectErr := hookexecution.CastRejectErr(err) if err != nil && !isRejectErr { @@ -307,15 +315,21 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h glog.Errorf("/openrtb2/amp Critical error: %v", err) ao.Status = http.StatusInternalServerError ao.Errors = append(ao.Errors, err) + if ao.AuctionResponse != nil { + // this check ensures that we collect nonBids from stageOutcomes only once. + // there could be a case where ao.AuctionResponse nil and reqWrapper.RebuildRequest returns error + seatNonBid.Append(getNonBidsFromStageOutcomes(hookExecutor.GetOutcomes())) + ao.SeatNonBid = seatNonBid.Get() + } return } if isRejectErr { - labels, ao = rejectAmpRequest(*rejectErr, w, hookExecutor, reqWrapper, account, labels, ao, errL) + labels, ao = rejectAmpRequest(*rejectErr, w, hookExecutor, reqWrapper, account, labels, ao, errL, *seatNonBid) return } - labels, ao = sendAmpResponse(w, hookExecutor, auctionResponse, reqWrapper, account, labels, ao, errL) + labels, ao = sendAmpResponse(w, hookExecutor, auctionResponse, reqWrapper, account, labels, ao, errL, *seatNonBid) } func rejectAmpRequest( @@ -327,12 +341,13 @@ func rejectAmpRequest( labels metrics.Labels, ao analytics.AmpObject, errs []error, + seatNonBid openrtb_ext.SeatNonBidBuilder, ) (metrics.Labels, analytics.AmpObject) { response := &openrtb2.BidResponse{NBR: openrtb3.NoBidReason(rejectErr.NBR).Ptr()} ao.AuctionResponse = response ao.Errors = append(ao.Errors, rejectErr) - return sendAmpResponse(w, hookExecutor, &exchange.AuctionResponse{BidResponse: response}, reqWrapper, account, labels, ao, errs) + return sendAmpResponse(w, hookExecutor, &exchange.AuctionResponse{BidResponse: response}, reqWrapper, account, labels, ao, errs, seatNonBid) } func sendAmpResponse( @@ -344,6 +359,7 @@ func sendAmpResponse( labels metrics.Labels, ao analytics.AmpObject, errs []error, + seatNonBid openrtb_ext.SeatNonBidBuilder, ) (metrics.Labels, analytics.AmpObject) { var response *openrtb2.BidResponse if auctionResponse != nil { @@ -371,6 +387,8 @@ func sendAmpResponse( glog.Errorf("/openrtb2/amp Critical error unpacking targets: %v", err) ao.Errors = append(ao.Errors, fmt.Errorf("Critical error while unpacking AMP targets: %v", err)) ao.Status = http.StatusInternalServerError + seatNonBid.Append(getNonBidsFromStageOutcomes(hookExecutor.GetOutcomes())) + ao.SeatNonBid = seatNonBid.Get() return labels, ao } for key, value := range bidExt.Prebid.Targeting { @@ -399,7 +417,7 @@ func sendAmpResponse( } // Now JSONify the targets for the AMP response. ampResponse := AmpResponse{Targeting: targets} - ao, ampResponse.ORTB2.Ext = getExtBidResponse(hookExecutor, auctionResponse, reqWrapper, account, ao, errs) + ao, ampResponse.ORTB2.Ext = getExtBidResponse(hookExecutor, auctionResponse, reqWrapper, account, ao, errs, seatNonBid) ao.AmpTargetingValues = targets @@ -430,6 +448,7 @@ func getExtBidResponse( account *config.Account, ao analytics.AmpObject, errs []error, + seatNonBid openrtb_ext.SeatNonBidBuilder, ) (analytics.AmpObject, openrtb_ext.ExtBidResponse) { var response *openrtb2.BidResponse if auctionResponse != nil { @@ -462,6 +481,7 @@ func getExtBidResponse( Warnings: warnings, } + stageOutcomes := hookExecutor.GetOutcomes() // add debug information if requested if reqWrapper != nil { if reqWrapper.Test == 1 && eRErr == nil { @@ -473,7 +493,6 @@ func getExtBidResponse( } } - stageOutcomes := hookExecutor.GetOutcomes() ao.HookExecutionOutcome = stageOutcomes modules, warns, err := hookexecution.GetModulesJSON(stageOutcomes, reqWrapper.BidRequest, account) if err != nil { @@ -489,8 +508,12 @@ func getExtBidResponse( } } - setSeatNonBid(&extBidResponse, reqWrapper, auctionResponse) - + // collect seatNonBid from all stage-outcomes and set in the response.ext.prebid + seatNonBid.Append(getNonBidsFromStageOutcomes(stageOutcomes)) + ao.SeatNonBid = seatNonBid.Get() + if returnAllBidStatus(reqWrapper) { + setSeatNonBid(&extBidResponse, ao.SeatNonBid) + } return ao, extBidResponse } @@ -871,23 +894,3 @@ func setTrace(req *openrtb2.BidRequest, value string) error { return nil } - -// setSeatNonBid populates bidresponse.ext.prebid.seatnonbid if bidrequest.ext.prebid.returnallbidstatus is true -func setSeatNonBid(finalExtBidResponse *openrtb_ext.ExtBidResponse, request *openrtb_ext.RequestWrapper, auctionResponse *exchange.AuctionResponse) bool { - if finalExtBidResponse == nil || auctionResponse == nil || request == nil { - return false - } - reqExt, err := request.GetRequestExt() - if err != nil { - return false - } - prebid := reqExt.GetPrebid() - if prebid == nil || !prebid.ReturnAllBidStatus { - return false - } - if finalExtBidResponse.Prebid == nil { - finalExtBidResponse.Prebid = &openrtb_ext.ExtResponsePrebid{} - } - finalExtBidResponse.Prebid.SeatNonBid = auctionResponse.GetSeatNonBid() - return true -} diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 3fe8a629f0..bcf9bb4b9a 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -1451,8 +1451,11 @@ func (s formatOverrideSpec) execute(t *testing.T) { } type mockAmpExchange struct { - lastRequest *openrtb2.BidRequest - requestExt json.RawMessage + lastRequest *openrtb2.BidRequest + requestExt json.RawMessage + returnError bool + setBidRequestToNil bool + seatNonBid openrtb_ext.SeatNonBidBuilder } var expectedErrorsFromHoldAuction map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage = map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{ @@ -1465,6 +1468,13 @@ var expectedErrorsFromHoldAuction map[openrtb_ext.BidderName][]openrtb_ext.ExtBi } func (m *mockAmpExchange) HoldAuction(ctx context.Context, auctionRequest *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { + if m.returnError { + return nil, hookexecution.RejectError{ + NBR: 1, + Stage: hooks.StageBidderRequest.String(), + Hook: hookexecution.HookID{ModuleCode: "foobar", HookImplCode: "foo"}, + } + } r := auctionRequest.BidRequestWrapper m.lastRequest = r.BidRequest @@ -1498,7 +1508,11 @@ func (m *mockAmpExchange) HoldAuction(ctx context.Context, auctionRequest *excha response.Ext = json.RawMessage(fmt.Sprintf(`{"debug": {"httpcalls": {}, "resolvedrequest": %s}}`, resolvedRequest)) } - return &exchange.AuctionResponse{BidResponse: response}, nil + if m.setBidRequestToNil { + auctionRequest.BidRequestWrapper.BidRequest = nil + } + + return &exchange.AuctionResponse{BidResponse: response, SeatNonBid: m.seatNonBid}, nil } type mockAmpExchangeWarnings struct{} @@ -1674,16 +1688,21 @@ func (logger mockLogger) Shutdown() {} func TestBuildAmpObject(t *testing.T) { testCases := []struct { - description string - inTagId string - exchange *mockAmpExchange - inStoredRequest json.RawMessage - expectedAmpObject *analytics.AmpObject + description string + inTagId string + exchange *mockAmpExchange + inStoredRequest json.RawMessage + planBuilder hooks.ExecutionPlanBuilder + returnErrorFromHoldAuction bool + setRequestToNil bool + seatNonBidFromHoldAuction openrtb_ext.SeatNonBidBuilder + expectedAmpObject *analytics.AmpObject }{ { description: "Stored Amp request with nil body. Only the error gets logged", inTagId: "test", inStoredRequest: nil, + planBuilder: hooks.EmptyPlanBuilder{}, expectedAmpObject: &analytics.AmpObject{ Status: http.StatusOK, Errors: []error{fmt.Errorf("unexpected end of JSON input")}, @@ -1693,24 +1712,163 @@ func TestBuildAmpObject(t *testing.T) { description: "Stored Amp request with no imps that should return error. Only the error gets logged", inTagId: "test", inStoredRequest: json.RawMessage(`{"id":"some-request-id","site":{"page":"prebid.org"},"imp":[],"tmax":500}`), + planBuilder: hooks.EmptyPlanBuilder{}, expectedAmpObject: &analytics.AmpObject{ Status: http.StatusOK, Errors: []error{fmt.Errorf("data for tag_id='test' does not define the required imp array")}, }, }, { - description: "Wrong tag_id, error gets logged", + description: "Wrong tag_id, error and seatnonbid gets logged", inTagId: "unknown", inStoredRequest: json.RawMessage(`{"id":"some-request-id","site":{"page":"prebid.org"},"imp":[{"id":"some-impression-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}}],"tmax":500}`), + planBuilder: mockPlanBuilder{entrypointPlan: makePlan[hookstage.Entrypoint](mockSeatNonBidHook{})}, expectedAmpObject: &analytics.AmpObject{ Status: http.StatusOK, Errors: []error{fmt.Errorf("unexpected end of JSON input")}, + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 100, + }, + }, + }, + }, + }, + }, + { + description: "AmpObject should contain seatNonBid when holdAuction returns error", + inTagId: "test", + inStoredRequest: json.RawMessage(`{"id":"some-request-id","site":{"page":"prebid.org"},"imp":[{"id":"some-impression-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}}],"tmax":500}`), + planBuilder: mockPlanBuilder{entrypointPlan: makePlan[hookstage.Entrypoint](mockSeatNonBidHook{})}, + returnErrorFromHoldAuction: true, + expectedAmpObject: &analytics.AmpObject{ + Status: http.StatusInternalServerError, + Errors: []error{ + fmt.Errorf("[Module foobar (hook: foo) rejected request with code 1 at bidder_request stage]"), + }, + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Device: &openrtb2.Device{ + IP: "192.0.2.1", + }, + Site: &openrtb2.Site{ + Page: "prebid.org", + Ext: json.RawMessage(`{"amp":1}`), + }, + Imp: []openrtb2.Imp{ + { + ID: "some-impression-id", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 300, + H: 250, + }, + }, + }, + Secure: func(val int8) *int8 { return &val }(1), //(*int8)(1), + Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}`), + }, + }, + AT: 1, + TMax: 500, + Ext: json.RawMessage(`{"prebid":{"cache":{"bids":{}},"channel":{"name":"amp","version":""},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"mediatypepricegranularity":{},"includewinners":true,"includebidderkeys":true}}}`), + }, + }, + Origin: "", + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 100, + }, + }, + }, + }, + }, + }, + { + description: "AmpObject should contain seatNonBid when RebuildRequest returns error after holdAuction", + inTagId: "test", + inStoredRequest: json.RawMessage(`{"id":"some-request-id","site":{"page":"prebid.org"},"imp":[{"id":"some-impression-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}}],"tmax":500}`), + planBuilder: mockPlanBuilder{entrypointPlan: makePlan[hookstage.Entrypoint](mockSeatNonBidHook{})}, + setRequestToNil: true, + seatNonBidFromHoldAuction: getNonBids(map[string][]openrtb_ext.NonBidParams{"pubmatic": {{Bid: &openrtb2.Bid{ImpID: "imp1"}, NonBidReason: 100}}}), + expectedAmpObject: &analytics.AmpObject{ + Status: http.StatusInternalServerError, + Errors: []error{ + fmt.Errorf("[Module foobar (hook: foo) rejected request with code 1 at bidder_request stage]"), + }, + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Device: &openrtb2.Device{ + IP: "192.0.2.1", + }, + Site: &openrtb2.Site{ + Page: "prebid.org", + Ext: json.RawMessage(`{"amp":1}`), + }, + Imp: []openrtb2.Imp{ + { + ID: "some-impression-id", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 300, + H: 250, + }, + }, + }, + Secure: func(val int8) *int8 { return &val }(1), + Ext: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}`), + }, + }, + AT: 1, + TMax: 500, + Ext: json.RawMessage(`{"prebid":{"cache":{"bids":{}},"channel":{"name":"amp","version":""},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"mediatypepricegranularity":{},"includewinners":true,"includebidderkeys":true}}}`), + }, + }, + AuctionResponse: &openrtb2.BidResponse{ + SeatBid: []openrtb2.SeatBid{{ + Bid: []openrtb2.Bid{{ + AdM: "", + Ext: json.RawMessage(`{ "prebid": {"targeting": { "hb_pb": "1.20", "hb_appnexus_pb": "1.20", "hb_cache_id": "some_id"}}}`), + }}, + Seat: "", + }}, + Ext: json.RawMessage(`{ "errors": {"openx":[ { "code": 1, "message": "The request exceeded the timeout allocated" } ] } }`), + }, + Origin: "", + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + { + ImpId: "imp", + StatusCode: 100, + }, + }, + }, + }, }, }, { description: "Valid stored Amp request, correct tag_id, a valid response should be logged", inTagId: "test", inStoredRequest: json.RawMessage(`{"id":"some-request-id","site":{"page":"prebid.org"},"imp":[{"id":"some-impression-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}}],"tmax":500}`), + planBuilder: hooks.EmptyPlanBuilder{}, expectedAmpObject: &analytics.AmpObject{ Status: http.StatusOK, Errors: nil, @@ -1767,6 +1925,7 @@ func TestBuildAmpObject(t *testing.T) { inTagId: "test", inStoredRequest: json.RawMessage(`{"id":"some-request-id","site":{"page":"prebid.org"},"imp":[{"id":"some-impression-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"prebid":{"bidder":{"appnexus":{"placementId":12883451}}}}}],"tmax":500}`), exchange: &mockAmpExchange{requestExt: json.RawMessage(`{ "prebid": {"targeting": { "test_key": "test_value", "hb_appnexus_pb": "9999" } }, "errors": {"openx":[ { "code": 1, "message": "The request exceeded the timeout allocated" } ] } }`)}, + planBuilder: hooks.EmptyPlanBuilder{}, expectedAmpObject: &analytics.AmpObject{ Status: http.StatusOK, Errors: nil, @@ -1828,9 +1987,13 @@ func TestBuildAmpObject(t *testing.T) { // Set up test, declare a new mock logger every time exchange := test.exchange if exchange == nil { - exchange = &mockAmpExchange{} + exchange = &mockAmpExchange{ + returnError: test.returnErrorFromHoldAuction, + setBidRequestToNil: test.setRequestToNil, + seatNonBid: test.seatNonBidFromHoldAuction, + } } - actualAmpObject, endpoint := ampObjectTestSetup(t, test.inTagId, test.inStoredRequest, false, exchange) + actualAmpObject, endpoint := ampObjectTestSetup(t, test.inTagId, test.inStoredRequest, false, exchange, test.planBuilder) // Run test endpoint(recorder, request, nil) @@ -1849,6 +2012,7 @@ func TestBuildAmpObject(t *testing.T) { assert.Equalf(t, test.expectedAmpObject.AuctionResponse, actualAmpObject.AuctionResponse, "Amp Object BidResponse doesn't match expected: %s\n", test.description) assert.Equalf(t, test.expectedAmpObject.AmpTargetingValues, actualAmpObject.AmpTargetingValues, "Amp Object AmpTargetingValues doesn't match expected: %s\n", test.description) assert.Equalf(t, test.expectedAmpObject.Origin, actualAmpObject.Origin, "Amp Object Origin field doesn't match expected: %s\n", test.description) + assert.Equalf(t, test.expectedAmpObject.SeatNonBid, actualAmpObject.SeatNonBid, "Amp Object SeatNonBid field doesn't match expected: %s\n", test.description) } } @@ -1904,13 +2068,13 @@ func TestIdGeneration(t *testing.T) { for _, test := range testCases { // Set up and run test - actualAmpObject, endpoint := ampObjectTestSetup(t, "test", test.givenInStoredRequest, test.givenGenerateRequestID, &mockAmpExchange{}) + actualAmpObject, endpoint := ampObjectTestSetup(t, "test", test.givenInStoredRequest, test.givenGenerateRequestID, &mockAmpExchange{}, hooks.EmptyPlanBuilder{}) endpoint(recorder, request, nil) assert.Equalf(t, test.expectedID, actualAmpObject.RequestWrapper.ID, "Bid Request ID is incorrect: %s\n", test.description) } } -func ampObjectTestSetup(t *testing.T, inTagId string, inStoredRequest json.RawMessage, generateRequestID bool, exchange *mockAmpExchange) (*analytics.AmpObject, httprouter.Handle) { +func ampObjectTestSetup(t *testing.T, inTagId string, inStoredRequest json.RawMessage, generateRequestID bool, exchange *mockAmpExchange, planBuilder hooks.ExecutionPlanBuilder) (*analytics.AmpObject, httprouter.Handle) { actualAmpObject := analytics.AmpObject{} logger := newMockLogger(&actualAmpObject, nil) @@ -1933,7 +2097,7 @@ func ampObjectTestSetup(t *testing.T, inTagId string, inStoredRequest json.RawMe []byte{}, openrtb_ext.BuildBidderMap(), empty_fetcher.EmptyFetcher{}, - hooks.EmptyPlanBuilder{}, + planBuilder, nil, ) return &actualAmpObject, endpoint @@ -2250,61 +2414,114 @@ func TestValidAmpResponseWhenRequestRejected(t *testing.T) { } } -func TestSendAmpResponse_LogsErrors(t *testing.T) { +func TestSendAmpResponse(t *testing.T) { + type want struct { + errors []error + status int + seatNonBid []openrtb_ext.SeatNonBid + } testCases := []struct { - description string - expectedErrors []error - expectedStatus int - writer http.ResponseWriter - request *openrtb2.BidRequest - response *openrtb2.BidResponse - hookExecutor hookexecution.HookStageExecutor + description string + writer http.ResponseWriter + request *openrtb2.BidRequest + response *openrtb2.BidResponse + hookExecutor hookexecution.HookStageExecutor + want want }{ { description: "Error logged when bid.ext unmarshal fails", - expectedErrors: []error{ - errors.New("Critical error while unpacking AMP targets: expect { or n, but found \""), + want: want{ + errors: []error{ + errors.New("Critical error while unpacking AMP targets: expect { or n, but found \""), + }, + status: http.StatusInternalServerError, }, - expectedStatus: http.StatusInternalServerError, - writer: httptest.NewRecorder(), - request: &openrtb2.BidRequest{ID: "some-id", Test: 1}, + writer: httptest.NewRecorder(), + request: &openrtb2.BidRequest{ID: "some-id", Test: 1}, response: &openrtb2.BidResponse{ID: "some-id", SeatBid: []openrtb2.SeatBid{ {Bid: []openrtb2.Bid{{Ext: json.RawMessage(`"hb_cache_id`)}}}, }}, - hookExecutor: &hookexecution.EmptyHookExecutor{}, + hookExecutor: hookexecution.EmptyHookExecutor{}, + }, + { + description: "Capture seatNonBid when bid.ext unmarshal fails", + want: want{ + errors: []error{ + errors.New("Critical error while unpacking AMP targets: expect { or n, but found \""), + }, + status: http.StatusInternalServerError, + seatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 100, + }, + }, + }, + }, + }, + writer: httptest.NewRecorder(), + request: &openrtb2.BidRequest{ID: "some-id", Test: 1}, + response: &openrtb2.BidResponse{ID: "some-id", SeatBid: []openrtb2.SeatBid{ + {Bid: []openrtb2.Bid{{Ext: json.RawMessage(`"hb_cache_id`)}}}, + }}, + hookExecutor: &mockStageExecutor{ + outcomes: []hookexecution.StageOutcome{ + { + Groups: []hookexecution.GroupOutcome{ + { + InvocationResults: []hookexecution.HookOutcome{ + { + Status: hookexecution.StatusSuccess, + SeatNonBid: getNonBids(map[string][]openrtb_ext.NonBidParams{"pubmatic": {{Bid: &openrtb2.Bid{ImpID: "imp"}, NonBidReason: 100}}}), + }, + }, + }, + }, + }, + }, + }, }, { description: "Error logged when test mode activated but no debug present in response", - expectedErrors: []error{ - errors.New("test set on request but debug not present in response"), + want: want{ + errors: []error{ + errors.New("test set on request but debug not present in response"), + }, + status: 0, }, - expectedStatus: 0, - writer: httptest.NewRecorder(), - request: &openrtb2.BidRequest{ID: "some-id", Test: 1}, - response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage("{}")}, - hookExecutor: &hookexecution.EmptyHookExecutor{}, + writer: httptest.NewRecorder(), + request: &openrtb2.BidRequest{ID: "some-id", Test: 1}, + response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage("{}")}, + hookExecutor: &hookexecution.EmptyHookExecutor{}, }, { description: "Error logged when response encoding fails", - expectedErrors: []error{ - errors.New("/openrtb2/amp Failed to send response: failed writing response"), + want: want{ + errors: []error{ + errors.New("/openrtb2/amp Failed to send response: failed writing response"), + }, + status: 0, }, - expectedStatus: 0, - writer: errorResponseWriter{}, - request: &openrtb2.BidRequest{ID: "some-id", Test: 1}, - response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage(`{"debug": {}}`)}, - hookExecutor: &hookexecution.EmptyHookExecutor{}, + writer: errorResponseWriter{}, + request: &openrtb2.BidRequest{ID: "some-id", Test: 1}, + response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage(`{"debug": {}}`)}, + hookExecutor: &hookexecution.EmptyHookExecutor{}, }, { description: "Error logged if hook enrichment returns warnings", - expectedErrors: []error{ - errors.New("Value is not a string: 1"), - errors.New("Value is not a boolean: active"), - }, - expectedStatus: 0, - writer: httptest.NewRecorder(), - request: &openrtb2.BidRequest{ID: "some-id", Ext: json.RawMessage(`{"prebid": {"debug": "active", "trace": 1}}`)}, - response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage("{}")}, + want: want{ + errors: []error{ + errors.New("Value is not a string: 1"), + errors.New("Value is not a boolean: active"), + }, + status: 0, + }, + writer: httptest.NewRecorder(), + request: &openrtb2.BidRequest{ID: "some-id", Ext: json.RawMessage(`{"prebid": {"debug": "active", "trace": 1}}`)}, + response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage("{}")}, hookExecutor: &mockStageExecutor{ outcomes: []hookexecution.StageOutcome{ { @@ -2338,10 +2555,11 @@ func TestSendAmpResponse_LogsErrors(t *testing.T) { account := &config.Account{DebugAllow: true} reqWrapper := openrtb_ext.RequestWrapper{BidRequest: test.request} - _, ao = sendAmpResponse(test.writer, test.hookExecutor, &exchange.AuctionResponse{BidResponse: test.response}, &reqWrapper, account, labels, ao, nil) + _, ao = sendAmpResponse(test.writer, test.hookExecutor, &exchange.AuctionResponse{BidResponse: test.response}, &reqWrapper, account, labels, ao, nil, openrtb_ext.SeatNonBidBuilder{}) - assert.Equal(t, test.expectedErrors, ao.Errors, "Invalid errors.") - assert.Equal(t, test.expectedStatus, ao.Status, "Invalid HTTP response status.") + assert.Equal(t, test.want.errors, ao.Errors, "Invalid errors.") + assert.Equal(t, test.want.status, ao.Status, "Invalid HTTP response status.") + assert.Equal(t, test.want.seatNonBid, ao.SeatNonBid, "Invalid seatNonBid.") }) } } @@ -2358,63 +2576,205 @@ func (e errorResponseWriter) Write(bytes []byte) (int, error) { func (e errorResponseWriter) WriteHeader(statusCode int) {} -func TestSetSeatNonBid(t *testing.T) { +func TestGetExtBidResponse(t *testing.T) { type args struct { - finalExtBidResponse *openrtb_ext.ExtBidResponse - request *openrtb_ext.RequestWrapper - auctionResponse *exchange.AuctionResponse + hookExecutor hookexecution.HookStageExecutor + auctionResponse *exchange.AuctionResponse + reqWrapper *openrtb_ext.RequestWrapper + account *config.Account + ao analytics.AmpObject + errs []error + seatNonBid openrtb_ext.SeatNonBidBuilder + } + type want struct { + respExt openrtb_ext.ExtBidResponse + ao analytics.AmpObject } tests := []struct { name string args args - want bool + want want }{ { - name: "nil-auctionResponse", - args: args{auctionResponse: nil}, - want: false, - }, - { - name: "nil-request", - args: args{auctionResponse: &exchange.AuctionResponse{}, request: nil}, - want: false, - }, - { - name: "invalid-req-ext", - args: args{auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`invalid json`)}}}, - want: false, - }, - { - name: "nil-prebid", - args: args{auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: nil}}}, - want: false, - }, - { - name: "returnallbidstatus-is-false", - args: args{auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid" : {"returnallbidstatus" : false}}`)}}}, - want: false, + name: "seatNonBid: returnallbidstatus is true and nonBids is empty", + args: args{ + hookExecutor: mockStageExecutor{ + outcomes: []hookexecution.StageOutcome{}, + }, + auctionResponse: &exchange.AuctionResponse{ + BidResponse: &openrtb2.BidResponse{ + Ext: json.RawMessage(`{}`), + }, + }, + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"returnallbidstatus": true}}`), + }, + }, + }, + want: want{ + respExt: openrtb_ext.ExtBidResponse{ + Warnings: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage), + }, + ao: analytics.AmpObject{ + SeatNonBid: nil, + }, + }, }, { - name: "finalExtBidResponse-is-nil", - args: args{finalExtBidResponse: nil}, - want: false, + name: "seatNonBid: returnallbidstatus is true and nonBids is present", + args: args{ + hookExecutor: mockStageExecutor{ + outcomes: []hookexecution.StageOutcome{ + { + Groups: []hookexecution.GroupOutcome{ + { + InvocationResults: []hookexecution.HookOutcome{ + { + Status: hookexecution.StatusSuccess, + SeatNonBid: getNonBids(map[string][]openrtb_ext.NonBidParams{"pubmatic": {{Bid: &openrtb2.Bid{ImpID: "imp"}, NonBidReason: 100}}}), + }, + }, + }, + }, + }, + }, + }, + auctionResponse: &exchange.AuctionResponse{ + BidResponse: &openrtb2.BidResponse{ + Ext: json.RawMessage(`{}`), + }, + }, + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"returnallbidstatus": true}}`), + }, + }, + ao: analytics.AmpObject{}, + seatNonBid: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "pubmatic": {{Bid: &openrtb2.Bid{ImpID: "imp1"}, NonBidReason: 100}}, + }), + }, + want: want{ + respExt: openrtb_ext.ExtBidResponse{ + Warnings: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage), + Prebid: &openrtb_ext.ExtResponsePrebid{ + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + { + ImpId: "imp", + StatusCode: 100, + }, + }, + Seat: "pubmatic", + }, + }, + }, + }, + ao: analytics.AmpObject{ + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + { + ImpId: "imp", + StatusCode: 100, + }, + }, + Seat: "pubmatic", + }, + }, + }, + }, }, { - name: "returnallbidstatus-is-true-and-responseExt.Prebid-is-nil", - args: args{finalExtBidResponse: &openrtb_ext.ExtBidResponse{Prebid: nil}, auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid" : {"returnallbidstatus" : true}}`)}}}, - want: true, + name: "seatNonBid: returnallbidstatus is false and nonBids is present", + args: args{ + hookExecutor: mockStageExecutor{}, + auctionResponse: &exchange.AuctionResponse{ + BidResponse: &openrtb2.BidResponse{ + Ext: json.RawMessage(`{}`), + }, + }, + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid":{"returnallbidstatus": false}}`), + }, + }, + ao: analytics.AmpObject{}, + seatNonBid: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "pubmatic": {{Bid: &openrtb2.Bid{ImpID: "imp1"}, NonBidReason: 100}}, + }), + }, + want: want{ + respExt: openrtb_ext.ExtBidResponse{ + Warnings: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage), + }, + ao: analytics.AmpObject{ + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + }, + Seat: "pubmatic", + }, + }, + }, + }, }, { - name: "returnallbidstatus-is-true-and-responseExt.Prebid-is-not-nil", - args: args{finalExtBidResponse: &openrtb_ext.ExtBidResponse{Prebid: nil}, auctionResponse: &exchange.AuctionResponse{}, request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid" : {"returnallbidstatus" : true}}`)}}}, - want: true, + name: "seatNonBid: reqWrapper is nil and nonBids is present then AmpObject should contain seatnonbid", + args: args{ + hookExecutor: mockStageExecutor{ + outcomes: []hookexecution.StageOutcome{}, + }, + auctionResponse: &exchange.AuctionResponse{ + BidResponse: &openrtb2.BidResponse{ + Ext: json.RawMessage(`{}`), + }, + }, + reqWrapper: nil, + seatNonBid: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "pubmatic": {{Bid: &openrtb2.Bid{ImpID: "imp1"}, NonBidReason: 100}}, + }), + ao: analytics.AmpObject{}, + }, + want: want{ + respExt: openrtb_ext.ExtBidResponse{ + Warnings: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage), + }, + ao: analytics.AmpObject{ + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + }, + Seat: "pubmatic", + }, + }, + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := setSeatNonBid(tt.args.finalExtBidResponse, tt.args.request, tt.args.auctionResponse); got != tt.want { - t.Errorf("setSeatNonBid() = %v, want %v", got, tt.want) - } + ao, ext := getExtBidResponse(tt.args.hookExecutor, tt.args.auctionResponse, tt.args.reqWrapper, tt.args.account, tt.args.ao, tt.args.errs, tt.args.seatNonBid) + assert.Equal(t, tt.want.respExt, ext, "Found invalid bidResponseExt") + assert.Equal(t, tt.want.ao.SeatNonBid, ao.SeatNonBid, "Found invalid seatNonBid in ampObject") }) } } diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index fa61c484f0..e92ea57dd8 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -164,6 +164,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http // We can respect timeouts more accurately if we note the *real* start time, and use it // to compute the auction timeout. start := time.Now() + seatNonBid := &openrtb_ext.SeatNonBidBuilder{} hookExecutor := hookexecution.NewHookExecutor(deps.hookExecutionPlanBuilder, hookexecution.EndpointAuction, deps.metricsEngine) @@ -181,8 +182,15 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http RequestStatus: metrics.RequestStatusOK, } + foundErrors := false activityControl := privacy.ActivityControl{} defer func() { + // if AuctionObject.Response is nil then collect nonbids from all stage outcomes and set it in the AuctionObject. + // Nil AuctionObject.Response indicates the occurrence of a fatal error. + if foundErrors { + seatNonBid.Append(getNonBidsFromStageOutcomes(hookExecutor.GetOutcomes())) + ao.SeatNonBid = seatNonBid.Get() + } deps.metricsEngine.RecordRequest(labels) deps.metricsEngine.RecordRequestTime(labels, time.Since(start)) deps.analytics.LogAuctionObject(&ao, activityControl) @@ -193,12 +201,13 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http req, impExtInfoMap, storedAuctionResponses, storedBidResponses, bidderImpReplaceImp, account, errL := deps.parseRequest(r, &labels, hookExecutor) if errortypes.ContainsFatalError(errL) && writeError(errL, w, &labels) { + foundErrors = true return } if rejectErr := hookexecution.FindFirstRejectOrNil(errL); rejectErr != nil { ao.RequestWrapper = req - labels, ao = rejectAuctionRequest(*rejectErr, w, hookExecutor, req.BidRequest, account, labels, ao) + labels, ao = rejectAuctionRequest(*rejectErr, w, hookExecutor, req.BidRequest, account, labels, ao, seatNonBid) return } @@ -234,6 +243,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http // Set Integration Information err := deps.setIntegrationType(req, account) if err != nil { + foundErrors = true errL = append(errL, err) writeError(errL, w, &labels) return @@ -272,11 +282,12 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http var response *openrtb2.BidResponse if auctionResponse != nil { response = auctionResponse.BidResponse + seatNonBid.Append(auctionResponse.SeatNonBid) } ao.Response = response - ao.SeatNonBid = auctionResponse.GetSeatNonBid() rejectErr, isRejectErr := hookexecution.CastRejectErr(err) if err != nil && !isRejectErr { + foundErrors = true if errortypes.ReadCode(err) == errortypes.BadInputErrorCode { writeError([]error{err}, w, &labels) return @@ -289,15 +300,11 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http ao.Errors = append(ao.Errors, err) return } else if isRejectErr { - labels, ao = rejectAuctionRequest(*rejectErr, w, hookExecutor, req.BidRequest, account, labels, ao) + labels, ao = rejectAuctionRequest(*rejectErr, w, hookExecutor, req.BidRequest, account, labels, ao, seatNonBid) return } - err = setSeatNonBidRaw(req, auctionResponse) - if err != nil { - glog.Errorf("Error setting seat non-bid: %v", err) - } - labels, ao = sendAuctionResponse(w, hookExecutor, response, req.BidRequest, account, labels, ao) + labels, ao = sendAuctionResponse(w, hookExecutor, response, req.BidRequest, account, labels, ao, seatNonBid) } // setSeatNonBidRaw is transitional function for setting SeatNonBid inside bidResponse.Ext @@ -305,18 +312,20 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http // 1. today exchange.HoldAuction prepares and marshals some piece of response.Ext which is then used by auction.go, amp_auction.go and video_auction.go // 2. As per discussion with Prebid Team we are planning to move away from - HoldAuction building openrtb2.BidResponse. instead respective auction modules will build this object // 3. So, we will need this method to do first, unmarshalling of response.Ext -func setSeatNonBidRaw(request *openrtb_ext.RequestWrapper, auctionResponse *exchange.AuctionResponse) error { - if auctionResponse == nil || auctionResponse.BidResponse == nil { +func setSeatNonBidRaw(request *openrtb_ext.RequestWrapper, response *openrtb2.BidResponse, nonBids []openrtb_ext.SeatNonBid) error { + if response == nil || !returnAllBidStatus(request) { return nil } + if response.Ext == nil { + response.Ext = json.RawMessage(`{}`) + } // unmarshalling is required here, until we are moving away from bidResponse.Ext, which is populated // by HoldAuction - response := auctionResponse.BidResponse respExt := &openrtb_ext.ExtBidResponse{} if err := jsonutil.Unmarshal(response.Ext, &respExt); err != nil { return err } - if setSeatNonBid(respExt, request, auctionResponse) { + if setSeatNonBid(respExt, nonBids) { if respExtJson, err := jsonutil.Marshal(respExt); err == nil { response.Ext = respExtJson return nil @@ -335,6 +344,7 @@ func rejectAuctionRequest( account *config.Account, labels metrics.Labels, ao analytics.AuctionObject, + seatNonBid *openrtb_ext.SeatNonBidBuilder, ) (metrics.Labels, analytics.AuctionObject) { response := &openrtb2.BidResponse{NBR: openrtb3.NoBidReason(rejectErr.NBR).Ptr()} if request != nil { @@ -344,7 +354,21 @@ func rejectAuctionRequest( ao.Response = response ao.Errors = append(ao.Errors, rejectErr) - return sendAuctionResponse(w, hookExecutor, response, request, account, labels, ao) + return sendAuctionResponse(w, hookExecutor, response, request, account, labels, ao, seatNonBid) +} + +func getNonBidsFromStageOutcomes(stageOutcomes []hookexecution.StageOutcome) openrtb_ext.SeatNonBidBuilder { + seatNonBid := openrtb_ext.SeatNonBidBuilder{} + for _, stageOutcome := range stageOutcomes { + for _, groups := range stageOutcome.Groups { + for _, result := range groups.InvocationResults { + if result.Status == hookexecution.StatusSuccess { + seatNonBid.Append(result.SeatNonBid) + } + } + } + } + return seatNonBid } func sendAuctionResponse( @@ -355,12 +379,20 @@ func sendAuctionResponse( account *config.Account, labels metrics.Labels, ao analytics.AuctionObject, + seatNonBid *openrtb_ext.SeatNonBidBuilder, ) (metrics.Labels, analytics.AuctionObject) { hookExecutor.ExecuteAuctionResponseStage(response) + stageOutcomes := hookExecutor.GetOutcomes() + seatNonBid.Append(getNonBidsFromStageOutcomes(stageOutcomes)) + ao.SeatNonBid = seatNonBid.Get() + if response != nil { - stageOutcomes := hookExecutor.GetOutcomes() ao.HookExecutionOutcome = stageOutcomes + err := setSeatNonBidRaw(ao.RequestWrapper, response, ao.SeatNonBid) + if err != nil { + glog.Errorf("Error setting seatNonBid in responseExt: %v", err) + } ext, warns, err := hookexecution.EnrichExtBidResponse(response.Ext, stageOutcomes, request, account) if err != nil { @@ -2005,3 +2037,31 @@ func checkIfAppRequest(request []byte) (bool, error) { } return false, nil } + +// setSeatNonBid populates bidresponse.ext.prebid.seatnonbid +func setSeatNonBid(finalExtBidResponse *openrtb_ext.ExtBidResponse, seatNonBid []openrtb_ext.SeatNonBid) bool { + if finalExtBidResponse == nil || len(seatNonBid) == 0 { + return false + } + if finalExtBidResponse.Prebid == nil { + finalExtBidResponse.Prebid = &openrtb_ext.ExtResponsePrebid{} + } + finalExtBidResponse.Prebid.SeatNonBid = seatNonBid + return true +} + +// returnAllBidStatus function returns the value of bidrequest.ext.prebid.returnallbidstatus flag +func returnAllBidStatus(request *openrtb_ext.RequestWrapper) bool { + if request == nil { + return false + } + reqExt, err := request.GetRequestExt() + if err != nil { + return false + } + prebid := reqExt.GetPrebid() + if prebid == nil { + return false + } + return prebid.ReturnAllBidStatus +} diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 2df4813d15..3b23665d1b 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -4948,7 +4948,7 @@ func TestValidResponseAfterExecutingStages(t *testing.T) { } } -func TestSendAuctionResponse_LogsErrors(t *testing.T) { +func TestSendAuctionResponse(t *testing.T) { hookExecutor := &mockStageExecutor{ outcomes: []hookexecution.StageOutcome{ { @@ -4965,6 +4965,14 @@ func TestSendAuctionResponse_LogsErrors(t *testing.T) { Status: hookexecution.StatusSuccess, Action: hookexecution.ActionNone, Warnings: []string{"warning message"}, + SeatNonBid: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "pubmatic": { + { + Bid: &openrtb2.Bid{ImpID: "imp1"}, + NonBidReason: int(openrtb_ext.ResponseRejectedCategoryMappingInvalid), + }, + }, + }), }, }, }, @@ -4972,50 +4980,134 @@ func TestSendAuctionResponse_LogsErrors(t *testing.T) { }, }, } - testCases := []struct { - description string - expectedErrors []error - expectedStatus int - request *openrtb2.BidRequest - response *openrtb2.BidResponse - hookExecutor hookexecution.HookStageExecutor + description string + expectedAuctionObject analytics.AuctionObject + expectedResponseBody string + request *openrtb2.BidRequest + response *openrtb2.BidResponse + hookExecutor hookexecution.HookStageExecutor + auctionObject analytics.AuctionObject }{ { description: "Error logged if hook enrichment fails", - expectedErrors: []error{ - errors.New("Failed to enrich Bid Response with hook debug information: Invalid JSON Document"), - errors.New("/openrtb2/auction Failed to send response: json: error calling MarshalJSON for type json.RawMessage: invalid character '.' looking for beginning of value"), + expectedAuctionObject: analytics.AuctionObject{ + Errors: []error{ + errors.New("Failed to enrich Bid Response with hook debug information: Invalid JSON Document"), + errors.New("/openrtb2/auction Failed to send response: json: error calling MarshalJSON for type json.RawMessage: invalid character '.' looking for beginning of value"), + }, + Status: 0, + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: int(openrtb_ext.ResponseRejectedCategoryMappingInvalid), + }, + }, + Seat: "pubmatic", + }, + }, }, - expectedStatus: 0, - request: &openrtb2.BidRequest{ID: "some-id", Test: 1}, - response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage("...")}, - hookExecutor: hookExecutor, + expectedResponseBody: "", + request: &openrtb2.BidRequest{ID: "some-id", Test: 1}, + response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage("...")}, + hookExecutor: hookExecutor, + auctionObject: analytics.AuctionObject{}, }, { description: "Error logged if hook enrichment returns warnings", - expectedErrors: []error{ - errors.New("Value is not a string: 1"), - errors.New("Value is not a boolean: active"), + expectedAuctionObject: analytics.AuctionObject{ + Errors: []error{ + errors.New("Value is not a string: 1"), + errors.New("Value is not a boolean: active"), + }, + Status: 0, + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: int(openrtb_ext.ResponseRejectedCategoryMappingInvalid), + }, + }, + Seat: "pubmatic", + }, + }, }, - expectedStatus: 0, - request: &openrtb2.BidRequest{ID: "some-id", Test: 1, Ext: json.RawMessage(`{"prebid": {"debug": "active", "trace": 1}}`)}, - response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage("{}")}, - hookExecutor: hookExecutor, + expectedResponseBody: "{\"id\":\"some-id\",\"ext\":{\"prebid\":{\"modules\":{\"warnings\":{\"foobar\":{\"foo\":[\"warning message\"]}}}}}}\n", + request: &openrtb2.BidRequest{ID: "some-id", Test: 1, Ext: json.RawMessage(`{"prebid": {"debug": "active", "trace": 1}}`)}, + response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage("{}")}, + hookExecutor: hookExecutor, + auctionObject: analytics.AuctionObject{}, + }, + { + description: "Response should contain seatNonBid if returnallbidstatus is true", + expectedAuctionObject: analytics.AuctionObject{ + Errors: nil, + Status: 0, + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: int(openrtb_ext.ResponseRejectedCategoryMappingInvalid), + }, + }, + Seat: "pubmatic", + }, + }, + }, + expectedResponseBody: "{\"id\":\"some-id\",\"ext\":{\"prebid\":{\"modules\":{\"warnings\":{\"foobar\":{\"foo\":[\"warning message\"]}}}," + + "\"seatnonbid\":[{\"nonbid\":[{\"impid\":\"imp1\",\"statuscode\":303,\"ext\":{\"prebid\":{\"bid\":{}}}}],\"seat\":\"pubmatic\"}]}}}\n", + request: &openrtb2.BidRequest{ID: "some-id", Test: 1, Ext: json.RawMessage(`"returnallbidstatus": true}}`)}, + response: &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage("{}")}, + hookExecutor: hookExecutor, + auctionObject: analytics.AuctionObject{ + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: json.RawMessage(`{"prebid": {"returnallbidstatus": true}}`), + }, + }, + }, + }, + { + description: "Expect seatNonBid in auctionObject even if response is nil", + expectedAuctionObject: analytics.AuctionObject{ + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: int(openrtb_ext.ResponseRejectedCategoryMappingInvalid), + }, + }, + Seat: "pubmatic", + }, + }, + }, + expectedResponseBody: "null\n", + request: &openrtb2.BidRequest{ID: "some-id", Test: 1, Ext: json.RawMessage(`{"prebid": {"debug": true, "trace":" 1"}}`)}, + response: nil, + hookExecutor: hookExecutor, + auctionObject: analytics.AuctionObject{}, }, } - for _, test := range testCases { t.Run(test.description, func(t *testing.T) { writer := httptest.NewRecorder() labels := metrics.Labels{} - ao := analytics.AuctionObject{} account := &config.Account{DebugAllow: true} + if test.auctionObject.RequestWrapper != nil { + test.auctionObject.RequestWrapper.RebuildRequest() + } - _, ao = sendAuctionResponse(writer, test.hookExecutor, test.response, test.request, account, labels, ao) + _, ao := sendAuctionResponse(writer, test.hookExecutor, test.response, test.request, account, labels, test.auctionObject, &openrtb_ext.SeatNonBidBuilder{}) - assert.Equal(t, ao.Errors, test.expectedErrors, "Invalid errors.") - assert.Equal(t, test.expectedStatus, ao.Status, "Invalid HTTP response status.") + assert.Equal(t, test.expectedAuctionObject.Errors, ao.Errors, "Invalid errors.") + assert.Equal(t, test.expectedAuctionObject.Status, ao.Status, "Invalid HTTP response status.") + assert.Equal(t, test.expectedResponseBody, writer.Body.String(), "Invalid response body.") + assert.Equal(t, test.expectedAuctionObject.SeatNonBid, ao.SeatNonBid, "Invalid seatNonBid present in auctionObject.") }) } } @@ -5156,46 +5248,112 @@ func (e mockStageExecutor) GetOutcomes() []hookexecution.StageOutcome { func TestSetSeatNonBidRaw(t *testing.T) { type args struct { - request *openrtb_ext.RequestWrapper - auctionResponse *exchange.AuctionResponse + request *openrtb_ext.RequestWrapper + response *openrtb2.BidResponse + nonBids []openrtb_ext.SeatNonBid + } + type want struct { + error bool + response *openrtb2.BidResponse } tests := []struct { - name string - args args - wantErr bool + name string + args args + want want }{ { - name: "nil-auctionResponse", - args: args{auctionResponse: nil}, - wantErr: false, + name: "nil response", + args: args{response: nil}, + want: want{ + error: false, + response: nil, + }, }, { - name: "nil-bidResponse", - args: args{auctionResponse: &exchange.AuctionResponse{BidResponse: nil}}, - wantErr: false, + name: "returnallbidstatus false", + args: args{response: &openrtb2.BidResponse{}, + request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid": { "returnallbidstatus" : false }}`)}}}, + want: want{ + error: false, + response: &openrtb2.BidResponse{}, + }, }, { - name: "invalid-response.Ext", - args: args{auctionResponse: &exchange.AuctionResponse{BidResponse: &openrtb2.BidResponse{Ext: []byte(`invalid_json`)}}}, - wantErr: true, + name: "invalid responseExt", + args: args{ + request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid": { "returnallbidstatus" : true }}`)}}, + response: &openrtb2.BidResponse{Ext: []byte(`{invalid}`)}, + nonBids: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 1, + }, + }, + }, + }, + }, + want: want{ + error: true, + response: &openrtb2.BidResponse{Ext: []byte(`{invalid}`)}, + }, + }, + { + name: "returnallbidstatus is true, update seatnonbid in nil responseExt", + args: args{ + request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid": { "returnallbidstatus" : true }}`)}}, + response: &openrtb2.BidResponse{Ext: nil}, + nonBids: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 1, + }, + }, + }, + }, + }, + want: want{ + error: false, + response: &openrtb2.BidResponse{ + Ext: json.RawMessage(`{"prebid":{"seatnonbid":[{"nonbid":[{"impid":"imp","statuscode":1,"ext":{"prebid":{"bid":{}}}}],"seat":"pubmatic"}]}}`), + }, + }, }, { - name: "update-seatnonbid-in-ext", + name: "returnallbidstatus is true, update seatnonbid in non-nil responseExt", args: args{ - request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid": { "returnallbidstatus" : true }}`)}}, - auctionResponse: &exchange.AuctionResponse{ - ExtBidResponse: &openrtb_ext.ExtBidResponse{Prebid: &openrtb_ext.ExtResponsePrebid{SeatNonBid: []openrtb_ext.SeatNonBid{}}}, - BidResponse: &openrtb2.BidResponse{Ext: []byte(`{}`)}, + request: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid": { "returnallbidstatus" : true }}`)}}, + response: &openrtb2.BidResponse{Ext: []byte(`{}`)}, + nonBids: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 1, + }, + }, + }, + }, + }, + want: want{ + error: false, + response: &openrtb2.BidResponse{ + Ext: json.RawMessage(`{"prebid":{"seatnonbid":[{"nonbid":[{"impid":"imp","statuscode":1,"ext":{"prebid":{"bid":{}}}}],"seat":"pubmatic"}]}}`), }, }, - wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := setSeatNonBidRaw(tt.args.request, tt.args.auctionResponse); (err != nil) != tt.wantErr { - t.Errorf("setSeatNonBidRaw() error = %v, wantErr %v", err, tt.wantErr) - } + err := setSeatNonBidRaw(tt.args.request, tt.args.response, tt.args.nonBids) + assert.Equal(t, err != nil, tt.want.error, "mismatched error.") + assert.Equal(t, tt.args.response, tt.want.response, "mismatched bidResponse.") }) } } @@ -5982,3 +6140,653 @@ func sortUserData(user *openrtb2.User) { } } } + +func TestGetNonBidsFromStageOutcomes(t *testing.T) { + tests := []struct { + name string + stageOutcomes []hookexecution.StageOutcome + expectedNonBids openrtb_ext.SeatNonBidBuilder + }{ + { + name: "nil groups", + stageOutcomes: []hookexecution.StageOutcome{ + { + Groups: nil, + }, + }, + expectedNonBids: getNonBids(map[string][]openrtb_ext.NonBidParams{}), + }, + { + name: "nil and empty invocation results", + stageOutcomes: []hookexecution.StageOutcome{ + { + Groups: []hookexecution.GroupOutcome{ + { + InvocationResults: nil, + }, + { + InvocationResults: []hookexecution.HookOutcome{}, + }, + }, + }, + }, + expectedNonBids: getNonBids(map[string][]openrtb_ext.NonBidParams{}), + }, + { + name: "single nonbid with failure hookoutcome status", + stageOutcomes: []hookexecution.StageOutcome{ + { + Groups: []hookexecution.GroupOutcome{ + { + InvocationResults: []hookexecution.HookOutcome{ + { + Status: hookexecution.StatusExecutionFailure, + SeatNonBid: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "pubmatic": { + { + Bid: &openrtb2.Bid{ImpID: "imp1"}, + NonBidReason: 100, + }, + }, + }), + }, + }, + }, + }, + }, + }, + expectedNonBids: getNonBids(map[string][]openrtb_ext.NonBidParams{}), + }, + { + name: "single nonbid with success hookoutcome status", + stageOutcomes: []hookexecution.StageOutcome{ + { + Groups: []hookexecution.GroupOutcome{ + { + InvocationResults: []hookexecution.HookOutcome{ + { + Status: hookexecution.StatusSuccess, + SeatNonBid: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "pubmatic": { + { + Bid: &openrtb2.Bid{ImpID: "imp1"}, + NonBidReason: 100, + }, + }, + }), + }, + }, + }, + }, + }, + }, + expectedNonBids: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "pubmatic": { + { + Bid: &openrtb2.Bid{ImpID: "imp1"}, + NonBidReason: 100, + }, + }, + }), + }, + { + name: "seatNonBid from multi stage outcomes", + stageOutcomes: []hookexecution.StageOutcome{ + { + Stage: hooks.StageAllProcessedBidResponses.String(), + Groups: []hookexecution.GroupOutcome{ + { + InvocationResults: []hookexecution.HookOutcome{ + { + Status: hookexecution.StatusSuccess, + SeatNonBid: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "pubmatic": { + { + Bid: &openrtb2.Bid{ImpID: "imp1"}, + NonBidReason: 100, + }, + }, + }), + }, + }, + }, + }, + }, + { + Stage: hooks.StageBidderRequest.String(), + Groups: []hookexecution.GroupOutcome{ + { + InvocationResults: []hookexecution.HookOutcome{ + { + Status: hookexecution.StatusSuccess, + SeatNonBid: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "appnexus": { + { + Bid: &openrtb2.Bid{ImpID: "imp1"}, + NonBidReason: 100, + }, + }, + }), + }, + }, + }, + }, + }, + }, + expectedNonBids: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "appnexus": { + { + Bid: &openrtb2.Bid{ImpID: "imp1"}, + NonBidReason: 100, + }, + }, + "pubmatic": { + { + Bid: &openrtb2.Bid{ImpID: "imp1"}, + NonBidReason: 100, + }, + }, + }), + }, + { + name: "seatNonBid for same seat from multi stage outcomes", + stageOutcomes: []hookexecution.StageOutcome{ + { + Stage: hooks.StageAllProcessedBidResponses.String(), + Groups: []hookexecution.GroupOutcome{ + { + InvocationResults: []hookexecution.HookOutcome{ + { + Status: hookexecution.StatusSuccess, + SeatNonBid: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "pubmatic": { + { + Bid: &openrtb2.Bid{ImpID: "imp1"}, + NonBidReason: 100, + }, + }, + }), + }, + }, + }, + }, + }, + { + Stage: hooks.StageBidderRequest.String(), + Groups: []hookexecution.GroupOutcome{ + { + InvocationResults: []hookexecution.HookOutcome{ + { + Status: hookexecution.StatusSuccess, + SeatNonBid: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "pubmatic": { + { + Bid: &openrtb2.Bid{ImpID: "imp2"}, + NonBidReason: 100, + }, + }, + }), + }, + }, + }, + }, + }, + }, + expectedNonBids: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "pubmatic": { + { + Bid: &openrtb2.Bid{ImpID: "imp1"}, + NonBidReason: 100, + }, + { + Bid: &openrtb2.Bid{ImpID: "imp2"}, + NonBidReason: 100, + }, + }, + }), + }, + { + name: "multi group outcomes with empty nonbids", + stageOutcomes: []hookexecution.StageOutcome{ + { + Stage: hooks.StageAllProcessedBidResponses.String(), + Groups: []hookexecution.GroupOutcome{ + { + InvocationResults: []hookexecution.HookOutcome{ + { + Status: hookexecution.StatusSuccess, + SeatNonBid: openrtb_ext.SeatNonBidBuilder{}, + }, + }, + }, + { + InvocationResults: []hookexecution.HookOutcome{ + { + Status: hookexecution.StatusSuccess, + SeatNonBid: openrtb_ext.SeatNonBidBuilder{}, + }, + }, + }, + }, + }, + }, + expectedNonBids: openrtb_ext.SeatNonBidBuilder{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nonBids := getNonBidsFromStageOutcomes(tt.stageOutcomes) + assert.Equal(t, nonBids, tt.expectedNonBids, "getNonBidsFromStageOutcomes returned incorrect nonBids") + }) + } +} + +// getNonBids is utility function which forms SeatNonBidBuilder from NonBidParams input +func getNonBids(bidParamsMap map[string][]openrtb_ext.NonBidParams) openrtb_ext.SeatNonBidBuilder { + nonBids := openrtb_ext.SeatNonBidBuilder{} + for bidder, bidParams := range bidParamsMap { + for _, bidParam := range bidParams { + nonBid := openrtb_ext.NewNonBid(bidParam) + nonBids.AddBid(nonBid, bidder) + } + } + return nonBids +} + +func TestSeatNonBidInAuction(t *testing.T) { + type args struct { + bidRequest openrtb2.BidRequest + seatNonBidFromHoldAuction openrtb_ext.SeatNonBidBuilder + errorFromHoldAuction error + rejectRawAuctionHook bool + errorFromHook error + } + type want struct { + statusCode int + body string + seatNonBid []openrtb_ext.SeatNonBid + } + testCases := []struct { + description string + args args + want want + }{ + { + description: "request parsing failed, auctionObject should contain seatNonBid", + args: args{ + bidRequest: openrtb2.BidRequest{ + Site: &openrtb2.Site{ + ID: "site-1", + }, + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{ + W: openrtb2.Int64Ptr(100), + H: openrtb2.Int64Ptr(100), + }, + }, + }, + }, + }, + want: want{ + body: "Invalid request: request missing required field: \"id\"\n", + statusCode: 400, + seatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 100, + }, + }, + }, + }, + }, + }, + { + description: "auctionObject and bidResponseExt should contain seatNonBid when returnallbidstatus is true", + args: args{ + bidRequest: openrtb2.BidRequest{ + ID: "id", + Site: &openrtb2.Site{ + ID: "site-1", + }, + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{ + W: openrtb2.Int64Ptr(100), + H: openrtb2.Int64Ptr(100), + }, + Ext: json.RawMessage(`{"prebid": {"bidder":{"pubmatic":{"publisherid":1234}}}}`), + }, + }, + Ext: json.RawMessage(`{"prebid": {"returnallbidstatus": true}}`), + }, + }, + want: want{ + statusCode: 200, + body: `{"id":"","seatbid":[{"bid":[{"id":"","impid":"","price":0,"adm":""}]}],"ext":{"prebid":` + + `{"seatnonbid":[{"nonbid":[{"impid":"imp","statuscode":100,"ext":{"prebid":{"bid":{}}}}],"seat":"pubmatic"}]}}}` + "\n", + seatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 100, + }, + }, + }, + }, + }, + }, + { + description: "auctionObject should contain seatNonBid from both holdAuction and hookOutcomes", + args: args{ + seatNonBidFromHoldAuction: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "appnexus": { + { + Bid: &openrtb2.Bid{ImpID: "imp"}, + NonBidReason: 100, + }, + }, + }), + bidRequest: openrtb2.BidRequest{ + ID: "id", + Site: &openrtb2.Site{ + ID: "site-1", + }, + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{ + W: openrtb2.Int64Ptr(100), + H: openrtb2.Int64Ptr(100), + }, + Ext: json.RawMessage(`{"prebid": {"bidder":{"pubmatic":{"publisherid":1234}}}}`), + }, + }, + Ext: json.RawMessage(`{"prebid": {"returnallbidstatus": false}}`), + }, + }, + want: want{ + statusCode: 200, + body: `{"id":"","seatbid":[{"bid":[{"id":"","impid":"","price":0,"adm":""}]}]}` + "\n", + seatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 100, + }, + }, + }, + { + Seat: "appnexus", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 100, + }, + }, + }, + }, + }, + }, + { + description: "hookexecutor returns hook-reject error after parseRequest, seatNonBid should be present in auctionObject and bidResponseExt", + args: args{ + rejectRawAuctionHook: true, + errorFromHook: &hookexecution.RejectError{Stage: hooks.StageEntrypoint.String(), NBR: 5}, + bidRequest: openrtb2.BidRequest{ + ID: "id", + Site: &openrtb2.Site{ + ID: "site-1", + }, + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{ + W: openrtb2.Int64Ptr(100), + H: openrtb2.Int64Ptr(100), + }, + Ext: json.RawMessage(`{"prebid": {"bidder":{"pubmatic":{"publisherid":1234}}}}`), + }, + }, + Ext: json.RawMessage(`{"prebid": {"returnallbidstatus": true}}`), + }, + }, + want: want{ + statusCode: 200, + body: `{"id":"id","nbr":10,"ext":{"prebid":{"seatnonbid":[{"nonbid":[{"impid":"imp","statuscode":100,` + + `"ext":{"prebid":{"bid":{}}}}],"seat":"pubmatic"}]}}}` + "\n", + seatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 100, + }, + }, + }, + }, + }, + }, + { + description: "holdAuction returns hookRejection error, seatNonBid should be present in auctionObject and bidResponseExt", + args: args{ + errorFromHoldAuction: &hookexecution.RejectError{Stage: hooks.StageAllProcessedBidResponses.String(), NBR: 5}, + bidRequest: openrtb2.BidRequest{ + ID: "id", + Site: &openrtb2.Site{ + ID: "site-1", + }, + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{ + W: openrtb2.Int64Ptr(100), + H: openrtb2.Int64Ptr(100), + }, + Ext: json.RawMessage(`{"prebid": {"bidder":{"pubmatic":{"publisherid":1234}}}}`), + }, + }, + Ext: json.RawMessage(`{"prebid": {"returnallbidstatus": true}}`), + }, + }, + want: want{ + statusCode: 200, + body: `{"id":"id","nbr":5,"ext":{"prebid":{"seatnonbid":[{"nonbid":[{"impid":"imp","statuscode":100,"ext":{"prebid":{"bid":{}}}}],"seat":"pubmatic"}]}}}` + "\n", + seatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 100, + }, + }, + }, + }, + }, + }, + { + description: "holdAuction returns non-hookRejection error, seatNonBid should be present in auctionObject", + args: args{ + errorFromHoldAuction: errors.New("any-error"), + bidRequest: openrtb2.BidRequest{ + ID: "id", + Site: &openrtb2.Site{ + ID: "site-1", + }, + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Banner: &openrtb2.Banner{ + W: openrtb2.Int64Ptr(100), + H: openrtb2.Int64Ptr(100), + }, + Ext: json.RawMessage(`{"prebid": {"bidder":{"pubmatic":{"publisherid":1234}}}}`), + }, + }, + Ext: json.RawMessage(`{"prebid": {"returnallbidstatus": true}}`), + }, + }, + want: want{ + statusCode: 500, + body: `Critical error while running the auction: any-error`, + seatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 100, + }, + }, + }, + }, + }, + }, + } + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + reqBody, _ := jsonutil.Marshal(test.args.bidRequest) + mockAnalytics := mockAnalyticsModule{} + deps := &endpointDeps{ + fakeUUIDGenerator{}, + &mockExchange{seatNonBid: test.args.seatNonBidFromHoldAuction, returnError: test.args.errorFromHoldAuction}, + &mockBidderParamValidator{}, + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: int64(len(reqBody))}, + &metricsConfig.NilMetricsEngine{}, + &mockAnalytics, + map[string]string{}, + false, + []byte{}, + openrtb_ext.BuildBidderMap(), + nil, + nil, + hardcodedResponseIPValidator{response: true}, + empty_fetcher.EmptyFetcher{}, + mockPlanBuilder{ + entrypointPlan: makePlan[hookstage.Entrypoint](mockSeatNonBidHook{}), + rawAuctionPlan: makePlan[hookstage.RawAuctionRequest]( + mockSeatNonBidHook{ + rejectRawAuctionHook: test.args.rejectRawAuctionHook, + returnError: test.args.errorFromHook, + }, + ), + }, + nil, + openrtb_ext.NormalizeBidderName, + } + + req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(string(reqBody))) + recorder := httptest.NewRecorder() + + deps.Auction(recorder, req, nil) + + assert.Equal(t, test.want.statusCode, recorder.Result().StatusCode, "mismatched status code.") + assert.Equal(t, test.want.body, recorder.Body.String(), "mismatched response body.") + assert.ElementsMatch(t, test.want.seatNonBid, mockAnalytics.auctionObjects[0].SeatNonBid, "mismatched seat-non-bids.") + }) + } +} + +func TestSetSeatNonBid(t *testing.T) { + type args struct { + finalExtBidResponse *openrtb_ext.ExtBidResponse + seatNonBid []openrtb_ext.SeatNonBid + } + type want struct { + setSeatNonBid bool + finalExtBidResponse *openrtb_ext.ExtBidResponse + } + tests := []struct { + name string + args args + want want + }{ + { + name: "nil seatNonBid", + args: args{seatNonBid: nil, finalExtBidResponse: &openrtb_ext.ExtBidResponse{}}, + want: want{ + setSeatNonBid: false, + finalExtBidResponse: &openrtb_ext.ExtBidResponse{}, + }, + }, + { + name: "empty seatNonBid", + args: args{seatNonBid: []openrtb_ext.SeatNonBid{}, finalExtBidResponse: &openrtb_ext.ExtBidResponse{}}, + want: want{ + setSeatNonBid: false, + finalExtBidResponse: &openrtb_ext.ExtBidResponse{}, + }, + }, + { + name: "finalExtBidResponse is nil", + args: args{finalExtBidResponse: nil}, + want: want{ + setSeatNonBid: false, + finalExtBidResponse: nil, + }, + }, + { + name: "finalExtBidResponse prebid is non-nil", + args: args{seatNonBid: []openrtb_ext.SeatNonBid{{Seat: "pubmatic", NonBid: []openrtb_ext.NonBid{{ImpId: "imp1", StatusCode: 100}}}}, + finalExtBidResponse: &openrtb_ext.ExtBidResponse{Prebid: &openrtb_ext.ExtResponsePrebid{}}}, + want: want{ + setSeatNonBid: true, + finalExtBidResponse: &openrtb_ext.ExtBidResponse{Prebid: &openrtb_ext.ExtResponsePrebid{ + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + }, + Seat: "pubmatic", + }, + }, + }}, + }, + }, + { + name: "finalExtBidResponse prebid is nil", + args: args{finalExtBidResponse: &openrtb_ext.ExtBidResponse{Prebid: nil}, seatNonBid: []openrtb_ext.SeatNonBid{{Seat: "pubmatic", NonBid: []openrtb_ext.NonBid{{ImpId: "imp1", StatusCode: 100}}}}}, + want: want{ + setSeatNonBid: true, + finalExtBidResponse: &openrtb_ext.ExtBidResponse{Prebid: &openrtb_ext.ExtResponsePrebid{ + SeatNonBid: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + }, + Seat: "pubmatic", + }, + }, + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := setSeatNonBid(tt.args.finalExtBidResponse, tt.args.seatNonBid) + assert.Equal(t, tt.want.setSeatNonBid, got, "setSeatNonBid returned invalid value") + assert.Equal(t, tt.want.finalExtBidResponse, tt.args.finalExtBidResponse, "setSeatNonBid incorrectly updated finalExtBidResponse") + }) + } +} diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go index e869ded80b..249456a6ac 100644 --- a/endpoints/openrtb2/test_utils.go +++ b/endpoints/openrtb2/test_utils.go @@ -38,6 +38,7 @@ import ( pbc "github.com/prebid/prebid-server/v3/prebid_cache_client" "github.com/prebid/prebid-server/v3/stored_requests" "github.com/prebid/prebid-server/v3/stored_requests/backends/empty_fetcher" + "github.com/prebid/prebid-server/v3/stored_responses" "github.com/prebid/prebid-server/v3/util/iputil" "github.com/prebid/prebid-server/v3/util/jsonutil" "github.com/prebid/prebid-server/v3/util/uuidutil" @@ -855,9 +856,15 @@ func (cf mockStoredReqFetcher) FetchResponses(ctx context.Context, ids []string) // mockExchange implements the Exchange interface type mockExchange struct { lastRequest *openrtb2.BidRequest + seatNonBid openrtb_ext.SeatNonBidBuilder + returnError error } func (m *mockExchange) HoldAuction(ctx context.Context, auctionRequest *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { + if m.returnError != nil { + return nil, m.returnError + } + r := auctionRequest.BidRequestWrapper m.lastRequest = r.BidRequest return &exchange.AuctionResponse{ @@ -868,6 +875,7 @@ func (m *mockExchange) HoldAuction(ctx context.Context, auctionRequest *exchange }}, }}, }, + SeatNonBid: m.seatNonBid, }, nil } @@ -1379,6 +1387,10 @@ func (v mockBidderParamValidator) Validate(name openrtb_ext.BidderName, ext json } func (v mockBidderParamValidator) Schema(name openrtb_ext.BidderName) string { return "" } +func (v *mockBidderParamValidator) ValidateImp(imp *openrtb_ext.ImpWrapper, cfg ortb.ValidationConfig, index int, aliases map[string]string, hasStoredResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) []error { + return nil +} + type mockAccountFetcher struct { data map[string]json.RawMessage } @@ -1582,6 +1594,76 @@ func (m mockRejectionHook) HandleRawBidderResponseHook( return result, nil } +// mockSeatNonBidHook can be used to return seatNonBid from hook stage +type mockSeatNonBidHook struct { + rejectEntrypointHook bool + rejectRawAuctionHook bool + rejectProcessedAuctionHook bool + rejectBidderRequestHook bool + rejectRawBidderResponseHook bool + returnError error +} + +func (m mockSeatNonBidHook) HandleEntrypointHook( + _ context.Context, + _ hookstage.ModuleInvocationContext, + _ hookstage.EntrypointPayload, +) (hookstage.HookResult[hookstage.EntrypointPayload], error) { + if m.rejectEntrypointHook { + return hookstage.HookResult[hookstage.EntrypointPayload]{NbrCode: 10, Reject: true}, m.returnError + } + result := hookstage.HookResult[hookstage.EntrypointPayload]{} + result.SeatNonBid = openrtb_ext.SeatNonBidBuilder{} + nonBid := openrtb_ext.NewNonBid(openrtb_ext.NonBidParams{Bid: &openrtb2.Bid{ImpID: "imp"}, NonBidReason: 100}) + result.SeatNonBid.AddBid(nonBid, "pubmatic") + + return result, m.returnError +} + +func (m mockSeatNonBidHook) HandleRawAuctionHook( + _ context.Context, + _ hookstage.ModuleInvocationContext, + _ hookstage.RawAuctionRequestPayload, +) (hookstage.HookResult[hookstage.RawAuctionRequestPayload], error) { + if m.rejectRawAuctionHook { + return hookstage.HookResult[hookstage.RawAuctionRequestPayload]{NbrCode: 10, Reject: true}, m.returnError + } + return hookstage.HookResult[hookstage.RawAuctionRequestPayload]{Reject: false, NbrCode: 0}, m.returnError +} + +func (m mockSeatNonBidHook) HandleProcessedAuctionHook( + _ context.Context, + _ hookstage.ModuleInvocationContext, + _ hookstage.ProcessedAuctionRequestPayload, +) (hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload], error) { + if m.rejectProcessedAuctionHook { + return hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload]{NbrCode: 10, Reject: true}, m.returnError + } + return hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload]{Reject: true, NbrCode: 0}, m.returnError +} + +func (m mockSeatNonBidHook) HandleBidderRequestHook( + _ context.Context, + _ hookstage.ModuleInvocationContext, + payload hookstage.BidderRequestPayload, +) (hookstage.HookResult[hookstage.BidderRequestPayload], error) { + if m.rejectBidderRequestHook { + return hookstage.HookResult[hookstage.BidderRequestPayload]{NbrCode: 10, Reject: true}, m.returnError + } + return hookstage.HookResult[hookstage.BidderRequestPayload]{}, m.returnError +} + +func (m mockSeatNonBidHook) HandleRawBidderResponseHook( + _ context.Context, + _ hookstage.ModuleInvocationContext, + payload hookstage.RawBidderResponsePayload, +) (hookstage.HookResult[hookstage.RawBidderResponsePayload], error) { + if m.rejectRawBidderResponseHook { + return hookstage.HookResult[hookstage.RawBidderResponsePayload]{NbrCode: 10, Reject: true}, m.returnError + } + return hookstage.HookResult[hookstage.RawBidderResponsePayload]{}, m.returnError +} + var entryPointHookUpdateWithErrors = hooks.HookWrapper[hookstage.Entrypoint]{ Module: "foobar", Code: "foo", diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index cf18840fbd..8fe02356a1 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -122,6 +122,7 @@ func NewVideoEndpoint( func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { start := time.Now() + seatNonBid := &openrtb_ext.SeatNonBidBuilder{} vo := analytics.VideoObject{ Status: http.StatusOK, Errors: make([]error, 0), @@ -345,9 +346,10 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re var response *openrtb2.BidResponse if auctionResponse != nil { response = auctionResponse.BidResponse + seatNonBid.Append(auctionResponse.SeatNonBid) } vo.Response = response - vo.SeatNonBid = auctionResponse.GetSeatNonBid() + vo.SeatNonBid = seatNonBid.Get() if err != nil { errL := []error{err} handleError(&labels, w, errL, &vo, &debugLog) @@ -362,7 +364,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re return } if bidReq.Test == 1 { - err = setSeatNonBidRaw(bidReqWrapper, auctionResponse) + err = setSeatNonBidRaw(bidReqWrapper, response, vo.SeatNonBid) if err != nil { glog.Errorf("Error setting seat non-bid: %v", err) } diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 79eaaab980..d115f4e00e 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -1389,6 +1389,7 @@ func (cf mockVideoStoredReqFetcher) FetchResponses(ctx context.Context, ids []st type mockExchangeVideo struct { lastRequest *openrtb2.BidRequest cache *mockCacheClient + seatNonBid openrtb_ext.SeatNonBidBuilder } func (m *mockExchangeVideo) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*exchange.AuctionResponse, error) { @@ -1423,7 +1424,9 @@ func (m *mockExchangeVideo) HoldAuction(ctx context.Context, r *exchange.Auction {ID: "16", ImpID: "5_2", Ext: ext}, }, }}, - }}, nil + }, + SeatNonBid: m.seatNonBid}, nil + } type mockExchangeAppendBidderNames struct { @@ -1513,3 +1516,105 @@ func TestVideoRequestValidationFailed(t *testing.T) { assert.Equal(t, 500, recorder.Code, "Should catch error in request") assert.Equal(t, "Critical error while running the video endpoint: request.tmax must be nonnegative. Got -2", errorMessage, "Incorrect request validation message") } + +func TestSeatNonBidInVideoAuction(t *testing.T) { + bidRequest := openrtb_ext.BidRequestVideo{ + Test: 1, + StoredRequestId: "80ce30c53c16e6ede735f123ef6e32361bfc7b22", + PodConfig: openrtb_ext.PodConfig{ + DurationRangeSec: []int{30, 50}, + RequireExactDuration: true, + Pods: []openrtb_ext.Pod{ + {PodId: 1, AdPodDurationSec: 30, ConfigId: "fba10607-0c12-43d1-ad07-b8a513bc75d6"}, + }, + }, + App: &openrtb2.App{Bundle: "pbs.com"}, + Video: &openrtb2.Video{ + MIMEs: []string{"mp4"}, + Protocols: []adcom1.MediaCreativeSubtype{1}, + }, + } + + type args struct { + nonBidsFromHoldAuction openrtb_ext.SeatNonBidBuilder + } + type want struct { + seatNonBid []openrtb_ext.SeatNonBid + } + testCases := []struct { + description string + args args + want want + }{ + { + description: "holdAuction returns seatNonBid", + args: args{ + nonBidsFromHoldAuction: getNonBids(map[string][]openrtb_ext.NonBidParams{ + "pubmatic": { + { + Bid: &openrtb2.Bid{ImpID: "imp"}, + NonBidReason: 100, + }, + }, + }), + }, + want: want{ + seatNonBid: []openrtb_ext.SeatNonBid{ + { + Seat: "pubmatic", + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp", + StatusCode: 100, + }, + }, + }, + }, + }, + }, + { + description: "holdAuction does not return seatNonBid", + args: args{ + nonBidsFromHoldAuction: openrtb_ext.SeatNonBidBuilder{}, + }, + want: want{ + seatNonBid: nil, + }, + }, + } + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + ex := &mockExchangeVideo{seatNonBid: test.args.nonBidsFromHoldAuction} + analyticsModule := mockAnalyticsModule{} + deps := &endpointDeps{ + fakeUUIDGenerator{}, + ex, + &mockBidderParamValidator{}, + &mockVideoStoredReqFetcher{}, + &mockVideoStoredReqFetcher{}, + &mockAccountFetcher{data: mockVideoAccountData}, + &config.Configuration{MaxRequestSize: maxSize}, + &metricsConfig.NilMetricsEngine{}, + &analyticsModule, + map[string]string{}, + false, + []byte{}, + openrtb_ext.BuildBidderMap(), + ex.cache, + regexp.MustCompile(`[<>]`), + hardcodedResponseIPValidator{response: true}, + empty_fetcher.EmptyFetcher{}, + hooks.EmptyPlanBuilder{}, + nil, + openrtb_ext.NormalizeBidderName, + } + + reqBody, _ := jsonutil.Marshal(bidRequest) + req := httptest.NewRequest("POST", "/openrtb2/video", strings.NewReader(string(reqBody))) + recorder := httptest.NewRecorder() + deps.VideoAuctionEndpoint(recorder, req, nil) + + assert.ElementsMatch(t, test.want.seatNonBid, analyticsModule.videoObjects[0].SeatNonBid, "mismatched seatnonbid.") + }) + } +} diff --git a/exchange/auction_response.go b/exchange/auction_response.go index 67ab2dc4bb..37c2160343 100644 --- a/exchange/auction_response.go +++ b/exchange/auction_response.go @@ -9,12 +9,5 @@ import ( type AuctionResponse struct { *openrtb2.BidResponse ExtBidResponse *openrtb_ext.ExtBidResponse -} - -// GetSeatNonBid returns array of seat non-bid if present. nil otherwise -func (ar *AuctionResponse) GetSeatNonBid() []openrtb_ext.SeatNonBid { - if ar != nil && ar.ExtBidResponse != nil && ar.ExtBidResponse.Prebid != nil { - return ar.ExtBidResponse.Prebid.SeatNonBid - } - return nil + SeatNonBid openrtb_ext.SeatNonBidBuilder } diff --git a/exchange/bidder.go b/exchange/bidder.go index 232da470fb..2af5f44abf 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -76,14 +76,14 @@ type bidRequestOptions struct { type extraBidderRespInfo struct { respProcessingStartTime time.Time - seatNonBidBuilder SeatNonBidBuilder + seatNonBidBuilder openrtb_ext.SeatNonBidBuilder } type extraAuctionResponseInfo struct { fledge *openrtb_ext.Fledge bidsFound bool bidderResponseStartTime time.Time - seatNonBidBuilder SeatNonBidBuilder + seatNonBidBuilder openrtb_ext.SeatNonBidBuilder } const ImpIdReqBody = "Stored bid response for impression id: " @@ -137,7 +137,7 @@ type bidderAdapterConfig struct { func (bidder *BidderAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error) { request := openrtb_ext.RequestWrapper{BidRequest: bidderRequest.BidRequest} reject := hookExecutor.ExecuteBidderRequestStage(&request, string(bidderRequest.BidderName)) - seatNonBidBuilder := SeatNonBidBuilder{} + seatNonBidBuilder := openrtb_ext.SeatNonBidBuilder{} if reject != nil { return nil, extraBidderRespInfo{}, []error{reject} } @@ -402,7 +402,7 @@ func (bidder *BidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde } else { errs = append(errs, httpInfo.err) nonBidReason := httpInfoToNonBidReason(httpInfo) - seatNonBidBuilder.rejectImps(httpInfo.request.ImpIDs, nonBidReason, string(bidderRequest.BidderName)) + seatNonBidBuilder.RejectImps(httpInfo.request.ImpIDs, nonBidReason, string(bidderRequest.BidderName)) } } diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index dbb167e053..2c7378ddc6 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -3110,7 +3110,7 @@ func TestSeatNonBid(t *testing.T) { } type expect struct { seatBids []*entities.PbsOrtbSeatBid - seatNonBids SeatNonBidBuilder + seatNonBids openrtb_ext.SeatNonBidBuilder errors []error } testCases := []struct { @@ -3130,10 +3130,10 @@ func TestSeatNonBid(t *testing.T) { client: &http.Client{Timeout: time.Nanosecond}, // for timeout }, expect: expect{ - seatNonBids: SeatNonBidBuilder{ + seatNonBids: openrtb_ext.SeatNonBidBuilder{ "pubmatic": {{ ImpId: "1234", - StatusCode: int(ErrorTimeout), + StatusCode: int(openrtb_ext.ErrorTimeout), }}, }, errors: []error{&errortypes.Timeout{Message: context.DeadlineExceeded.Error()}}, @@ -3150,10 +3150,10 @@ func TestSeatNonBid(t *testing.T) { }, }, expect: expect{ - seatNonBids: SeatNonBidBuilder{ + seatNonBids: openrtb_ext.SeatNonBidBuilder{ "appnexus": { - {ImpId: "1234", StatusCode: int(ErrorBidderUnreachable)}, - {ImpId: "4567", StatusCode: int(ErrorBidderUnreachable)}, + {ImpId: "1234", StatusCode: int(openrtb_ext.ErrorBidderUnreachable)}, + {ImpId: "4567", StatusCode: int(openrtb_ext.ErrorBidderUnreachable)}, }, }, seatBids: []*entities.PbsOrtbSeatBid{{Bids: []*entities.PbsOrtbBid{}, Currency: "USD", Seat: "appnexus", HttpCalls: []*openrtb_ext.ExtHttpCall{}}}, @@ -3171,7 +3171,7 @@ func TestSeatNonBid(t *testing.T) { }, }, expect: expect{ - seatNonBids: SeatNonBidBuilder{}, + seatNonBids: openrtb_ext.SeatNonBidBuilder{}, seatBids: []*entities.PbsOrtbSeatBid{{Bids: []*entities.PbsOrtbBid{}, Currency: "USD", HttpCalls: []*openrtb_ext.ExtHttpCall{}}}, errors: []error{&url.Error{Op: "Get", URL: "", Err: errors.New("some_error")}}, }, diff --git a/exchange/exchange.go b/exchange/exchange.go index 9ab91ee9ea..a918320c6a 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -105,7 +105,7 @@ type bidResponseWrapper struct { bidder openrtb_ext.BidderName adapter openrtb_ext.BidderName bidderResponseStartTime time.Time - seatNonBidBuilder SeatNonBidBuilder + seatNonBidBuilder openrtb_ext.SeatNonBidBuilder } type BidIDGenerator interface { @@ -377,7 +377,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog anyBidsReturned bool // List of bidders we have requests for. liveAdapters []openrtb_ext.BidderName - seatNonBidBuilder SeatNonBidBuilder = SeatNonBidBuilder{} + seatNonBidBuilder = openrtb_ext.SeatNonBidBuilder{} ) if len(r.StoredAuctionResponses) > 0 { @@ -425,11 +425,13 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog errs = append(errs, &errortypes.Warning{ Message: fmt.Sprintf("%s bid id %s rejected - bid price %.4f %s is less than bid floor %.4f %s for imp %s", rejectedBid.Seat, rejectedBid.Bids[0].Bid.ID, rejectedBid.Bids[0].Bid.Price, rejectedBid.Currency, rejectedBid.Bids[0].BidFloors.FloorValue, rejectedBid.Bids[0].BidFloors.FloorCurrency, rejectedBid.Bids[0].Bid.ImpID), WarningCode: errortypes.FloorBidRejectionWarningCode}) - rejectionReason := ResponseRejectedBelowFloor + rejectionReason := openrtb_ext.ResponseRejectedBelowFloor if rejectedBid.Bids[0].Bid.DealID != "" { - rejectionReason = ResponseRejectedBelowDealFloor + rejectionReason = openrtb_ext.ResponseRejectedBelowDealFloor } - seatNonBidBuilder.rejectBid(rejectedBid.Bids[0], int(rejectionReason), rejectedBid.Seat) + nonBid := openrtb_ext.NewNonBid(openrtb_ext.NonBidParams{Bid: rejectedBid.Bids[0].Bid, NonBidReason: int(rejectionReason), + OriginalBidCPM: rejectedBid.Bids[0].OriginalBidCPM, OriginalBidCur: rejectedBid.Bids[0].OriginalBidCur}) + seatNonBidBuilder.AddBid(nonBid, rejectedBid.Seat) } } @@ -539,11 +541,13 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog if err != nil { return nil, err } - bidResponseExt = setSeatNonBid(bidResponseExt, seatNonBidBuilder) + // // Remove this change move it to auction after adding hooks outcome to SeatNonBids. + // bidResponseExt = setSeatNonBid(bidResponseExt, seatNonBidBuilder) return &AuctionResponse{ BidResponse: bidResponse, ExtBidResponse: bidResponseExt, + SeatNonBid: seatNonBidBuilder, }, nil } @@ -719,7 +723,7 @@ func (e *exchange) getAllBids( adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, len(bidderRequests)) adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, len(bidderRequests)) chBids := make(chan *bidResponseWrapper, len(bidderRequests)) - extraRespInfo := extraAuctionResponseInfo{seatNonBidBuilder: SeatNonBidBuilder{}} + extraRespInfo := extraAuctionResponseInfo{seatNonBidBuilder: openrtb_ext.SeatNonBidBuilder{}} e.me.RecordOverheadTime(metrics.MakeBidderRequests, time.Since(pbsRequestStartTime)) @@ -814,7 +818,7 @@ func (e *exchange) getAllBids( adapterExtra[brw.bidder] = brw.adapterExtra // collect adapter non bids - extraRespInfo.seatNonBidBuilder.append(brw.seatNonBidBuilder) + extraRespInfo.seatNonBidBuilder.Append(brw.seatNonBidBuilder) } @@ -933,7 +937,7 @@ func errsToBidderWarnings(errs []error) []openrtb_ext.ExtBidderMessage { } // This piece takes all the bids supplied by the adapters and crafts an openRTB response to send back to the requester -func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb_ext.RequestWrapper, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error, seatNonBidBuilder *SeatNonBidBuilder) *openrtb2.BidResponse { +func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb_ext.RequestWrapper, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error, seatNonBidBuilder *openrtb_ext.SeatNonBidBuilder) *openrtb2.BidResponse { bidResponse := new(openrtb2.BidResponse) bidResponse.ID = bidRequest.ID @@ -968,7 +972,7 @@ func encodeBidResponseExt(bidResponseExt *openrtb_ext.ExtBidResponse) ([]byte, e return buffer.Bytes(), err } -func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestTargeting, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData, booleanGenerator deduplicateChanceGenerator, seatNonBidBuilder *SeatNonBidBuilder) (map[string]string, map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, []string, error) { +func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestTargeting, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData, booleanGenerator deduplicateChanceGenerator, seatNonBidBuilder *openrtb_ext.SeatNonBidBuilder) (map[string]string, map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, []string, error) { res := make(map[string]string) type bidDedupe struct { @@ -1030,7 +1034,9 @@ func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestT //on receiving bids from adapters if no unique IAB category is returned or if no ad server category is returned discard the bid bidsToRemove = append(bidsToRemove, bidInd) rejections = updateRejections(rejections, bidID, "Bid did not contain a category") - seatNonBidBuilder.rejectBid(bid, int(ResponseRejectedCategoryMappingInvalid), string(bidderName)) + nonBid := openrtb_ext.NewNonBid(openrtb_ext.NonBidParams{Bid: bid.Bid, NonBidReason: int(openrtb_ext.ResponseRejectedCategoryMappingInvalid), + OriginalBidCPM: bid.OriginalBidCPM, OriginalBidCur: bid.OriginalBidCur}) + seatNonBidBuilder.AddBid(nonBid, string(bidderName)) continue } if translateCategories { @@ -1267,7 +1273,7 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*en // Return an openrtb seatBid for a bidder // buildBidResponse is responsible for ensuring nil bid seatbids are not included -func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string, seatNonBidBuilder *SeatNonBidBuilder) *openrtb2.SeatBid { +func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string, seatNonBidBuilder *openrtb_ext.SeatNonBidBuilder) *openrtb2.SeatBid { seatBid := &openrtb2.SeatBid{ Seat: adapter.String(), Group: 0, // Prebid cannot support roadblocking @@ -1282,7 +1288,7 @@ func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter open return seatBid } -func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, seatNonBidBuilder *SeatNonBidBuilder) ([]openrtb2.Bid, []error) { +func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, seatNonBidBuilder *openrtb_ext.SeatNonBidBuilder) ([]openrtb2.Bid, []error) { result := make([]openrtb2.Bid, 0, len(bids)) errs := make([]error, 0, 1) @@ -1293,13 +1299,14 @@ func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCrea Message: fmt.Sprintf("bid rejected: %s", err.Error()), } bidResponseExt.Warnings[adapter] = append(bidResponseExt.Warnings[adapter], dsaMessage) - - seatNonBidBuilder.rejectBid(bid, int(ResponseRejectedGeneral), adapter.String()) + nonBid := openrtb_ext.NewNonBid(openrtb_ext.NonBidParams{Bid: bid.Bid, NonBidReason: int(openrtb_ext.ResponseRejectedGeneral), OriginalBidCPM: bid.OriginalBidCPM, OriginalBidCur: bid.OriginalBidCur}) + seatNonBidBuilder.AddBid(nonBid, adapter.String()) continue // Don't add bid to result } if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationEnforce && bid.BidType == openrtb_ext.BidTypeBanner { if !e.validateBannerCreativeSize(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.BannerCreativeMaxSize) { - seatNonBidBuilder.rejectBid(bid, int(ResponseRejectedCreativeSizeNotAllowed), adapter.String()) + nonBid := openrtb_ext.NewNonBid(openrtb_ext.NonBidParams{Bid: bid.Bid, NonBidReason: int(openrtb_ext.ResponseRejectedCreativeSizeNotAllowed), OriginalBidCPM: bid.OriginalBidCPM, OriginalBidCur: bid.OriginalBidCur}) + seatNonBidBuilder.AddBid(nonBid, adapter.String()) continue // Don't add bid to result } } else if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationWarn && bid.BidType == openrtb_ext.BidTypeBanner { @@ -1308,7 +1315,8 @@ func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCrea if _, ok := impExtInfoMap[bid.Bid.ImpID]; ok { if e.bidValidationEnforcement.SecureMarkup == config.ValidationEnforce && (bid.BidType == openrtb_ext.BidTypeBanner || bid.BidType == openrtb_ext.BidTypeVideo) { if !e.validateBidAdM(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.SecureMarkup) { - seatNonBidBuilder.rejectBid(bid, int(ResponseRejectedCreativeNotSecure), adapter.String()) + nonBid := openrtb_ext.NewNonBid(openrtb_ext.NonBidParams{Bid: bid.Bid, NonBidReason: int(openrtb_ext.ResponseRejectedCreativeNotSecure), OriginalBidCPM: bid.OriginalBidCPM, OriginalBidCur: bid.OriginalBidCur}) + seatNonBidBuilder.AddBid(nonBid, adapter.String()) continue // Don't add bid to result } } else if e.bidValidationEnforcement.SecureMarkup == config.ValidationWarn && (bid.BidType == openrtb_ext.BidTypeBanner || bid.BidType == openrtb_ext.BidTypeVideo) { @@ -1607,18 +1615,19 @@ func setErrorMessageSecureMarkup(validationType string) string { return "" } -// setSeatNonBid adds SeatNonBids within bidResponse.Ext.Prebid.SeatNonBid -func setSeatNonBid(bidResponseExt *openrtb_ext.ExtBidResponse, seatNonBidBuilder SeatNonBidBuilder) *openrtb_ext.ExtBidResponse { - if len(seatNonBidBuilder) == 0 { - return bidResponseExt - } - if bidResponseExt == nil { - bidResponseExt = &openrtb_ext.ExtBidResponse{} - } - if bidResponseExt.Prebid == nil { - bidResponseExt.Prebid = &openrtb_ext.ExtResponsePrebid{} - } - - bidResponseExt.Prebid.SeatNonBid = seatNonBidBuilder.Slice() - return bidResponseExt -} +// // Remove this code. Move to auction file. +// // setSeatNonBid adds SeatNonBids within bidResponse.Ext.Prebid.SeatNonBid +// func setSeatNonBid(bidResponseExt *openrtb_ext.ExtBidResponse, seatNonBidBuilder SeatNonBidBuilder) *openrtb_ext.ExtBidResponse { +// if len(seatNonBidBuilder) == 0 { +// return bidResponseExt +// } +// if bidResponseExt == nil { +// bidResponseExt = &openrtb_ext.ExtBidResponse{} +// } +// if bidResponseExt.Prebid == nil { +// bidResponseExt.Prebid = &openrtb_ext.ExtResponsePrebid{} +// } + +// bidResponseExt.Prebid.SeatNonBid = seatNonBidBuilder.Slice() +// return bidResponseExt +// } diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 87b53b101e..ec92ac515e 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -172,7 +172,7 @@ func TestCharacterEscape(t *testing.T) { var errList []error // 4) Build bid response - bidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, nil, "", errList, &SeatNonBidBuilder{}) + bidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, nil, "", errList, &openrtb_ext.SeatNonBidBuilder{}) // 5) Assert we have no errors and one '&' character as we are supposed to if len(errList) > 0 { @@ -1343,7 +1343,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) { var errList []error // 4) Build bid response - bid_resp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, nil, "", errList, &SeatNonBidBuilder{}) + bid_resp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, nil, "", errList, &openrtb_ext.SeatNonBidBuilder{}) expectedBidResponse := &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{ @@ -1433,7 +1433,7 @@ func TestBidReturnsCreative(t *testing.T) { //Run tests for _, test := range testCases { - resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil, &openrtb_ext.RequestWrapper{}, nil, "", "", &SeatNonBidBuilder{}) + resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil, &openrtb_ext.RequestWrapper{}, nil, "", "", &openrtb_ext.SeatNonBidBuilder{}) assert.Equal(t, 0, len(resultingErrs), "%s. Test should not return errors \n", test.description) assert.Equal(t, test.expectedCreativeMarkup, resultingBids[0].AdM, "%s. Ad markup string doesn't match expected \n", test.description) @@ -1718,7 +1718,7 @@ func TestBidResponseCurrency(t *testing.T) { } // Run tests for i := range testCases { - actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, nil, "", errList, &SeatNonBidBuilder{}) + actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, nil, "", errList, &openrtb_ext.SeatNonBidBuilder{}) assert.Equalf(t, testCases[i].expectedBidResponse, actualBidResp, fmt.Sprintf("[TEST_FAILED] Objects must be equal for test: %s \n Expected: >>%s<< \n Actual: >>%s<< ", testCases[i].description, testCases[i].expectedBidResponse.Ext, actualBidResp.Ext)) } } @@ -1786,7 +1786,7 @@ func TestBidResponseImpExtInfo(t *testing.T) { expectedBidResponseExt := `{"origbidcpm":0,"prebid":{"meta":{"adaptercode":"appnexus"},"type":"video","passthrough":{"imp_passthrough_val":1}},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]}}` - actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, nil, nil, nil, true, impExtInfo, "", errList, &SeatNonBidBuilder{}) + actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, nil, nil, nil, true, impExtInfo, "", errList, &openrtb_ext.SeatNonBidBuilder{}) resBidExt := string(actualBidResp.SeatBid[0].Bid[0].Ext) assert.Equalf(t, expectedBidResponseExt, resBidExt, "Expected bid response extension is incorrect") @@ -2230,10 +2230,11 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { aucResponse, err := ex.HoldAuction(ctx, auctionRequest, debugLog) var bid *openrtb2.BidResponse - var bidExt *openrtb_ext.ExtBidResponse + var seatnonbid *openrtb_ext.SeatNonBidBuilder + if aucResponse != nil { bid = aucResponse.BidResponse - bidExt = aucResponse.ExtBidResponse + seatnonbid = &aucResponse.SeatNonBid } if len(spec.Response.Error) > 0 && spec.Response.Bids == nil { if err.Error() != spec.Response.Error { @@ -2331,9 +2332,10 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { } assert.Equal(t, expectedBidRespExt.Errors, actualBidRespExt.Errors, "Expected errors from response ext do not match") } - if expectedBidRespExt.Prebid != nil { - assert.ElementsMatch(t, expectedBidRespExt.Prebid.SeatNonBid, bidExt.Prebid.SeatNonBid, "Expected seatNonBids from response ext do not match") + if len(spec.Response.SeatNonBids) > 0 { + assert.ElementsMatch(t, seatnonbid.Get(), spec.Response.SeatNonBids, "Expected seatNonBids from response ext do not match") } + } func findBiddersInAuction(t *testing.T, context string, req *openrtb2.BidRequest) []string { @@ -2611,7 +2613,7 @@ func TestCategoryMapping(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &openrtb_ext.SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") @@ -2666,7 +2668,7 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &openrtb_ext.SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be no bid rejection messages") @@ -2718,7 +2720,7 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &openrtb_ext.SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") @@ -2800,7 +2802,7 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &openrtb_ext.SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be no bid rejection messages") @@ -2878,7 +2880,7 @@ func TestCategoryDedupe(t *testing.T) { }, } deduplicateGenerator := fakeBooleanGenerator{value: tt.dedupeGeneratorValue} - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &deduplicateGenerator, &SeatNonBidBuilder{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &deduplicateGenerator, &openrtb_ext.SeatNonBidBuilder{}) assert.Nil(t, err) assert.Equal(t, 3, len(rejections)) @@ -2947,7 +2949,7 @@ func TestNoCategoryDedupe(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &openrtb_ext.SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 2, len(rejections), "There should be 2 bid rejection messages") @@ -3012,7 +3014,7 @@ func TestCategoryMappingBidderName(t *testing.T) { adapterBids[bidderName1] = &seatBid1 adapterBids[bidderName2] = &seatBid2 - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &openrtb_ext.SeatNonBidBuilder{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be 0 bid rejection messages") @@ -3066,7 +3068,7 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) { adapterBids[bidderName1] = &seatBid1 adapterBids[bidderName2] = &seatBid2 - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &openrtb_ext.SeatNonBidBuilder{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be 0 bid rejection messages") @@ -3077,112 +3079,134 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) { assert.Len(t, bidCategory, 2, "Bidders category mapping doesn't match") } -func TestBidRejectionErrors(t *testing.T) { - categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") - if error != nil { - t.Errorf("Failed to create a category Fetcher: %v", error) - } - - requestExt := newExtRequest() - requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50} - - targData := &targetData{ - priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity, - includeWinners: true, - } - - invalidReqExt := newExtRequest() - invalidReqExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50} - invalidReqExt.Prebid.Targeting.IncludeBrandCategory.PrimaryAdServer = 2 - invalidReqExt.Prebid.Targeting.IncludeBrandCategory.Publisher = "some_publisher" - - adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) - bidderName := openrtb_ext.BidderName("appnexus") - - testCases := []struct { - description string - reqExt openrtb_ext.ExtRequest - bids []*openrtb2.Bid - duration int - expectedRejections []string - expectedCatDur string - }{ - { - description: "Bid should be rejected due to not containing a category", - reqExt: requestExt, - bids: []*openrtb2.Bid{ - {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{}, W: 1, H: 1}, - }, - duration: 30, - expectedRejections: []string{ - "bid rejected [bid ID: bid_id1] reason: Bid did not contain a category", - }, - }, - { - description: "Bid should be rejected due to missing category mapping file", - reqExt: invalidReqExt, - bids: []*openrtb2.Bid{ - {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, - }, - duration: 30, - expectedRejections: []string{ - "bid rejected [bid ID: bid_id1] reason: Category mapping file for primary ad server: 'dfp', publisher: 'some_publisher' not found", - }, - }, - { - description: "Bid should be rejected due to duration exceeding maximum", - reqExt: requestExt, - bids: []*openrtb2.Bid{ - {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, - }, - duration: 70, - expectedRejections: []string{ - "bid rejected [bid ID: bid_id1] reason: bid duration exceeds maximum allowed", - }, - }, - { - description: "Bid should be rejected due to duplicate bid", - reqExt: requestExt, - bids: []*openrtb2.Bid{ - {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, - {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, - }, - duration: 30, - expectedRejections: []string{ - "bid rejected [bid ID: bid_id1] reason: Bid was deduplicated", - }, - expectedCatDur: "10.00_VideoGames_30s", - }, - } - - for _, test := range testCases { - innerBids := []*entities.PbsOrtbBid{} - for _, bid := range test.bids { - currentBid := entities.PbsOrtbBid{ - Bid: bid, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} - innerBids = append(innerBids, ¤tBid) - } - - seatBid := entities.PbsOrtbSeatBid{Bids: innerBids, Currency: "USD"} - - adapterBids[bidderName] = &seatBid - - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *test.reqExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) - - if len(test.expectedCatDur) > 0 { - // Bid deduplication case - assert.Equal(t, 1, len(adapterBids[bidderName].Bids), "Bidders number doesn't match") - assert.Equal(t, 1, len(bidCategory), "Bidders category mapping doesn't match") - assert.Equal(t, test.expectedCatDur, bidCategory["bid_id1"], "Bid category did not contain expected hb_pb_cat_dur") - } else { - assert.Empty(t, adapterBids[bidderName].Bids, "Bidders number doesn't match") - assert.Empty(t, bidCategory, "Bidders category mapping doesn't match") - } - - assert.Empty(t, err, "Category mapping error should be empty") - assert.Equal(t, test.expectedRejections, rejections, test.description) - } -} +// func TestBidRejectionErrors(t *testing.T) { +// categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") +// if error != nil { +// t.Errorf("Failed to create a category Fetcher: %v", error) +// } + +// requestExt := newExtRequest() +// requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50} + +// targData := &targetData{ +// priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity, +// includeWinners: true, +// } + +// invalidReqExt := newExtRequest() +// invalidReqExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50} +// invalidReqExt.Prebid.Targeting.IncludeBrandCategory.PrimaryAdServer = 2 +// invalidReqExt.Prebid.Targeting.IncludeBrandCategory.Publisher = "some_publisher" + +// adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) +// bidderName := openrtb_ext.BidderName("appnexus") + +// testCases := []struct { +// description string +// reqExt openrtb_ext.ExtRequest +// bids []*openrtb2.Bid +// duration int +// expectedRejections []string +// expectedCatDur string +// expectedSeatNonBid openrtb_ext.SeatNonBidBuilder +// }{ +// { +// description: "Bid should be rejected due to not containing a category", +// reqExt: requestExt, +// bids: []*openrtb2.Bid{ +// {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{}, W: 1, H: 1}, +// }, +// duration: 30, +// expectedRejections: []string{ +// "bid rejected [bid ID: bid_id1] reason: Bid did not contain a category", +// }, +// expectedSeatNonBid: func() openrtb_ext.SeatNonBidBuilder { +// seatNonBid := openrtb_ext.SeatNonBidBuilder{} +// nonBid := openrtb_ext.NewNonBid(openrtb_ext.NonBidParams{ +// Bid: &openrtb2.Bid{ImpID: "imp_id1", Price: 10, W: 1, H: 1, Cat: []string{}}, +// NonBidReason: 303, +// OriginalBidCPM: 10, +// OriginalBidCur: "USD", +// }) +// seatNonBid.AddBid(nonBid, "appnexus") +// return seatNonBid +// }(), +// }, +// { +// description: "Bid should be rejected due to missing category mapping file", +// reqExt: invalidReqExt, +// bids: []*openrtb2.Bid{ +// {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, +// }, +// duration: 30, +// expectedRejections: []string{ +// "bid rejected [bid ID: bid_id1] reason: Category mapping file for primary ad server: 'dfp', publisher: 'some_publisher' not found", +// }, +// expectedSeatNonBid: func() openrtb_ext.SeatNonBidBuilder { +// return openrtb_ext.SeatNonBidBuilder{} +// }(), +// }, +// { +// description: "Bid should be rejected due to duration exceeding maximum", +// reqExt: requestExt, +// bids: []*openrtb2.Bid{ +// {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, +// }, +// duration: 70, +// expectedRejections: []string{ +// "bid rejected [bid ID: bid_id1] reason: bid duration exceeds maximum allowed", +// }, +// expectedSeatNonBid: func() openrtb_ext.SeatNonBidBuilder { +// return openrtb_ext.SeatNonBidBuilder{} +// }(), +// }, +// { +// description: "Bid should be rejected due to duplicate bid", +// reqExt: requestExt, +// bids: []*openrtb2.Bid{ +// {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, +// {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, +// }, +// duration: 30, +// expectedRejections: []string{ +// "bid rejected [bid ID: bid_id1] reason: Bid was deduplicated", +// }, +// expectedCatDur: "10.00_VideoGames_30s", +// expectedSeatNonBid: func() openrtb_ext.SeatNonBidBuilder { +// return openrtb_ext.SeatNonBidBuilder{} +// }(), +// }, +// } + +// for _, test := range testCases { +// innerBids := []*entities.PbsOrtbBid{} +// for _, bid := range test.bids { +// currentBid := entities.PbsOrtbBid{ +// Bid: bid, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} +// innerBids = append(innerBids, ¤tBid) +// } + +// seatBid := entities.PbsOrtbSeatBid{Bids: innerBids, Currency: "USD"} + +// adapterBids[bidderName] = &seatBid + +// bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *test.reqExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &openrtb_ext.SeatNonBidBuilder{}) + +// if len(test.expectedCatDur) > 0 { +// // Bid deduplication case +// assert.Equal(t, 1, len(adapterBids[bidderName].Bids), "Bidders number doesn't match") +// assert.Equal(t, 1, len(bidCategory), "Bidders category mapping doesn't match") +// assert.Equal(t, test.expectedCatDur, bidCategory["bid_id1"], "Bid category did not contain expected hb_pb_cat_dur") +// } else { +// assert.Empty(t, adapterBids[bidderName].Bids, "Bidders number doesn't match") +// assert.Empty(t, bidCategory, "Bidders category mapping doesn't match") +// } + +// assert.Empty(t, err, "Category mapping error should be empty") +// assert.Equal(t, test.expectedRejections, rejections, test.description) +// assert.Equal(t, test.expectedSeatNonBid, seatNonBid, "SeatNonBid doesn't match") +// } +// } func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { @@ -3230,7 +3254,7 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { adapterBids[bidderNameApn1] = &seatBidApn1 adapterBids[bidderNameApn2] = &seatBidApn2 - bidCategory, _, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) + bidCategory, _, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &openrtb_ext.SeatNonBidBuilder{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Len(t, rejections, 1, "There should be 1 bid rejection message") @@ -3314,7 +3338,7 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) adapterBids[bidderNameApn1] = &seatBidApn1 adapterBids[bidderNameApn2] = &seatBidApn2 - _, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &fakeBooleanGenerator{value: true}, &SeatNonBidBuilder{}) + _, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &fakeBooleanGenerator{value: true}, &openrtb_ext.SeatNonBidBuilder{}) assert.NoError(t, err, "Category mapping error should be empty") @@ -4778,7 +4802,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids []*entities.PbsOrtbBid givenSeat openrtb_ext.BidderName expectedNumOfBids int - expectedNonBids *SeatNonBidBuilder + expectedNonBids *openrtb_ext.SeatNonBidBuilder expectedNumDebugErrors int expectedNumDebugWarnings int }{ @@ -4789,13 +4813,13 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{Ext: json.RawMessage(`{"dsa": {"adrender":1}}`)}}, {Bid: &openrtb2.Bid{}}}, givenSeat: "pubmatic", expectedNumOfBids: 1, - expectedNonBids: &SeatNonBidBuilder{ + expectedNonBids: &openrtb_ext.SeatNonBidBuilder{ "pubmatic": { { StatusCode: 300, - Ext: &openrtb_ext.NonBidExt{ - Prebid: openrtb_ext.ExtResponseNonBidPrebid{ - Bid: openrtb_ext.NonBidObject{}, + Ext: openrtb_ext.ExtNonBid{ + Prebid: openrtb_ext.ExtNonBidPrebid{ + Bid: openrtb_ext.ExtNonBidPrebidBid{}, }, }, }, @@ -4809,13 +4833,13 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 1, - expectedNonBids: &SeatNonBidBuilder{ + expectedNonBids: &openrtb_ext.SeatNonBidBuilder{ "pubmatic": { { StatusCode: 351, - Ext: &openrtb_ext.NonBidExt{ - Prebid: openrtb_ext.ExtResponseNonBidPrebid{ - Bid: openrtb_ext.NonBidObject{ + Ext: openrtb_ext.ExtNonBid{ + Prebid: openrtb_ext.ExtNonBidPrebid{ + Bid: openrtb_ext.ExtNonBidPrebidBid{ W: 200, H: 200, }, @@ -4832,7 +4856,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 2, - expectedNonBids: &SeatNonBidBuilder{}, + expectedNonBids: &openrtb_ext.SeatNonBidBuilder{}, expectedNumDebugErrors: 1, }, { @@ -4841,14 +4865,14 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 1, - expectedNonBids: &SeatNonBidBuilder{ + expectedNonBids: &openrtb_ext.SeatNonBidBuilder{ "pubmatic": { { ImpId: "1", StatusCode: 352, - Ext: &openrtb_ext.NonBidExt{ - Prebid: openrtb_ext.ExtResponseNonBidPrebid{ - Bid: openrtb_ext.NonBidObject{}, + Ext: openrtb_ext.ExtNonBid{ + Prebid: openrtb_ext.ExtNonBidPrebid{ + Bid: openrtb_ext.ExtNonBidPrebidBid{}, }, }, }, @@ -4862,7 +4886,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 2, - expectedNonBids: &SeatNonBidBuilder{}, + expectedNonBids: &openrtb_ext.SeatNonBidBuilder{}, expectedNumDebugErrors: 1, }, { @@ -4871,7 +4895,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid"}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 2, - expectedNonBids: &SeatNonBidBuilder{}, + expectedNonBids: &openrtb_ext.SeatNonBidBuilder{}, }, { name: "Creative_size_validation_skipped,_Adm_Validation_enforced,_one_of_two_bids_has_invalid_dimensions", @@ -4879,7 +4903,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 2, - expectedNonBids: &SeatNonBidBuilder{}, + expectedNonBids: &openrtb_ext.SeatNonBidBuilder{}, }, } @@ -4928,7 +4952,7 @@ func TestMakeBidWithValidation(t *testing.T) { } e.bidValidationEnforcement = test.givenValidations sampleBids := test.givenBids - nonBids := &SeatNonBidBuilder{} + nonBids := &openrtb_ext.SeatNonBidBuilder{} resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, true, ImpExtInfoMap, bidRequest, bidExtResponse, test.givenSeat, "", nonBids) assert.Equal(t, 0, len(resultingErrs)) @@ -5531,9 +5555,10 @@ type exchangeRequest struct { } type exchangeResponse struct { - Bids *openrtb2.BidResponse `json:"bids"` - Error string `json:"error,omitempty"` - Ext json.RawMessage `json:"ext,omitempty"` + Bids *openrtb2.BidResponse `json:"bids"` + Error string `json:"error,omitempty"` + Ext json.RawMessage `json:"ext,omitempty"` + SeatNonBids []openrtb_ext.SeatNonBid `json:"seatnonbids,omitempty"` } type exchangeServer struct { @@ -6061,42 +6086,6 @@ func TestSelectNewDuration(t *testing.T) { } } -func TestSetSeatNonBid(t *testing.T) { - type args struct { - bidResponseExt *openrtb_ext.ExtBidResponse - seatNonBids SeatNonBidBuilder - } - tests := []struct { - name string - args args - want *openrtb_ext.ExtBidResponse - }{ - { - name: "empty-seatNonBidsMap", - args: args{seatNonBids: SeatNonBidBuilder{}, bidResponseExt: nil}, - want: nil, - }, - { - name: "nil-bidResponseExt", - args: args{seatNonBids: SeatNonBidBuilder{"key": nil}, bidResponseExt: nil}, - want: &openrtb_ext.ExtBidResponse{ - Prebid: &openrtb_ext.ExtResponsePrebid{ - SeatNonBid: []openrtb_ext.SeatNonBid{{ - Seat: "key", - }}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := setSeatNonBid(tt.args.bidResponseExt, tt.args.seatNonBids); !reflect.DeepEqual(got, tt.want) { - t.Errorf("setSeatNonBid() = %v, want %v", got, tt.want) - } - }) - } -} - func TestBuildMultiBidMap(t *testing.T) { type testCase struct { desc string diff --git a/exchange/exchangetest/bid_response_validation_enforce_one_bid_rejected.json b/exchange/exchangetest/bid_response_validation_enforce_one_bid_rejected.json index 8c5d22e269..acd8077660 100644 --- a/exchange/exchangetest/bid_response_validation_enforce_one_bid_rejected.json +++ b/exchange/exchangetest/bid_response_validation_enforce_one_bid_rejected.json @@ -191,31 +191,29 @@ "message": "bidResponse rejected: size WxH" } ] - }, - "prebid": { - "seatnonbid": [ + } + }, + "seatnonbids": [ + { + "nonbid": [ { - "nonbid": [ - { - "impid": "some-imp-id", - "statuscode": 351, - "ext": { - "prebid": { - "bid": { - "price": 0.3, - "w": 200, - "h": 500, - "origbidcpm": 0.3 - } - } + "impid": "some-imp-id", + "statuscode": 351, + "ext": { + "prebid": { + "bid": { + "price": 0.3, + "w": 200, + "h": 500, + "origbidcpm": 0.3 } } - ], - "seat": "appnexus", - "ext": null + } } - ] + ], + "seat": "appnexus", + "ext": null } - } + ] } } \ No newline at end of file diff --git a/exchange/non_bid_reason.go b/exchange/non_bid_reason.go index 05d4ea3ee6..9bdadbf7bf 100644 --- a/exchange/non_bid_reason.go +++ b/exchange/non_bid_reason.go @@ -6,31 +6,15 @@ import ( "syscall" "github.com/prebid/prebid-server/v3/errortypes" + "github.com/prebid/prebid-server/v3/openrtb_ext" ) -// SeatNonBid list the reasons why bid was not resulted in positive bid -// reason could be either No bid, Error, Request rejection or Response rejection -// Reference: https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/extensions/community_extensions/seat-non-bid.md#list-non-bid-status-codes -type NonBidReason int64 - -const ( - ErrorGeneral NonBidReason = 100 // Error - General - ErrorTimeout NonBidReason = 101 // Error - Timeout - ErrorBidderUnreachable NonBidReason = 103 // Error - Bidder Unreachable - ResponseRejectedGeneral NonBidReason = 300 - ResponseRejectedBelowFloor NonBidReason = 301 // Response Rejected - Below Floor - ResponseRejectedCategoryMappingInvalid NonBidReason = 303 // Response Rejected - Category Mapping Invalid - ResponseRejectedBelowDealFloor NonBidReason = 304 // Response Rejected - Bid was Below Deal Floor - ResponseRejectedCreativeSizeNotAllowed NonBidReason = 351 // Response Rejected - Invalid Creative (Size Not Allowed) - ResponseRejectedCreativeNotSecure NonBidReason = 352 // Response Rejected - Invalid Creative (Not Secure) -) - -func errorToNonBidReason(err error) NonBidReason { +func errorToNonBidReason(err error) openrtb_ext.NonBidReason { switch errortypes.ReadCode(err) { case errortypes.TimeoutErrorCode: - return ErrorTimeout + return openrtb_ext.ErrorTimeout default: - return ErrorGeneral + return openrtb_ext.ErrorGeneral } } @@ -38,15 +22,15 @@ func errorToNonBidReason(err error) NonBidReason { // It will first try to resolve the NBR based on prebid's proprietary error code. // If proprietary error code not found then it will try to determine NBR using // system call level error code -func httpInfoToNonBidReason(httpInfo *httpCallInfo) NonBidReason { +func httpInfoToNonBidReason(httpInfo *httpCallInfo) openrtb_ext.NonBidReason { nonBidReason := errorToNonBidReason(httpInfo.err) - if nonBidReason != ErrorGeneral { + if nonBidReason != openrtb_ext.ErrorGeneral { return nonBidReason } if isBidderUnreachableError(httpInfo) { - return ErrorBidderUnreachable + return openrtb_ext.ErrorBidderUnreachable } - return ErrorGeneral + return openrtb_ext.ErrorGeneral } // isBidderUnreachableError checks if the error is due to connection refused or no such host diff --git a/exchange/non_bid_reason_test.go b/exchange/non_bid_reason_test.go index fb9c5e434f..9e51c06be9 100644 --- a/exchange/non_bid_reason_test.go +++ b/exchange/non_bid_reason_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/prebid/prebid-server/v3/errortypes" + "github.com/prebid/prebid-server/v3/openrtb_ext" "github.com/stretchr/testify/assert" ) @@ -17,7 +18,7 @@ func Test_httpInfoToNonBidReason(t *testing.T) { tests := []struct { name string args args - want NonBidReason + want openrtb_ext.NonBidReason }{ { name: "error-timeout", @@ -26,7 +27,7 @@ func Test_httpInfoToNonBidReason(t *testing.T) { err: &errortypes.Timeout{}, }, }, - want: ErrorTimeout, + want: openrtb_ext.ErrorTimeout, }, { name: "error-general", @@ -35,7 +36,7 @@ func Test_httpInfoToNonBidReason(t *testing.T) { err: errors.New("some_error"), }, }, - want: ErrorGeneral, + want: openrtb_ext.ErrorGeneral, }, { name: "error-bidderUnreachable", @@ -44,7 +45,7 @@ func Test_httpInfoToNonBidReason(t *testing.T) { err: syscall.ECONNREFUSED, }, }, - want: ErrorBidderUnreachable, + want: openrtb_ext.ErrorBidderUnreachable, }, { name: "error-biddersUnreachable-no-such-host", @@ -53,7 +54,7 @@ func Test_httpInfoToNonBidReason(t *testing.T) { err: &net.DNSError{IsNotFound: true}, }, }, - want: ErrorBidderUnreachable, + want: openrtb_ext.ErrorBidderUnreachable, }, } for _, tt := range tests { diff --git a/exchange/seat_non_bids.go b/exchange/seat_non_bids.go deleted file mode 100644 index fd6fd1da3f..0000000000 --- a/exchange/seat_non_bids.go +++ /dev/null @@ -1,77 +0,0 @@ -package exchange - -import ( - "github.com/prebid/prebid-server/v3/exchange/entities" - "github.com/prebid/prebid-server/v3/openrtb_ext" -) - -type SeatNonBidBuilder map[string][]openrtb_ext.NonBid - -// rejectBid appends a non bid object to the builder based on a bid -func (b SeatNonBidBuilder) rejectBid(bid *entities.PbsOrtbBid, nonBidReason int, seat string) { - if b == nil || bid == nil || bid.Bid == nil { - return - } - - nonBid := openrtb_ext.NonBid{ - ImpId: bid.Bid.ImpID, - StatusCode: nonBidReason, - Ext: &openrtb_ext.NonBidExt{ - Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{ - Price: bid.Bid.Price, - ADomain: bid.Bid.ADomain, - CatTax: bid.Bid.CatTax, - Cat: bid.Bid.Cat, - DealID: bid.Bid.DealID, - W: bid.Bid.W, - H: bid.Bid.H, - Dur: bid.Bid.Dur, - MType: bid.Bid.MType, - OriginalBidCPM: bid.OriginalBidCPM, - OriginalBidCur: bid.OriginalBidCur, - }}, - }, - } - b[seat] = append(b[seat], nonBid) -} - -// rejectImps appends a non bid object to the builder for every specified imp -func (b SeatNonBidBuilder) rejectImps(impIds []string, nonBidReason NonBidReason, seat string) { - nonBids := []openrtb_ext.NonBid{} - for _, impId := range impIds { - nonBid := openrtb_ext.NonBid{ - ImpId: impId, - StatusCode: int(nonBidReason), - } - nonBids = append(nonBids, nonBid) - } - - if len(nonBids) > 0 { - b[seat] = append(b[seat], nonBids...) - } -} - -// slice transforms the seat non bid map into a slice of SeatNonBid objects representing the non-bids for each seat -func (b SeatNonBidBuilder) Slice() []openrtb_ext.SeatNonBid { - seatNonBid := make([]openrtb_ext.SeatNonBid, 0) - for seat, nonBids := range b { - seatNonBid = append(seatNonBid, openrtb_ext.SeatNonBid{ - Seat: seat, - NonBid: nonBids, - }) - } - return seatNonBid -} - -// append adds the nonBids from the input nonBids to the current nonBids. -// This method is not thread safe as we are initializing and writing to map -func (b SeatNonBidBuilder) append(nonBids ...SeatNonBidBuilder) { - if b == nil { - return - } - for _, nonBid := range nonBids { - for seat, nonBids := range nonBid { - b[seat] = append(b[seat], nonBids...) - } - } -} diff --git a/exchange/seat_non_bids_test.go b/exchange/seat_non_bids_test.go deleted file mode 100644 index 4a8f9e8f78..0000000000 --- a/exchange/seat_non_bids_test.go +++ /dev/null @@ -1,533 +0,0 @@ -package exchange - -import ( - "testing" - - "github.com/prebid/openrtb/v20/openrtb2" - "github.com/prebid/prebid-server/v3/exchange/entities" - "github.com/prebid/prebid-server/v3/openrtb_ext" - "github.com/stretchr/testify/assert" -) - -func TestRejectBid(t *testing.T) { - type fields struct { - builder SeatNonBidBuilder - } - type args struct { - bid *entities.PbsOrtbBid - nonBidReason int - seat string - } - tests := []struct { - name string - fields fields - args args - want SeatNonBidBuilder - }{ - { - name: "nil_builder", - fields: fields{ - builder: nil, - }, - args: args{}, - want: nil, - }, - { - name: "nil_pbsortbid", - fields: fields{ - builder: SeatNonBidBuilder{}, - }, - args: args{ - bid: nil, - }, - want: SeatNonBidBuilder{}, - }, - { - name: "nil_bid", - fields: fields{ - builder: SeatNonBidBuilder{}, - }, - args: args{ - bid: &entities.PbsOrtbBid{ - Bid: nil, - }, - }, - want: SeatNonBidBuilder{}, - }, - { - name: "append_nonbids_new_seat", - fields: fields{ - builder: SeatNonBidBuilder{}, - }, - args: args{ - bid: &entities.PbsOrtbBid{ - Bid: &openrtb2.Bid{ - ImpID: "Imp1", - Price: 10, - }, - }, - nonBidReason: int(ErrorGeneral), - seat: "seat1", - }, - want: SeatNonBidBuilder{ - "seat1": []openrtb_ext.NonBid{ - { - ImpId: "Imp1", - StatusCode: int(ErrorGeneral), - Ext: &openrtb_ext.NonBidExt{ - Prebid: openrtb_ext.ExtResponseNonBidPrebid{ - Bid: openrtb_ext.NonBidObject{ - Price: 10, - }, - }, - }, - }, - }, - }, - }, - { - name: "append_nonbids_for_different_seat", - fields: fields{ - builder: SeatNonBidBuilder{ - "seat1": []openrtb_ext.NonBid{ - { - ImpId: "Imp1", - StatusCode: int(ErrorGeneral), - }, - }, - }, - }, - args: args{ - bid: &entities.PbsOrtbBid{ - Bid: &openrtb2.Bid{ - ImpID: "Imp2", - Price: 10, - }, - }, - nonBidReason: int(ErrorGeneral), - seat: "seat2", - }, - want: SeatNonBidBuilder{ - "seat1": []openrtb_ext.NonBid{ - { - ImpId: "Imp1", - StatusCode: int(ErrorGeneral), - }, - }, - "seat2": []openrtb_ext.NonBid{ - { - ImpId: "Imp2", - StatusCode: int(ErrorGeneral), - Ext: &openrtb_ext.NonBidExt{ - Prebid: openrtb_ext.ExtResponseNonBidPrebid{ - Bid: openrtb_ext.NonBidObject{ - Price: 10, - }, - }, - }, - }, - }, - }, - }, - { - name: "append_nonbids_for_existing_seat", - fields: fields{ - builder: SeatNonBidBuilder{ - "seat1": []openrtb_ext.NonBid{ - { - ImpId: "Imp1", - StatusCode: int(ErrorGeneral), - }, - }, - }, - }, - args: args{ - bid: &entities.PbsOrtbBid{ - Bid: &openrtb2.Bid{ - ImpID: "Imp2", - Price: 10, - }, - }, - nonBidReason: int(ErrorGeneral), - seat: "seat1", - }, - want: SeatNonBidBuilder{ - "seat1": []openrtb_ext.NonBid{ - { - ImpId: "Imp1", - StatusCode: int(ErrorGeneral), - }, - { - ImpId: "Imp2", - StatusCode: int(ErrorGeneral), - Ext: &openrtb_ext.NonBidExt{ - Prebid: openrtb_ext.ExtResponseNonBidPrebid{ - Bid: openrtb_ext.NonBidObject{ - Price: 10, - }, - }, - }, - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - snb := tt.fields.builder - snb.rejectBid(tt.args.bid, tt.args.nonBidReason, tt.args.seat) - assert.Equal(t, tt.want, snb) - }) - } -} - -func TestAppend(t *testing.T) { - tests := []struct { - name string - builder SeatNonBidBuilder - toAppend []SeatNonBidBuilder - expected SeatNonBidBuilder - }{ - { - name: "nil_buider", - builder: nil, - toAppend: []SeatNonBidBuilder{{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}}, - expected: nil, - }, - { - name: "empty_builder", - builder: SeatNonBidBuilder{}, - toAppend: []SeatNonBidBuilder{{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}}, - expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, - }, - { - name: "append_one_different_seat", - builder: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, - toAppend: []SeatNonBidBuilder{{"seat2": []openrtb_ext.NonBid{{ImpId: "imp2"}}}}, - expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}, "seat2": []openrtb_ext.NonBid{{ImpId: "imp2"}}}, - }, - { - name: "append_multiple_different_seats", - builder: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, - toAppend: []SeatNonBidBuilder{{"seat2": []openrtb_ext.NonBid{{ImpId: "imp2"}}}, {"seat3": []openrtb_ext.NonBid{{ImpId: "imp3"}}}}, - expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}, "seat2": []openrtb_ext.NonBid{{ImpId: "imp2"}}, "seat3": []openrtb_ext.NonBid{{ImpId: "imp3"}}}, - }, - { - name: "nil_append", - builder: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, - toAppend: nil, - expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, - }, - { - name: "empty_append", - builder: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, - toAppend: []SeatNonBidBuilder{}, - expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, - }, - { - name: "append_multiple_same_seat", - builder: SeatNonBidBuilder{ - "seat1": []openrtb_ext.NonBid{ - {ImpId: "imp1"}, - }, - }, - toAppend: []SeatNonBidBuilder{ - { - "seat1": []openrtb_ext.NonBid{ - {ImpId: "imp2"}, - }, - }, - }, - expected: SeatNonBidBuilder{ - "seat1": []openrtb_ext.NonBid{ - {ImpId: "imp1"}, - {ImpId: "imp2"}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.builder.append(tt.toAppend...) - assert.Equal(t, tt.expected, tt.builder) - }) - } -} - -func TestRejectImps(t *testing.T) { - tests := []struct { - name string - impIDs []string - builder SeatNonBidBuilder - want SeatNonBidBuilder - }{ - { - name: "nil_imps", - impIDs: nil, - builder: SeatNonBidBuilder{}, - want: SeatNonBidBuilder{}, - }, - { - name: "empty_imps", - impIDs: []string{}, - builder: SeatNonBidBuilder{}, - want: SeatNonBidBuilder{}, - }, - { - name: "one_imp", - impIDs: []string{"imp1"}, - builder: SeatNonBidBuilder{}, - want: SeatNonBidBuilder{ - "seat1": []openrtb_ext.NonBid{ - { - ImpId: "imp1", - StatusCode: 300, - }, - }, - }, - }, - { - name: "many_imps", - impIDs: []string{"imp1", "imp2"}, - builder: SeatNonBidBuilder{}, - want: SeatNonBidBuilder{ - "seat1": []openrtb_ext.NonBid{ - { - ImpId: "imp1", - StatusCode: 300, - }, - { - ImpId: "imp2", - StatusCode: 300, - }, - }, - }, - }, - { - name: "many_imps_appended_to_prepopulated_list", - impIDs: []string{"imp1", "imp2"}, - builder: SeatNonBidBuilder{ - "seat0": []openrtb_ext.NonBid{ - { - ImpId: "imp0", - StatusCode: 0, - }, - }, - }, - want: SeatNonBidBuilder{ - "seat0": []openrtb_ext.NonBid{ - { - ImpId: "imp0", - StatusCode: 0, - }, - }, - "seat1": []openrtb_ext.NonBid{ - { - ImpId: "imp1", - StatusCode: 300, - }, - { - ImpId: "imp2", - StatusCode: 300, - }, - }, - }, - }, - { - name: "many_imps_appended_to_prepopulated_list_same_seat", - impIDs: []string{"imp1", "imp2"}, - builder: SeatNonBidBuilder{ - "seat1": []openrtb_ext.NonBid{ - { - ImpId: "imp0", - StatusCode: 300, - }, - }, - }, - want: SeatNonBidBuilder{ - "seat1": []openrtb_ext.NonBid{ - { - ImpId: "imp0", - StatusCode: 300, - }, - { - ImpId: "imp1", - StatusCode: 300, - }, - { - ImpId: "imp2", - StatusCode: 300, - }, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - test.builder.rejectImps(test.impIDs, 300, "seat1") - - assert.Equal(t, len(test.builder), len(test.want)) - for seat := range test.want { - assert.ElementsMatch(t, test.want[seat], test.builder[seat]) - } - }) - } -} - -func TestSlice(t *testing.T) { - tests := []struct { - name string - builder SeatNonBidBuilder - want []openrtb_ext.SeatNonBid - }{ - { - name: "nil", - builder: nil, - want: []openrtb_ext.SeatNonBid{}, - }, - { - name: "empty", - builder: SeatNonBidBuilder{}, - want: []openrtb_ext.SeatNonBid{}, - }, - { - name: "one_no_nonbids", - builder: SeatNonBidBuilder{ - "a": []openrtb_ext.NonBid{}, - }, - want: []openrtb_ext.SeatNonBid{ - { - NonBid: []openrtb_ext.NonBid{}, - Seat: "a", - }, - }, - }, - { - name: "one_with_nonbids", - builder: SeatNonBidBuilder{ - "a": []openrtb_ext.NonBid{ - { - ImpId: "imp1", - StatusCode: 100, - }, - { - ImpId: "imp2", - StatusCode: 200, - }, - }, - }, - want: []openrtb_ext.SeatNonBid{ - { - NonBid: []openrtb_ext.NonBid{ - { - ImpId: "imp1", - StatusCode: 100, - }, - { - ImpId: "imp2", - StatusCode: 200, - }, - }, - Seat: "a", - }, - }, - }, - { - name: "many_no_nonbids", - builder: SeatNonBidBuilder{ - "a": []openrtb_ext.NonBid{}, - "b": []openrtb_ext.NonBid{}, - "c": []openrtb_ext.NonBid{}, - }, - want: []openrtb_ext.SeatNonBid{ - { - NonBid: []openrtb_ext.NonBid{}, - Seat: "a", - }, - { - NonBid: []openrtb_ext.NonBid{}, - Seat: "b", - }, - { - NonBid: []openrtb_ext.NonBid{}, - Seat: "c", - }, - }, - }, - { - name: "many_with_nonbids", - builder: SeatNonBidBuilder{ - "a": []openrtb_ext.NonBid{ - { - ImpId: "imp1", - StatusCode: 100, - }, - { - ImpId: "imp2", - StatusCode: 200, - }, - }, - "b": []openrtb_ext.NonBid{ - { - ImpId: "imp3", - StatusCode: 300, - }, - }, - "c": []openrtb_ext.NonBid{ - { - ImpId: "imp4", - StatusCode: 400, - }, - { - ImpId: "imp5", - StatusCode: 500, - }, - }, - }, - want: []openrtb_ext.SeatNonBid{ - { - NonBid: []openrtb_ext.NonBid{ - { - ImpId: "imp1", - StatusCode: 100, - }, - { - ImpId: "imp2", - StatusCode: 200, - }, - }, - Seat: "a", - }, - { - NonBid: []openrtb_ext.NonBid{ - { - ImpId: "imp3", - StatusCode: 300, - }, - }, - Seat: "b", - }, - { - NonBid: []openrtb_ext.NonBid{ - { - ImpId: "imp4", - StatusCode: 400, - }, - { - ImpId: "imp5", - StatusCode: 500, - }, - }, - Seat: "c", - }, - }, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - result := test.builder.Slice() - assert.ElementsMatch(t, test.want, result) - }) - } -} diff --git a/hooks/hookexecution/enricher_test.go b/hooks/hookexecution/enricher_test.go index beb07444d1..14271780e3 100644 --- a/hooks/hookexecution/enricher_test.go +++ b/hooks/hookexecution/enricher_test.go @@ -31,14 +31,15 @@ type GroupOutcomeTest struct { type HookOutcomeTest struct { ExecutionTime - AnalyticsTags hookanalytics.Analytics `json:"analytics_tags"` - HookID HookID `json:"hook_id"` - Status Status `json:"status"` - Action Action `json:"action"` - Message string `json:"message"` - DebugMessages []string `json:"debug_messages"` - Errors []string `json:"errors"` - Warnings []string `json:"warnings"` + AnalyticsTags hookanalytics.Analytics `json:"analytics_tags"` + HookID HookID `json:"hook_id"` + Status Status `json:"status"` + Action Action `json:"action"` + Message string `json:"message"` + DebugMessages []string `json:"debug_messages"` + Errors []string `json:"errors"` + Warnings []string `json:"warnings"` + SeatNonBid openrtb_ext.SeatNonBidBuilder `json:"seatnonbid"` } func TestEnrichBidResponse(t *testing.T) { diff --git a/hooks/hookexecution/execution.go b/hooks/hookexecution/execution.go index caaba59c3b..82034c8e53 100644 --- a/hooks/hookexecution/execution.go +++ b/hooks/hookexecution/execution.go @@ -193,6 +193,7 @@ func handleHookResponse[P any]( Warnings: hr.Result.Warnings, DebugMessages: hr.Result.DebugMessages, AnalyticsTags: hr.Result.AnalyticsTags, + SeatNonBid: hr.Result.SeatNonBid, ExecutionTime: ExecutionTime{ExecutionTimeMillis: hr.ExecutionTime}, } diff --git a/hooks/hookexecution/outcome.go b/hooks/hookexecution/outcome.go index d17d3cd467..7f8976ac3c 100644 --- a/hooks/hookexecution/outcome.go +++ b/hooks/hookexecution/outcome.go @@ -4,6 +4,7 @@ import ( "time" "github.com/prebid/prebid-server/v3/hooks/hookanalytics" + "github.com/prebid/prebid-server/v3/openrtb_ext" ) // Status indicates the result of hook execution. @@ -77,14 +78,15 @@ type GroupOutcome struct { type HookOutcome struct { // ExecutionTime is the execution time of a specific hook without applying its result. ExecutionTime - AnalyticsTags hookanalytics.Analytics `json:"analytics_tags"` - HookID HookID `json:"hook_id"` - Status Status `json:"status"` - Action Action `json:"action"` - Message string `json:"message"` // arbitrary string value returned from hook execution - DebugMessages []string `json:"debug_messages,omitempty"` - Errors []string `json:"-"` - Warnings []string `json:"-"` + AnalyticsTags hookanalytics.Analytics `json:"analytics_tags"` + HookID HookID `json:"hook_id"` + Status Status `json:"status"` + Action Action `json:"action"` + Message string `json:"message"` // arbitrary string value returned from hook execution + DebugMessages []string `json:"debug_messages,omitempty"` + Errors []string `json:"-"` + Warnings []string `json:"-"` + SeatNonBid openrtb_ext.SeatNonBidBuilder `json:"-"` } // HookID points to the specific hook defined by the hook execution plan. diff --git a/hooks/hookstage/invocation.go b/hooks/hookstage/invocation.go index 6408e8667d..246a6423f2 100644 --- a/hooks/hookstage/invocation.go +++ b/hooks/hookstage/invocation.go @@ -4,6 +4,7 @@ import ( "encoding/json" "github.com/prebid/prebid-server/v3/hooks/hookanalytics" + "github.com/prebid/prebid-server/v3/openrtb_ext" ) // HookResult represents the result of execution the concrete hook instance. @@ -16,7 +17,8 @@ type HookResult[T any] struct { Warnings []string DebugMessages []string AnalyticsTags hookanalytics.Analytics - ModuleContext ModuleContext // holds values that the module wants to pass to itself at later stages + ModuleContext ModuleContext // holds values that the module wants to pass to itself at later stages + SeatNonBid openrtb_ext.SeatNonBidBuilder // holds list of seatnonbid rejected by hook } // ModuleInvocationContext holds data passed to the module hook during invocation. diff --git a/openrtb_ext/response.go b/openrtb_ext/response.go index 449ff939bf..9feeb969e4 100644 --- a/openrtb_ext/response.go +++ b/openrtb_ext/response.go @@ -102,10 +102,10 @@ const ( UserSyncPixel UserSyncType = "pixel" ) -// NonBidObject is subset of Bid object with exact json signature +// ExtNonBidPrebidBid is subset of Bid object with exact json signature +// defined at https://github.com/prebid/openrtb/blob/v19.0.0/openrtb2/bid.go // It also contains the custom fields -type NonBidObject struct { - // SubSet +type ExtNonBidPrebidBid struct { Price float64 `json:"price,omitempty"` ADomain []string `json:"adomain,omitempty"` CatTax adcom1.CategoryTaxonomy `json:"cattax,omitempty"` @@ -121,20 +121,20 @@ type NonBidObject struct { OriginalBidCur string `json:"origbidcur,omitempty"` } -// ExtResponseNonBidPrebid represents bidresponse.ext.prebid.seatnonbid[].nonbid[].ext -type ExtResponseNonBidPrebid struct { - Bid NonBidObject `json:"bid"` +// ExtNonBidPrebid represents bidresponse.ext.prebid.seatnonbid[].nonbid[].ext +type ExtNonBidPrebid struct { + Bid ExtNonBidPrebidBid `json:"bid"` } -type NonBidExt struct { - Prebid ExtResponseNonBidPrebid `json:"prebid"` +type ExtNonBid struct { + Prebid ExtNonBidPrebid `json:"prebid"` } // NonBid represnts the Non Bid Reason (statusCode) for given impression ID type NonBid struct { - ImpId string `json:"impid"` - StatusCode int `json:"statuscode"` - Ext *NonBidExt `json:"ext,omitempty"` + ImpId string `json:"impid"` + StatusCode int `json:"statuscode"` + Ext ExtNonBid `json:"ext,omitempty"` } // SeatNonBid is collection of NonBid objects with seat information diff --git a/openrtb_ext/seat_non_bids.go b/openrtb_ext/seat_non_bids.go new file mode 100644 index 0000000000..4e5c56d40b --- /dev/null +++ b/openrtb_ext/seat_non_bids.go @@ -0,0 +1,112 @@ +package openrtb_ext + +import ( + "github.com/prebid/openrtb/v20/openrtb2" +) + +// SeatNonBid list the reasons why bid was not resulted in positive bid +// reason could be either No bid, Error, Request rejection or Response rejection +// Reference: https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/extensions/community_extensions/seat-non-bid.md#list-non-bid-status-codes +type NonBidReason int64 + +const ( + ErrorGeneral NonBidReason = 100 // Error - General + ErrorTimeout NonBidReason = 101 // Error - Timeout + ErrorBidderUnreachable NonBidReason = 103 // Error - Bidder Unreachable + ResponseRejectedGeneral NonBidReason = 300 + ResponseRejectedBelowFloor NonBidReason = 301 // Response Rejected - Below Floor + ResponseRejectedCategoryMappingInvalid NonBidReason = 303 // Response Rejected - Category Mapping Invalid + ResponseRejectedBelowDealFloor NonBidReason = 304 // Response Rejected - Bid was Below Deal Floor + ResponseRejectedCreativeSizeNotAllowed NonBidReason = 351 // Response Rejected - Invalid Creative (Size Not Allowed) + ResponseRejectedCreativeNotSecure NonBidReason = 352 // Response Rejected - Invalid Creative (Not Secure) +) + +// NonBidCollection contains the map of seat with list of nonBids +type SeatNonBidBuilder map[string][]NonBid + +// NonBidParams contains the fields that are required to form the nonBid object +type NonBidParams struct { + Bid *openrtb2.Bid + NonBidReason int + OriginalBidCPM float64 + OriginalBidCur string +} + +// NewNonBid creates the NonBid object from NonBidParams and return it +func NewNonBid(bidParams NonBidParams) NonBid { + if bidParams.Bid == nil { + bidParams.Bid = &openrtb2.Bid{} + } + return NonBid{ + ImpId: bidParams.Bid.ImpID, + StatusCode: bidParams.NonBidReason, + Ext: ExtNonBid{ + Prebid: ExtNonBidPrebid{Bid: ExtNonBidPrebidBid{ + Price: bidParams.Bid.Price, + ADomain: bidParams.Bid.ADomain, + CatTax: bidParams.Bid.CatTax, + Cat: bidParams.Bid.Cat, + DealID: bidParams.Bid.DealID, + W: bidParams.Bid.W, + H: bidParams.Bid.H, + Dur: bidParams.Bid.Dur, + MType: bidParams.Bid.MType, + OriginalBidCPM: bidParams.OriginalBidCPM, + OriginalBidCur: bidParams.OriginalBidCur, + }}, + }, + } +} + +// AddBid adds the nonBid into the map against the respective seat. +// Note: This function is not a thread safe. +func (snb *SeatNonBidBuilder) AddBid(nonBid NonBid, seat string) { + if *snb == nil { + *snb = make(map[string][]NonBid) + } + (*snb)[seat] = append((*snb)[seat], nonBid) +} + +// append adds the nonBids from the input nonBids to the current nonBids. +// This method is not thread safe as we are initializing and writing to map +func (snb *SeatNonBidBuilder) Append(nonBids ...SeatNonBidBuilder) { + if *snb == nil { + return + } + for _, nonBid := range nonBids { + for seat, nonBids := range nonBid { + (*snb)[seat] = append((*snb)[seat], nonBids...) + } + } +} + +// Get function converts the internal seatNonBidsMap to standard openrtb seatNonBid structure and returns it +func (snb *SeatNonBidBuilder) Get() []SeatNonBid { + if *snb == nil { + return nil + } + var seatNonBid []SeatNonBid + for seat, nonBids := range *snb { + seatNonBid = append(seatNonBid, SeatNonBid{ + Seat: seat, + NonBid: nonBids, + }) + } + return seatNonBid +} + +// rejectImps appends a non bid object to the builder for every specified imp +func (b *SeatNonBidBuilder) RejectImps(impIds []string, nonBidReason NonBidReason, seat string) { + nonBids := []NonBid{} + for _, impId := range impIds { + nonBid := NonBid{ + ImpId: impId, + StatusCode: int(nonBidReason), + } + nonBids = append(nonBids, nonBid) + } + + if len(nonBids) > 0 { + (*b)[seat] = append((*b)[seat], nonBids...) + } +} diff --git a/openrtb_ext/seat_non_bids_test.go b/openrtb_ext/seat_non_bids_test.go new file mode 100644 index 0000000000..ff91749c90 --- /dev/null +++ b/openrtb_ext/seat_non_bids_test.go @@ -0,0 +1,177 @@ +package openrtb_ext + +import ( + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/stretchr/testify/assert" +) + +func TestNewNonBid(t *testing.T) { + tests := []struct { + name string + bidParams NonBidParams + expectedNonBid NonBid + }{ + { + name: "nil-bid-present-in-bidparams", + bidParams: NonBidParams{Bid: nil}, + expectedNonBid: NonBid{}, + }, + { + name: "non-nil-bid-present-in-bidparams", + bidParams: NonBidParams{Bid: &openrtb2.Bid{ImpID: "imp1"}, NonBidReason: 100}, + expectedNonBid: NonBid{ImpId: "imp1", StatusCode: 100}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nonBid := NewNonBid(tt.bidParams) + assert.Equal(t, tt.expectedNonBid, nonBid, "found incorrect nonBid") + }) + } +} + +func TestSeatNonBidsAdd(t *testing.T) { + type fields struct { + seatNonBidsMap SeatNonBidBuilder + } + type args struct { + nonbid NonBid + seat string + } + tests := []struct { + name string + fields fields + args args + want SeatNonBidBuilder + }{ + { + name: "nil-seatNonBidsMap", + fields: fields{seatNonBidsMap: nil}, + args: args{ + nonbid: NonBid{}, + seat: "bidder1", + }, + want: sampleSeatNonBidMap("bidder1", 1), + }, + { + name: "non-nil-seatNonBidsMap", + fields: fields{seatNonBidsMap: nil}, + args: args{ + + nonbid: NonBid{}, + seat: "bidder1", + }, + want: sampleSeatNonBidMap("bidder1", 1), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + snb := tt.fields.seatNonBidsMap + snb.AddBid(tt.args.nonbid, tt.args.seat) + assert.Equalf(t, tt.want, snb, "found incorrect seatNonBidsMap") + }) + } +} + +func TestSeatNonBidsGet(t *testing.T) { + type fields struct { + snb SeatNonBidBuilder + } + tests := []struct { + name string + fields fields + want []SeatNonBid + }{ + { + name: "get-seat-nonbids", + fields: fields{sampleSeatNonBidMap("bidder1", 2)}, + want: sampleSeatBids("bidder1", 2), + }, + { + name: "nil-seat-nonbids", + fields: fields{nil}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.fields.snb.Get(); !assert.Equal(t, tt.want, got) { + t.Errorf("seatNonBids.get() = %v, want %v", got, tt.want) + } + }) + } +} + +var sampleSeatNonBidMap = func(seat string, nonBidCount int) SeatNonBidBuilder { + nonBids := make([]NonBid, 0) + for i := 0; i < nonBidCount; i++ { + nonBids = append(nonBids, NonBid{ + Ext: ExtNonBid{Prebid: ExtNonBidPrebid{Bid: ExtNonBidPrebidBid{}}}, + }) + } + return SeatNonBidBuilder{ + seat: nonBids, + } +} + +var sampleSeatBids = func(seat string, nonBidCount int) []SeatNonBid { + seatNonBids := make([]SeatNonBid, 0) + seatNonBid := SeatNonBid{ + Seat: seat, + NonBid: make([]NonBid, 0), + } + for i := 0; i < nonBidCount; i++ { + seatNonBid.NonBid = append(seatNonBid.NonBid, NonBid{ + Ext: ExtNonBid{Prebid: ExtNonBidPrebid{Bid: ExtNonBidPrebidBid{}}}, + }) + } + seatNonBids = append(seatNonBids, seatNonBid) + return seatNonBids +} + +func TestSeatNonBidsMerge(t *testing.T) { + + tests := []struct { + name string + snb SeatNonBidBuilder + input SeatNonBidBuilder + want SeatNonBidBuilder + }{ + { + name: "target-SeatNonBidBuilder-is-nil", + snb: nil, + want: nil, + }, + { + name: "input-SeatNonBidBuilder-contains-nil-map", + snb: SeatNonBidBuilder{}, + input: nil, + want: SeatNonBidBuilder{}, + }, + { + name: "input-SeatNonBidBuilder-contains-empty-nonBids", + snb: SeatNonBidBuilder{}, + input: SeatNonBidBuilder{}, + want: SeatNonBidBuilder{}, + }, + { + name: "append-nonbids-in-empty-target-SeatNonBidBuilder", + snb: SeatNonBidBuilder{}, + input: sampleSeatNonBidMap("pubmatic", 1), + want: sampleSeatNonBidMap("pubmatic", 1), + }, + { + name: "merge-multiple-nonbids-in-non-empty-target-SeatNonBidBuilder", + snb: sampleSeatNonBidMap("pubmatic", 1), + input: sampleSeatNonBidMap("pubmatic", 1), + want: sampleSeatNonBidMap("pubmatic", 2), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.snb.Append(tt.input) + assert.Equal(t, tt.want, tt.snb, "incorrect SeatNonBidBuilder generated by Append") + }) + } +}