diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 8cc8e9bd454..1f3d622b80d 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -35,10 +35,10 @@ import ( const defaultAmpRequestTimeoutMillis = 900 type AmpResponse struct { - Targeting map[string]string `json:"targeting"` - Debug *openrtb_ext.ExtResponseDebug `json:"debug,omitempty"` - Errors map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError `json:"errors,omitempty"` - Warnings map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError `json:"warnings,omitempty"` + Targeting map[string]string `json:"targeting"` + Debug *openrtb_ext.ExtResponseDebug `json:"debug,omitempty"` + Errors map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage `json:"errors,omitempty"` + Warnings map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage `json:"warnings,omitempty"` } // NewAmpEndpoint modifies the OpenRTB endpoint to handle AMP requests. This will basically modify the parsing @@ -239,9 +239,12 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h ao.Errors = append(ao.Errors, fmt.Errorf("AMP response: failed to unpack OpenRTB response.ext, debug info cannot be forwarded: %v", eRErr)) } - warnings := make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError) + warnings := extResponse.Warnings + if warnings == nil { + warnings = make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage) + } for _, v := range errortypes.WarningOnly(errL) { - bidderErr := openrtb_ext.ExtBidderError{ + bidderErr := openrtb_ext.ExtBidderMessage{ Code: errortypes.ReadCode(v), Message: v.Error(), } @@ -528,8 +531,9 @@ func readPolicy(consent string) (privacy.PolicyWriter, error) { return ccpa.ConsentWriter{consent}, nil } - return privacy.NilPolicyWriter{}, &errortypes.InvalidPrivacyConsent{ - Message: fmt.Sprintf("Consent '%s' is not recognized as either CCPA or GDPR TCF.", consent), + return privacy.NilPolicyWriter{}, &errortypes.Warning{ + Message: fmt.Sprintf("Consent '%s' is not recognized as either CCPA or GDPR TCF.", consent), + WarningCode: errortypes.InvalidPrivacyConsentWarningCode, } } diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 08461d40da3..77e76613c29 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -395,103 +395,119 @@ func TestCCPAConsent(t *testing.T) { } } -func TestNoConsent(t *testing.T) { - // Build Request - bid, err := getTestBidRequest(true, nil, true, nil) - if err != nil { - t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) +func TestConsentWarnings(t *testing.T) { + type inputTest struct { + regs *openrtb_ext.ExtRegs + invalidConsentURL bool + expectedWarnings map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage } + invalidConsent := "invalid" - // Simulated Stored Request Backend - stored := map[string]json.RawMessage{"1": json.RawMessage(bid)} + bidderWarning := openrtb_ext.ExtBidderMessage{ + Code: 10003, + Message: "debug turned off for bidder", + } + invalidCCPAWarning := openrtb_ext.ExtBidderMessage{ + Code: 10001, + Message: "Consent '" + invalidConsent + "' is not recognized as either CCPA or GDPR TCF.", + } + invalidConsentWarning := openrtb_ext.ExtBidderMessage{ + Code: 10001, + Message: "CCPA consent is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)", + } - // Build Exchange Endpoint - mockExchange := &mockAmpExchange{} - endpoint, _ := NewAmpEndpoint( - mockExchange, - newParamsValidator(t), - &mockAmpStoredReqFetcher{stored}, - empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: maxSize}, - newTestMetrics(), - analyticsConf.NewPBSAnalytics(&config.Analytics{}), - map[string]string{}, - []byte{}, - openrtb_ext.BuildBidderMap(), - ) + testData := []inputTest{ + { + regs: nil, + invalidConsentURL: false, + expectedWarnings: nil, + }, + { + regs: nil, + invalidConsentURL: true, + expectedWarnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{openrtb_ext.BidderReservedGeneral: {invalidCCPAWarning}}, + }, + { + regs: &openrtb_ext.ExtRegs{USPrivacy: "invalid"}, + invalidConsentURL: true, + expectedWarnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{ + openrtb_ext.BidderReservedGeneral: {invalidCCPAWarning, invalidConsentWarning}, + openrtb_ext.BidderName("appnexus"): {bidderWarning}, + }, + }, + { + regs: &openrtb_ext.ExtRegs{USPrivacy: "1NYN"}, + invalidConsentURL: false, + expectedWarnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{openrtb_ext.BidderName("appnexus"): {bidderWarning}}, + }, + } - // Invoke Endpoint - request := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1", nil) - responseRecorder := httptest.NewRecorder() - endpoint(responseRecorder, request, nil) + for _, testCase := range testData { - // Parse Response - var response AmpResponse - if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { - t.Fatalf("Error unmarshalling response: %s", err.Error()) - } + bid, err := getTestBidRequest(true, nil, testCase.regs == nil, testCase.regs) + if err != nil { + t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) + } - // Assert Result - result := mockExchange.lastRequest - assert.NotNil(t, result, "lastRequest") - assert.Nil(t, result.User, "lastRequest.User") - assert.Nil(t, result.Regs, "lastRequest.Regs") - assert.Equal(t, expectedErrorsFromHoldAuction, response.Errors) - assert.Empty(t, response.Warnings) -} + // Simulated Stored Request Backend + stored := map[string]json.RawMessage{"1": json.RawMessage(bid)} -func TestInvalidConsent(t *testing.T) { - // Build Request - bid, err := getTestBidRequest(true, nil, true, nil) - if err != nil { - t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) - } + // Build Exchange Endpoint + var mockExchange exchange.Exchange + if testCase.regs != nil { + mockExchange = &mockAmpExchangeWarnings{} + } else { + mockExchange = &mockAmpExchange{} + } + endpoint, _ := NewAmpEndpoint( + mockExchange, + newParamsValidator(t), + &mockAmpStoredReqFetcher{stored}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: maxSize}, + newTestMetrics(), + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + []byte{}, + openrtb_ext.BuildBidderMap(), + ) - // Simulated Stored Request Backend - stored := map[string]json.RawMessage{"1": json.RawMessage(bid)} + // Invoke Endpoint + var request *http.Request - // Build Exchange Endpoint - mockExchange := &mockAmpExchange{} - endpoint, _ := NewAmpEndpoint( - mockExchange, - newParamsValidator(t), - &mockAmpStoredReqFetcher{stored}, - empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: maxSize}, - newTestMetrics(), - analyticsConf.NewPBSAnalytics(&config.Analytics{}), - map[string]string{}, - []byte{}, - openrtb_ext.BuildBidderMap(), - ) + if testCase.invalidConsentURL { + request = httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1&consent_string="+invalidConsent, nil) - // Invoke Endpoint - invalidConsent := "invalid" - request := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1&consent_string="+invalidConsent, nil) - responseRecorder := httptest.NewRecorder() - endpoint(responseRecorder, request, nil) + } else { + request = httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1", nil) + } - // Parse Response - var response AmpResponse - if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { - t.Fatalf("Error unmarshalling response: %s", err.Error()) - } + responseRecorder := httptest.NewRecorder() + endpoint(responseRecorder, request, nil) - // Assert Result - expectedWarnings := map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError{ - openrtb_ext.BidderReservedGeneral: { - { - Code: 10001, - Message: "Consent '" + invalidConsent + "' is not recognized as either CCPA or GDPR TCF.", - }, - }, + // Parse Response + var response AmpResponse + if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { + t.Fatalf("Error unmarshalling response: %s", err.Error()) + } + + // Assert Result + if testCase.regs == nil { + result := mockExchange.(*mockAmpExchange).lastRequest + assert.NotNil(t, result, "lastRequest") + assert.Nil(t, result.User, "lastRequest.User") + assert.Nil(t, result.Regs, "lastRequest.Regs") + assert.Equal(t, expectedErrorsFromHoldAuction, response.Errors) + if testCase.invalidConsentURL { + assert.Equal(t, testCase.expectedWarnings, response.Warnings) + } else { + assert.Empty(t, response.Warnings) + } + + } else { + assert.Equal(t, testCase.expectedWarnings, response.Warnings) + } } - result := mockExchange.lastRequest - assert.NotNil(t, result, "lastRequest") - assert.Nil(t, result.User, "lastRequest.User") - assert.Nil(t, result.Regs, "lastRequest.Regs") - assert.Equal(t, expectedErrorsFromHoldAuction, response.Errors) - assert.Equal(t, expectedWarnings, response.Warnings) } func TestNewAndLegacyConsentBothProvided(t *testing.T) { @@ -929,7 +945,7 @@ type mockAmpExchange struct { lastRequest *openrtb.BidRequest } -var expectedErrorsFromHoldAuction map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError = map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError{ +var expectedErrorsFromHoldAuction map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage = map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{ openrtb_ext.BidderName("openx"): { { Code: 1, @@ -962,6 +978,21 @@ func (m *mockAmpExchange) HoldAuction(ctx context.Context, r exchange.AuctionReq return response, nil } +type mockAmpExchangeWarnings struct{} + +func (m *mockAmpExchangeWarnings) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { + response := &openrtb.BidResponse{ + SeatBid: []openrtb.SeatBid{{ + Bid: []openrtb.Bid{{ + AdM: "", + Ext: json.RawMessage(`{ "prebid": {"targeting": { "hb_pb": "1.20", "hb_appnexus_pb": "1.20", "hb_cache_id": "some_id"}}}`), + }}, + }}, + Ext: json.RawMessage(`{ "warnings": {"appnexus": [{"code": 10003, "message": "debug turned off for bidder"}] }}`), + } + return response, nil +} + func getTestBidRequest(nilUser bool, userExt *openrtb_ext.ExtUser, nilRegs bool, regsExt *openrtb_ext.ExtRegs) ([]byte, error) { var width uint64 = 300 var height uint64 = 300 @@ -1025,7 +1056,6 @@ func getTestBidRequest(nilUser bool, userExt *openrtb_ext.ExtUser, nilRegs bool, Ext: regsExtData, } } - return json.Marshal(bidRequest) } diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index e4567bbcd29..b5c217b90dc 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -139,6 +139,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http if errortypes.ContainsFatalError(errL) && writeError(errL, w, &labels) { return } + warnings := errortypes.WarningOnly(errL) ctx := context.Background() @@ -179,6 +180,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http RequestType: labels.RType, StartTime: start, LegacyLabels: labels, + Warnings: warnings, } response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil) @@ -363,8 +365,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error { if ccpaPolicy, err := ccpa.ReadFromRequest(req); err != nil { return append(errL, err) } else if _, err := ccpaPolicy.Parse(exchange.GetValidBidders(aliases)); err != nil { - if _, invalidConsent := err.(*errortypes.InvalidPrivacyConsent); invalidConsent { - errL = append(errL, &errortypes.InvalidPrivacyConsent{Message: fmt.Sprintf("CCPA consent is invalid and will be ignored. (%v)", err)}) + if _, invalidConsent := err.(*errortypes.Warning); invalidConsent { + errL = append(errL, &errortypes.Warning{ + Message: fmt.Sprintf("CCPA consent is invalid and will be ignored. (%v)", err), + WarningCode: errortypes.InvalidPrivacyConsentWarningCode}) consentWriter := ccpa.ConsentWriter{Consent: ""} if err := consentWriter.Write(req); err != nil { return append(errL, fmt.Errorf("Unable to remove invalid CCPA consent from the request. (%v)", err)) diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 768e60593a1..46c84c83033 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -1505,7 +1505,9 @@ func TestCCPAInvalid(t *testing.T) { errL := deps.validateRequest(&req) - expectedWarning := errortypes.InvalidPrivacyConsent{Message: "CCPA consent is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)"} + expectedWarning := errortypes.Warning{ + Message: "CCPA consent is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)", + WarningCode: errortypes.InvalidPrivacyConsentWarningCode} assert.ElementsMatch(t, errL, []error{&expectedWarning}) assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request") @@ -2104,6 +2106,55 @@ func TestIOS14EndToEnd(t *testing.T) { assert.Equal(t, &lmtOne, result.Device.Lmt) } +func TestAuctionWarnings(t *testing.T) { + reqBody := validRequest(t, "us-privacy-invalid.json") + deps := &endpointDeps{ + &warningsCheckExchange{}, + newParamsValidator(t), + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: int64(len(reqBody))}, + newTestMetrics(), + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BuildBidderMap(), + nil, + nil, + hardcodedResponseIPValidator{response: true}, + } + + req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) + recorder := httptest.NewRecorder() + + deps.Auction(recorder, req, nil) + + if recorder.Code != http.StatusOK { + t.Errorf("Endpoint should return a 200") + } + warnings := deps.ex.(*warningsCheckExchange).auctionRequest.Warnings + if !assert.Len(t, warnings, 1, "One warning should be returned from exchange") { + t.FailNow() + } + actualWarning := warnings[0].(*errortypes.Warning) + expectedMessage := "CCPA consent is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)" + assert.Equal(t, expectedMessage, actualWarning.Message, "Warning message is incorrect") + + assert.Equal(t, errortypes.InvalidPrivacyConsentWarningCode, actualWarning.WarningCode, "Warning code is incorrect") +} + +// warningsCheckExchange is a well-behaved exchange which stores all incoming warnings. +type warningsCheckExchange struct { + auctionRequest exchange.AuctionRequest +} + +func (e *warningsCheckExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { + e.auctionRequest = r + return nil, nil +} + // nobidExchange is a well-behaved exchange which always bids "no bid". type nobidExchange struct { gotRequest *openrtb.BidRequest diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/us-privacy-invalid.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/us-privacy-invalid.json new file mode 100644 index 00000000000..2ccdfb7ccdc --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/us-privacy-invalid.json @@ -0,0 +1,52 @@ +{ + "description": "Well formed amp request with invalid CCPA consent value", + "mockBidRequest": { + "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", + "site": { + "page": "prebid.org", + "publisher": { + "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" + } + }, + "source": { + "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5" + }, + "tmax": 1000, + "imp": [ + { + "id": "/19968336/header-bid-tag-0", + "ext": { + "appnexus": { + "placementId": 12883451 + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + } + } + ], + "regs": { + "ext": { + "us_privacy": "{invalid}" + } + }, + "user": { + "ext": {} + } + }, + "expectedBidResponse": { + "id":"b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 +} diff --git a/errortypes/code.go b/errortypes/code.go index 80a5eb45542..2749b978006 100644 --- a/errortypes/code.go +++ b/errortypes/code.go @@ -17,6 +17,8 @@ const ( const ( UnknownWarningCode = 10999 InvalidPrivacyConsentWarningCode = iota + 10000 + AccountLevelDebugDisabledWarningCode + BidderLevelDebugDisabledWarningCode ) // Coder provides an error or warning code with severity. diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go index c953f9b7e08..1fed2d7da6e 100644 --- a/errortypes/errortypes.go +++ b/errortypes/errortypes.go @@ -167,7 +167,8 @@ func (err *BidderTemporarilyDisabled) Severity() Severity { // Warning is a generic non-fatal error. type Warning struct { - Message string + Message string + WarningCode int } func (err *Warning) Error() string { @@ -175,26 +176,9 @@ func (err *Warning) Error() string { } func (err *Warning) Code() int { - return UnknownWarningCode + return err.WarningCode } func (err *Warning) Severity() Severity { return SeverityWarning } - -// InvalidPrivacyConsent is a warning for when the privacy consent string is invalid and is ignored. -type InvalidPrivacyConsent struct { - Message string -} - -func (err *InvalidPrivacyConsent) Error() string { - return err.Message -} - -func (err *InvalidPrivacyConsent) Code() int { - return InvalidPrivacyConsentWarningCode -} - -func (err *InvalidPrivacyConsent) Severity() Severity { - return SeverityWarning -} diff --git a/exchange/bidder.go b/exchange/bidder.go index 6df5fafb5f6..b0818dfdbe3 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -164,9 +164,17 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb.Bi // - debugContextKey (url param) in true // - account debug is allowed // - bidder debug is allowed - if accountDebugAllowed && bidder.config.DebugInfo.Allow { - if debugInfo := ctx.Value(DebugContextKey); debugInfo != nil && debugInfo.(bool) { - seatBid.httpCalls = append(seatBid.httpCalls, makeExt(httpInfo)) + if debugInfo := ctx.Value(DebugContextKey); debugInfo != nil && debugInfo.(bool) { + if accountDebugAllowed { + if bidder.config.DebugInfo.Allow { + seatBid.httpCalls = append(seatBid.httpCalls, makeExt(httpInfo)) + } else { + debugDisabledWarning := errortypes.Warning{ + WarningCode: errortypes.BidderLevelDebugDisabledWarningCode, + Message: "debug turned off for bidder", + } + errs = append(errs, &debugDisabledWarning) + } } } diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index dbf8a022255..e4eb7b9574e 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -20,6 +20,7 @@ import ( "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/currency" + "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/metrics" metricsConfig "github.com/prebid/prebid-server/metrics/config" "github.com/prebid/prebid-server/openrtb_ext" @@ -109,9 +110,13 @@ func TestSingleBidder(t *testing.T) { } // Make sure the returned values are what we expect - if len(errs) != 0 { + if len(errortypes.FatalOnly(errs)) != 0 { t.Errorf("bidder.Bid returned %d errors. Expected 0", len(errs)) } + + if !test.debugInfo.Allow && len(errortypes.WarningOnly(errs)) != 1 { + t.Errorf("bidder.Bid returned %d warnings. Expected 1", len(errs)) + } if len(seatBid.bids) != len(mockBidderResponse.Bids) { t.Fatalf("Expected %d bids. Got %d", len(mockBidderResponse.Bids), len(seatBid.bids)) } diff --git a/exchange/exchange.go b/exchange/exchange.go index 45081c16e71..d8ac60ba46a 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -66,7 +66,8 @@ type exchange struct { // Container to pass out response ext data from the GetAllBids goroutines back into the main thread type seatResponseExtra struct { ResponseTimeMillis int - Errors []openrtb_ext.ExtBidderError + Errors []openrtb_ext.ExtBidderMessage + Warnings []openrtb_ext.ExtBidderMessage // httpCalls is the list of debugging info. It should only be populated if the request.test == 1. // This will become response.ext.debug.httpcalls.{bidder} on the final Response. HttpCalls []*openrtb_ext.ExtHttpCall @@ -106,6 +107,7 @@ type AuctionRequest struct { UserSyncs IdFetcher RequestType metrics.RequestType StartTime time.Time + Warnings []error // LegacyLabels is included here for temporary compatability with cleanOpenRTBRequests // in HoldAuction until we get to factoring it away. Do not use for anything new. @@ -138,9 +140,9 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * debugLog = &DebugLog{Enabled: false} } - debugInfo := getDebugInfo(r.BidRequest, requestExt) + requestDebugInfo := getDebugInfo(r.BidRequest, requestExt) - debugInfo = debugInfo && r.Account.DebugAllow + debugInfo := requestDebugInfo && r.Account.DebugAllow debugLog.Enabled = debugLog.Enabled && r.Account.DebugAllow if debugInfo { @@ -236,6 +238,22 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * } } + if !r.Account.DebugAllow && requestDebugInfo { + accountDebugDisabledWarning := openrtb_ext.ExtBidderMessage{ + Code: errortypes.AccountLevelDebugDisabledWarningCode, + Message: "debug turned off for account", + } + bidResponseExt.Warnings[openrtb_ext.BidderReservedGeneral] = append(bidResponseExt.Warnings[openrtb_ext.BidderReservedGeneral], accountDebugDisabledWarning) + } + + for _, warning := range r.Warnings { + generalWarning := openrtb_ext.ExtBidderMessage{ + Code: errortypes.ReadCode(warning), + Message: warning.Error(), + } + bidResponseExt.Warnings[openrtb_ext.BidderReservedGeneral] = append(bidResponseExt.Warnings[openrtb_ext.BidderReservedGeneral], generalWarning) + } + // Build the response return e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, errs) } @@ -397,11 +415,11 @@ func (e *exchange) getAllBids( // Timing statistics e.me.RecordAdapterTime(bidderRequest.BidderLabels, time.Since(start)) - serr := errsToBidderErrors(err) bidderRequest.BidderLabels.AdapterBids = bidsToMetric(brw.adapterBids) bidderRequest.BidderLabels.AdapterErrors = errorsToMetric(err) // Append any bid validation errors to the error list - ae.Errors = serr + ae.Errors = errsToBidderErrors(err) + ae.Warnings = errsToBidderWarnings(err) brw.adapterExtra = ae if bids != nil { for _, bid := range bids.bids { @@ -494,13 +512,29 @@ func errorsToMetric(errs []error) map[metrics.AdapterError]struct{} { return ret } -func errsToBidderErrors(errs []error) []openrtb_ext.ExtBidderError { - serr := make([]openrtb_ext.ExtBidderError, len(errs)) - for i := 0; i < len(errs); i++ { - serr[i].Code = errortypes.ReadCode(errs[i]) - serr[i].Message = errs[i].Error() +func errsToBidderErrors(errs []error) []openrtb_ext.ExtBidderMessage { + sErr := make([]openrtb_ext.ExtBidderMessage, 0) + for _, err := range errortypes.FatalOnly(errs) { + newErr := openrtb_ext.ExtBidderMessage{ + Code: errortypes.ReadCode(err), + Message: err.Error(), + } + sErr = append(sErr, newErr) } - return serr + + return sErr +} + +func errsToBidderWarnings(errs []error) []openrtb_ext.ExtBidderMessage { + sWarn := make([]openrtb_ext.ExtBidderMessage, 0) + for _, warn := range errortypes.WarningOnly(errs) { + newErr := openrtb_ext.ExtBidderMessage{ + Code: errortypes.ReadCode(warn), + Message: warn.Error(), + } + sWarn = append(sWarn, newErr) + } + return sWarn } // This piece takes all the bids supplied by the adapters and crafts an openRTB response to send back to the requester @@ -749,7 +783,8 @@ func getPrimaryAdServer(adServerId int) (string, error) { func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, r AuctionRequest, debugInfo bool, errList []error) *openrtb_ext.ExtBidResponse { req := r.BidRequest bidResponseExt := &openrtb_ext.ExtBidResponse{ - Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError, len(adapterBids)), + Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage, len(adapterBids)), + Warnings: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage, len(adapterBids)), ResponseTimeMillis: make(map[openrtb_ext.BidderName]int, len(adapterBids)), RequestTimeoutMillis: req.TMax, } @@ -771,6 +806,9 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pb if debugInfo && len(responseExtra.HttpCalls) > 0 { bidResponseExt.Debug.HttpCalls[bidderName] = responseExtra.HttpCalls } + if len(responseExtra.Warnings) > 0 { + bidResponseExt.Warnings[bidderName] = responseExtra.Warnings + } // Only make an entry for bidder errors if the bidder reported any. if len(responseExtra.Errors) > 0 { bidResponseExt.Errors[bidderName] = responseExtra.Errors diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index f615d412c71..3eaa625f74f 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/prebid/prebid-server/errortypes" "io/ioutil" "net/http" "net/http/httptest" @@ -141,7 +142,7 @@ func TestCharacterEscape(t *testing.T) { adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, 1) adapterExtra["appnexus"] = &seatResponseExtra{ ResponseTimeMillis: 5, - Errors: []openrtb_ext.ExtBidderError{{Code: 999, Message: "Post ib.adnxs.com/openrtb2?query1&query2: unsupported protocol scheme \"\""}}, + Errors: []openrtb_ext.ExtBidderMessage{{Code: 999, Message: "Post ib.adnxs.com/openrtb2?query1&query2: unsupported protocol scheme \"\""}}, } var errList []error @@ -180,59 +181,68 @@ func TestDebugBehaviour(t *testing.T) { } type aTest struct { - desc string - in inTest - out outTest - debugData debugData + desc string + in inTest + out outTest + debugData debugData + generateWarnings bool } testCases := []aTest{ { - desc: "test flag equals zero, ext debug flag false, no debug info expected", - in: inTest{test: 0, debug: false}, - out: outTest{debugInfoIncluded: false}, - debugData: debugData{true, true}, + desc: "test flag equals zero, ext debug flag false, no debug info expected", + in: inTest{test: 0, debug: false}, + out: outTest{debugInfoIncluded: false}, + debugData: debugData{true, true}, + generateWarnings: false, }, { - desc: "test flag equals zero, ext debug flag true, debug info expected", - in: inTest{test: 0, debug: true}, - out: outTest{debugInfoIncluded: true}, - debugData: debugData{true, true}, + desc: "test flag equals zero, ext debug flag true, debug info expected", + in: inTest{test: 0, debug: true}, + out: outTest{debugInfoIncluded: true}, + debugData: debugData{true, true}, + generateWarnings: false, }, { - desc: "test flag equals 1, ext debug flag false, debug info expected", - in: inTest{test: 1, debug: false}, - out: outTest{debugInfoIncluded: true}, - debugData: debugData{true, true}, + desc: "test flag equals 1, ext debug flag false, debug info expected", + in: inTest{test: 1, debug: false}, + out: outTest{debugInfoIncluded: true}, + debugData: debugData{true, true}, + generateWarnings: false, }, { - desc: "test flag equals 1, ext debug flag true, debug info expected", - in: inTest{test: 1, debug: true}, - out: outTest{debugInfoIncluded: true}, - debugData: debugData{true, true}, + desc: "test flag equals 1, ext debug flag true, debug info expected", + in: inTest{test: 1, debug: true}, + out: outTest{debugInfoIncluded: true}, + debugData: debugData{true, true}, + generateWarnings: false, }, { - desc: "test flag not equal to 0 nor 1, ext debug flag false, no debug info expected", - in: inTest{test: 2, debug: false}, - out: outTest{debugInfoIncluded: false}, - debugData: debugData{true, true}, + desc: "test flag not equal to 0 nor 1, ext debug flag false, no debug info expected", + in: inTest{test: 2, debug: false}, + out: outTest{debugInfoIncluded: false}, + debugData: debugData{true, true}, + generateWarnings: false, }, { - desc: "test flag not equal to 0 nor 1, ext debug flag true, debug info expected", - in: inTest{test: -1, debug: true}, - out: outTest{debugInfoIncluded: true}, - debugData: debugData{true, true}, + desc: "test flag not equal to 0 nor 1, ext debug flag true, debug info expected", + in: inTest{test: -1, debug: true}, + out: outTest{debugInfoIncluded: true}, + debugData: debugData{true, true}, + generateWarnings: true, }, { - desc: "test account level debug disabled", - in: inTest{test: -1, debug: true}, - out: outTest{debugInfoIncluded: false}, - debugData: debugData{true, false}, + desc: "test account level debug disabled", + in: inTest{test: -1, debug: true}, + out: outTest{debugInfoIncluded: false}, + debugData: debugData{true, false}, + generateWarnings: true, }, { - desc: "test bidder level debug disabled", - in: inTest{test: -1, debug: true}, - out: outTest{debugInfoIncluded: false}, - debugData: debugData{false, true}, + desc: "test bidder level debug disabled", + in: inTest{test: -1, debug: true}, + out: outTest{debugInfoIncluded: false}, + debugData: debugData{false, true}, + generateWarnings: true, }, } @@ -243,9 +253,9 @@ func TestDebugBehaviour(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(noBidServer)) defer server.Close() - categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") - if error != nil { - t.Errorf("Failed to create a category Fetcher: %v", error) + categoriesFetcher, err := newCategoryFetcher("./test/category-mapping") + if err != nil { + t.Errorf("Failed to create a category Fetcher: %v", err) } bidRequest := &openrtb.BidRequest{ @@ -305,6 +315,13 @@ func TestDebugBehaviour(t *testing.T) { UserSyncs: &emptyUsersync{}, StartTime: time.Now(), } + if test.generateWarnings { + var errL []error + errL = append(errL, &errortypes.Warning{ + Message: fmt.Sprintf("CCPA consent test warning."), + WarningCode: errortypes.InvalidPrivacyConsentWarningCode}) + auctionRequest.Warnings = errL + } // Run test outBidResponse, err := e.HoldAuction(ctx, auctionRequest, nil) @@ -339,6 +356,35 @@ func TestDebugBehaviour(t *testing.T) { } else { assert.Nil(t, actualExt.Debug, "%s. ext.debug.httpcalls array should not be empty", "With bidder level debug disable option http calls should be empty") } + + if test.out.debugInfoIncluded && !test.debugData.accountLevelDebugAllowed { + assert.Len(t, actualExt.Warnings, 1, "warnings should have one warning") + assert.NotNil(t, actualExt.Warnings["general"], "general warning should be present") + assert.Equal(t, "debug turned off for account", actualExt.Warnings["general"][0].Message, "account debug disabled message should be present") + } + + if !test.out.debugInfoIncluded && test.in.debug && test.debugData.accountLevelDebugAllowed { + if test.generateWarnings { + assert.Len(t, actualExt.Warnings, 2, "warnings should have one warning") + } else { + assert.Len(t, actualExt.Warnings, 1, "warnings should have one warning") + } + assert.NotNil(t, actualExt.Warnings["appnexus"], "bidder warning should be present") + assert.Equal(t, "debug turned off for bidder", actualExt.Warnings["appnexus"][0].Message, "account debug disabled message should be present") + } + + if test.generateWarnings { + assert.NotNil(t, actualExt.Warnings["general"], "general warning should be present") + CCPAWarningPresent := false + for _, warn := range actualExt.Warnings["general"] { + if warn.Code == errortypes.InvalidPrivacyConsentWarningCode { + CCPAWarningPresent = true + break + } + } + assert.True(t, CCPAWarningPresent, "CCPA Warning should be present") + } + } } @@ -393,7 +439,7 @@ func TestTwoBiddersDebugDisabledAndEnabled(t *testing.T) { e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.categoriesFetcher = categoriesFetcher - debugLog := DebugLog{} + debugLog := DebugLog{Enabled: true} for _, testCase := range testCases { bidRequest := &openrtb.BidRequest{ @@ -762,7 +808,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) { adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{ bidderName: { ResponseTimeMillis: 5, - Errors: []openrtb_ext.ExtBidderError{ + Errors: []openrtb_ext.ExtBidderMessage{ { Code: 999, Message: "Post ib.adnxs.com/openrtb2?query1&query2: unsupported protocol scheme \"\"", diff --git a/exchange/utils_test.go b/exchange/utils_test.go index f089d70523b..1945aee1b98 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -637,7 +637,10 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) { description: "Invalid Consent", reqExt: json.RawMessage(`{"prebid":{"nosale":["*"]}}`), reqRegsExt: json.RawMessage(`{"us_privacy":"malformed"}`), - expectError: &errortypes.InvalidPrivacyConsent{"request.regs.ext.us_privacy must contain 4 characters"}, + expectError: &errortypes.Warning{ + Message: "request.regs.ext.us_privacy must contain 4 characters", + WarningCode: errortypes.InvalidPrivacyConsentWarningCode, + }, }, { description: "Invalid No Sale Bidders", diff --git a/openrtb_ext/response.go b/openrtb_ext/response.go index 02370d19376..d517704f44d 100644 --- a/openrtb_ext/response.go +++ b/openrtb_ext/response.go @@ -8,7 +8,8 @@ import ( type ExtBidResponse struct { Debug *ExtResponseDebug `json:"debug,omitempty"` // Errors defines the contract for bidresponse.ext.errors - Errors map[BidderName][]ExtBidderError `json:"errors,omitempty"` + Errors map[BidderName][]ExtBidderMessage `json:"errors,omitempty"` + Warnings map[BidderName][]ExtBidderMessage `json:"warnings,omitempty"` // ResponseTimeMillis defines the contract for bidresponse.ext.responsetimemillis ResponseTimeMillis map[BidderName]int `json:"responsetimemillis,omitempty"` // RequestTimeoutMillis returns the timeout used in the auction. @@ -47,8 +48,8 @@ type ExtUserSync struct { Type UserSyncType `json:"type"` } -// ExtBidderError defines an error object to be returned, consiting of a machine readable error code, and a human readable error message string. -type ExtBidderError struct { +// ExtBidderMessage defines an error object to be returned, consiting of a machine readable error code, and a human readable error message string. +type ExtBidderMessage struct { Code int `json:"code"` Message string `json:"message"` } diff --git a/privacy/ccpa/parsedpolicy.go b/privacy/ccpa/parsedpolicy.go index 3c934e67822..7b9c2d1fa7c 100644 --- a/privacy/ccpa/parsedpolicy.go +++ b/privacy/ccpa/parsedpolicy.go @@ -43,7 +43,10 @@ func (p Policy) Parse(validBidders map[string]struct{}) (ParsedPolicy, error) { consentOptOut, err := parseConsent(p.Consent) if err != nil { msg := fmt.Sprintf("request.regs.ext.us_privacy %s", err.Error()) - return ParsedPolicy{}, &errortypes.InvalidPrivacyConsent{Message: msg} + return ParsedPolicy{}, &errortypes.Warning{ + Message: msg, + WarningCode: errortypes.InvalidPrivacyConsentWarningCode, + } } noSaleForAllBidders, noSaleSpecificBidders, err := parseNoSaleBidders(p.NoSaleBidders, validBidders)