Repository: prebid/prebid-server
Pull request #2450 (2022-11-14 17:32:02)

Bid Response Validation
This PR addresses this issue https://github.com/prebid/prebid-server/issues/1288

The goal of this PR is to validate bids that will be apart of the bid response, and remove them if they are invalid.

The criteria for the bids are based on Creative Size Max's for Banner and Secure Markup for Banner and Video

Creative Size Max's refer to a `maxWidth` and `maxHeight` that can be set by the host, and we can check a `bid.W` and `bid.H` to ensure they are less than these max's

Secure Markup refers to the `bid.AdM` value, and ensuring it doesn't have certain strings present in the AdM

Bid Response Validation can be toggled by both the host and account, through the `Validations` struct, that has a `BannerCreativeMaxSize` and `SecureMarkup` variables, that can be set to the string values `enforce`, `warn`, and `skip`.

If either of these variables are set to enforce or warn, then we run a validation for that specific criteria, and if the bid is invalid, we add a message to the debug array, we log metrics, and if the setting was `enforce` we also then remove the bid from the response.

If the value is set to `skip`, then we skip the validation altogether

The bulk of the validation logic is found in `makeBid()` in `exchange.go`

This Validations struct is also where the host can define the `maxWidth` and `maxHeight` allowed.

An account can also utilize the `Validations` struct to set their own values for `BannerCreativeMaxSize` and `SecureMarkup`, and the values the account set take precedence over the host.

Here are the new metrics that were added for this Validation for both Prometheus and Go Metrics (they have slightly different names)

Adapter Metrics: `BidValidationCreativeSizeErrorMeter`, `BidValidationCreativeSizeWarnMeter`, `BidValidationSecureMarkupErrorMeter`, `BidValidationSecureMarkupWarnMeter`

Account Metrics which are disabled by default through `AccountAdapterDetails`: `bidValidationCreativeSizeMeter`, `bidValidationCreativeSizeWarnMeter`, `bidValidationSecureMarkupMeter`, `bidValidationSecureMarkupWarnMeter`

Tests were provided for all of this work 

AlexBVolcy: Thanks for the feedback Gus, I will be pushing a commit soon to address them. And in reference to re-naming variables, I want to keep the names consistent with Java, so I think they are fine to remain the same.
guscarreon: Hi @AlexBVolcy there are conflicts that must be resolved

Patch:
diff --git a/adapters/outbrain/outbrain.go b/adapters/outbrain/outbrain.go
index a01b535a..94cd1b6a 100644
--- a/adapters/outbrain/outbrain.go
+++ b/adapters/outbrain/outbrain.go
@@ -155,12 +155,14 @@ func getMediaTypeForImp(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType,
 				return openrtb_ext.BidTypeNative, nil
 			} else if imp.Banner != nil {
 				return openrtb_ext.BidTypeBanner, nil
+			} else if imp.Video != nil {
+				return openrtb_ext.BidTypeVideo, nil
 			}
 		}
 	}
 
 	return "", &errortypes.BadInput{
-		Message: fmt.Sprintf("Failed to find native/banner impression \"%s\" ", impID),
+		Message: fmt.Sprintf("Failed to find native/banner/video impression \"%s\" ", impID),
 	}
 }
 
diff --git a/amp/parse.go b/amp/parse.go
index bbbcee79..170e0659 100644
--- a/amp/parse.go
+++ b/amp/parse.go
@@ -31,6 +31,7 @@ type Params struct {
 	StoredRequestID   string
 	Targeting         string
 	Timeout           *uint64
+	Trace             string
 }
 
 // Size defines size information of an AMP request.
@@ -172,6 +173,7 @@ func ParseParams(httpRequest *http.Request) (Params, error) {
 		Slot:            query.Get("slot"),
 		StoredRequestID: tagID,
 		Targeting:       query.Get("targeting"),
+		Trace:           query.Get("trace"),
 	}
 	var err error
 	urlQueryGdprApplies := query.Get("gdpr_applies")
diff --git a/analytics/core.go b/analytics/core.go
index e2d5a071..f19d41ec 100644
--- a/analytics/core.go
+++ b/analytics/core.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/prebid/openrtb/v17/openrtb2"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/hooks/hookexecution"
 	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
@@ -27,23 +28,25 @@ type PBSAnalyticsModule interface {
 
 // Loggable object of a transaction at /openrtb2/auction endpoint
 type AuctionObject struct {
-	Status    int
-	Errors    []error
-	Request   *openrtb2.BidRequest
-	Response  *openrtb2.BidResponse
-	Account   *config.Account
-	StartTime time.Time
+	Status               int
+	Errors               []error
+	Request              *openrtb2.BidRequest
+	Response             *openrtb2.BidResponse
+	Account              *config.Account
+	StartTime            time.Time
+	HookExecutionOutcome []hookexecution.StageOutcome
 }
 
 // Loggable object of a transaction at /openrtb2/amp endpoint
 type AmpObject struct {
-	Status             int
-	Errors             []error
-	Request            *openrtb2.BidRequest
-	AuctionResponse    *openrtb2.BidResponse
-	AmpTargetingValues map[string]string
-	Origin             string
-	StartTime          time.Time
+	Status               int
+	Errors               []error
+	Request              *openrtb2.BidRequest
+	AuctionResponse      *openrtb2.BidResponse
+	AmpTargetingValues   map[string]string
+	Origin               string
+	StartTime            time.Time
+	HookExecutionOutcome []hookexecution.StageOutcome
 }
 
 // Loggable object of a transaction at /openrtb2/video endpoint
diff --git a/config/accounts.go b/config/accounts.go
index 9f52554e..7211a988 100644
--- a/config/accounts.go
+++ b/config/accounts.go
@@ -35,6 +35,7 @@ type Account struct {
 	TruncateTargetAttribute *int                                 `mapstructure:"truncate_target_attr" json:"truncate_target_attr"`
 	AlternateBidderCodes    *openrtb_ext.ExtAlternateBidderCodes `mapstructure:"alternatebiddercodes" json:"alternatebiddercodes"`
 	Hooks                   AccountHooks                         `mapstructure:"hooks" json:"hooks"`
+	Validations             Validations                          `mapstructure:"validations" json:"validations"`
 }
 
 // CookieSync represents the account-level defaults for the cookie sync endpoint.
diff --git a/config/config.go b/config/config.go
index 38def167..6630cdbc 100644
--- a/config/config.go
+++ b/config/config.go
@@ -100,7 +100,8 @@ type Configuration struct {
 	// Refers to main.go `configFileName` constant
 	BidderInfos BidderInfos `mapstructure:"adapters"`
 	// Hooks provides a way to specify hook execution plan for specific endpoints and stages
-	Hooks Hooks `mapstructure:"hooks"`
+	Hooks       Hooks       `mapstructure:"hooks"`
+	Validations Validations `mapstructure:"validations"`
 }
 
 const MIN_COOKIE_SIZE_BYTES = 500
@@ -632,6 +633,25 @@ type TimeoutNotification struct {
 	FailOnly bool `mapstructure:"fail_only"`
 }
 
+type Validations struct {
+	BannerCreativeMaxSize string `mapstructure:"banner_creative_max_size" json:"banner_creative_max_size"`
+	SecureMarkup          string `mapstructure:"secure_markup" json:"secure_markup"`
+	MaxCreativeWidth      int64  `mapstructure:"max_creative_width" json:"max_creative_width"`
+	MaxCreativeHeight     int64  `mapstructure:"max_creative_height" json:"max_creative_height"`
+}
+
+const (
+	ValidationEnforce string = "enforce"
+	ValidationWarn    string = "warn"
+	ValidationSkip    string = "skip"
+)
+
+func (host *Validations) SetBannerCreativeMaxSize(account Validations) {
+	if len(account.BannerCreativeMaxSize) > 0 {
+		host.BannerCreativeMaxSize = account.BannerCreativeMaxSize
+	}
+}
+
 func (cfg *TimeoutNotification) validate(errs []error) []error {
 	if cfg.SamplingRate < 0.0 || cfg.SamplingRate > 1.0 {
 		errs = append(errs, fmt.Errorf("debug.timeout_notification.sampling_rate must be positive and not greater than 1.0. Got %f", cfg.SamplingRate))
@@ -822,6 +842,10 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) {
 	v.SetDefault("host_cookie.ttl_days", 90)
 	v.SetDefault("host_cookie.max_cookie_size_bytes", 0)
 	v.SetDefault("host_schain_node", nil)
+	v.SetDefault("validations.banner_creative_max_size", ValidationSkip)
+	v.SetDefault("validations.secure_markup", ValidationSkip)
+	v.SetDefault("validations.max_creative_size.height", 0)
+	v.SetDefault("validations.max_creative_size.width", 0)
 	v.SetDefault("http_client.max_connections_per_host", 0) // unlimited
 	v.SetDefault("http_client.max_idle_connections", 400)
 	v.SetDefault("http_client.max_idle_connections_per_host", 10)
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index c2ad1832..7ee9f675 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -42,10 +42,12 @@ const defaultAmpRequestTimeoutMillis = 900
 var nilBody []byte = nil
 
 type AmpResponse struct {
-	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"`
+	Targeting map[string]string `json:"targeting"`
+	ORTB2     ORTB2             `json:"ortb2"`
+}
+
+type ORTB2 struct {
+	Ext openrtb_ext.ExtBidResponse `json:"ext"`
 }
 
 // NewAmpEndpoint modifies the OpenRTB endpoint to handle AMP requests. This will basically modify the parsing
@@ -147,13 +149,15 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 	w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver))
 
 	// There is no body for AMP requests, so we pass a nil body and ignore the return value.
-	if _, rejectErr := deps.hookExecutor.ExecuteEntrypointStage(r, nilBody); rejectErr != nil {
-		labels, ao = rejectAmpRequest(*rejectErr, w, deps.hookExecutor, nil, labels, ao, nil)
-		return
-	}
-
+	_, rejectErr := deps.hookExecutor.ExecuteEntrypointStage(r, nilBody)
 	reqWrapper, storedAuctionResponses, storedBidResponses, bidderImpReplaceImp, errL := deps.parseAmpRequest(r)
 	ao.Errors = append(ao.Errors, errL...)
+	// 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, deps.hookExecutor, reqWrapper, nil, labels, ao, nil)
+		return
+	}
 
 	if errortypes.ContainsFatalError(errL) {
 		w.WriteHeader(http.StatusBadRequest)
@@ -255,11 +259,11 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 	}
 
 	if isRejectErr {
-		labels, ao = rejectAmpRequest(*rejectErr, w, deps.hookExecutor, reqWrapper, labels, ao, errL)
+		labels, ao = rejectAmpRequest(*rejectErr, w, deps.hookExecutor, reqWrapper, account, labels, ao, errL)
 		return
 	}
 
-	labels, ao = sendAmpResponse(w, deps.hookExecutor, response, reqWrapper, labels, ao, errL)
+	labels, ao = sendAmpResponse(w, deps.hookExecutor, response, reqWrapper, account, labels, ao, errL)
 }
 
 func rejectAmpRequest(
@@ -267,6 +271,7 @@ func rejectAmpRequest(
 	w http.ResponseWriter,
 	hookExecutor hookexecution.HookStageExecutor,
 	reqWrapper *openrtb_ext.RequestWrapper,
+	account *config.Account,
 	labels metrics.Labels,
 	ao analytics.AmpObject,
 	errs []error,
@@ -275,7 +280,7 @@ func rejectAmpRequest(
 	ao.AuctionResponse = response
 	ao.Errors = append(ao.Errors, rejectErr)
 
-	return sendAmpResponse(w, hookExecutor, response, reqWrapper, labels, ao, errs)
+	return sendAmpResponse(w, hookExecutor, response, reqWrapper, account, labels, ao, errs)
 }
 
 func sendAmpResponse(
@@ -283,6 +288,7 @@ func sendAmpResponse(
 	hookExecutor hookexecution.HookStageExecutor,
 	response *openrtb2.BidResponse,
 	reqWrapper *openrtb_ext.RequestWrapper,
+	account *config.Account,
 	labels metrics.Labels,
 	ao analytics.AmpObject,
 	errs []error,
@@ -292,31 +298,62 @@ func sendAmpResponse(
 	// go in the AMP response
 	targets := map[string]string{}
 	byteCache := []byte("\"hb_cache_id")
-	for _, seatBids := range response.SeatBid {
-		for _, bid := range seatBids.Bid {
-			if bytes.Contains(bid.Ext, byteCache) {
-				// Looking for cache_id to be set, as this should only be set on winning bids (or
-				// deal bids), and AMP can only deliver cached ads in any case.
-				// Note, this could cause issues if a targeting key value starts with "hb_cache_id",
-				// but this is a very unlikely corner case. Doing this so we can catch "hb_cache_id"
-				// and "hb_cache_id_{deal}", which allows for deal support in AMP.
-				bidExt := &openrtb_ext.ExtBid{}
-				err := json.Unmarshal(bid.Ext, bidExt)
-				if err != nil {
-					w.WriteHeader(http.StatusInternalServerError)
-					fmt.Fprintf(w, "Critical error while unpacking AMP targets: %v", err)
-					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
-					return labels, ao
-				}
-				for key, value := range bidExt.Prebid.Targeting {
-					targets[key] = value
+	if response != nil {
+		for _, seatBids := range response.SeatBid {
+			for _, bid := range seatBids.Bid {
+				if bytes.Contains(bid.Ext, byteCache) {
+					// Looking for cache_id to be set, as this should only be set on winning bids (or
+					// deal bids), and AMP can only deliver cached ads in any case.
+					// Note, this could cause issues if a targeting key value starts with "hb_cache_id",
+					// but this is a very unlikely corner case. Doing this so we can catch "hb_cache_id"
+					// and "hb_cache_id_{deal}", which allows for deal support in AMP.
+					bidExt := &openrtb_ext.ExtBid{}
+					err := json.Unmarshal(bid.Ext, bidExt)
+					if err != nil {
+						w.WriteHeader(http.StatusInternalServerError)
+						fmt.Fprintf(w, "Critical error while unpacking AMP targets: %v", err)
+						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
+						return labels, ao
+					}
+					for key, value := range bidExt.Prebid.Targeting {
+						targets[key] = value
+					}
 				}
 			}
 		}
 	}
 
+	// Now JSONify the targets for the AMP response.
+	ampResponse := AmpResponse{Targeting: targets}
+	ao, ampResponse.ORTB2.Ext = getExtBidResponse(hookExecutor, response, reqWrapper, account, ao, errs)
+
+	ao.AmpTargetingValues = targets
+
+	// Fixes #231
+	enc := json.NewEncoder(w)
+	enc.SetEscapeHTML(false)
+
+	// If an error happens when encoding the response, there isn't much we can do.
+	// If we've sent _any_ bytes, then Go would have sent the 200 status code first.
+	// That status code can't be un-sent... so the best we can do is log the error.
+	if err := enc.Encode(ampResponse); err != nil {
+		labels.RequestStatus = metrics.RequestStatusNetworkErr
+		ao.Errors = append(ao.Errors, fmt.Errorf("/openrtb2/amp Failed to send response: %v", err))
+	}
+
+	return labels, ao
+}
+
+func getExtBidResponse(
+	hookExecutor hookexecution.HookStageExecutor,
+	response *openrtb2.BidResponse,
+	reqWrapper *openrtb_ext.RequestWrapper,
+	account *config.Account,
+	ao analytics.AmpObject,
+	errs []error,
+) (analytics.AmpObject, openrtb_ext.ExtBidResponse) {
 	// Extract any errors
 	var extResponse openrtb_ext.ExtBidResponse
 	eRErr := json.Unmarshal(response.Ext, &extResponse)
@@ -336,38 +373,39 @@ func sendAmpResponse(
 		warnings[openrtb_ext.BidderReservedGeneral] = append(warnings[openrtb_ext.BidderReservedGeneral], bidderErr)
 	}
 
-	// Now JSONify the targets for the AMP response.
-	ampResponse := AmpResponse{
-		Targeting: targets,
-		Errors:    extResponse.Errors,
-		Warnings:  warnings,
+	extBidResponse := openrtb_ext.ExtBidResponse{
+		Errors:   extResponse.Errors,
+		Warnings: warnings,
 	}
 
-	ao.AmpTargetingValues = targets
-
 	// add debug information if requested
-	if reqWrapper != nil && reqWrapper.Test == 1 && eRErr == nil {
-		if extResponse.Debug != nil {
-			ampResponse.Debug = extResponse.Debug
-		} else {
-			glog.Errorf("Test set on request but debug not present in response.")
-			ao.Errors = append(ao.Errors, fmt.Errorf("test set on request but debug not present in response"))
+	if reqWrapper != nil {
+		if reqWrapper.Test == 1 && eRErr == nil {
+			if extResponse.Debug != nil {
+				extBidResponse.Debug = extResponse.Debug
+			} else {
+				glog.Errorf("Test set on request but debug not present in response.")
+				ao.Errors = append(ao.Errors, fmt.Errorf("test set on request but debug not present in response"))
+			}
 		}
-	}
 
-	// Fixes #231
-	enc := json.NewEncoder(w)
-	enc.SetEscapeHTML(false)
+		stageOutcomes := hookExecutor.GetOutcomes()
+		ao.HookExecutionOutcome = stageOutcomes
+		modules, warns, err := hookexecution.GetModulesJSON(stageOutcomes, reqWrapper.BidRequest, account)
+		if err != nil {
+			err := fmt.Errorf("Failed to get modules outcome: %s", err)
+			glog.Errorf(err.Error())
+			ao.Errors = append(ao.Errors, err)
+		} else if modules != nil {
+			extBidResponse.Prebid = &openrtb_ext.ExtResponsePrebid{Modules: modules}
+		}
 
-	// If an error happens when encoding the response, there isn't much we can do.
-	// If we've sent _any_ bytes, then Go would have sent the 200 status code first.
-	// That status code can't be un-sent... so the best we can do is log the error.
-	if err := enc.Encode(ampResponse); err != nil {
-		labels.RequestStatus = metrics.RequestStatusNetworkErr
-		ao.Errors = append(ao.Errors, fmt.Errorf("/openrtb2/amp Failed to send response: %v", err))
+		if len(warns) > 0 {
+			ao.Errors = append(ao.Errors, warns...)
+		}
 	}
 
-	return labels, ao
+	return ao, extBidResponse
 }
 
 // parseRequest turns the HTTP request into an OpenRTB request.
@@ -534,6 +572,10 @@ func (deps *endpointDeps) overrideWithParams(ampParams amp.Params, req *openrtb2
 		return []error{err}
 	}
 
+	if err := setTrace(req, ampParams.Trace); err != nil {
+		return []error{err}
+	}
+
 	return nil
 }
 
@@ -715,3 +757,24 @@ func setEffectiveAmpPubID(req *openrtb2.BidRequest, account string) {
 		pub.ID = account
 	}
 }
+
+func setTrace(req *openrtb2.BidRequest, value string) (err error) {
+	if value == "" {
+		return nil
+	}
+
+	ext, err := json.Marshal(map[string]map[string]string{"prebid": {"trace": value}})
+	if err != nil {
+		return err
+	}
+
+	if len(req.Ext) > 0 {
+		ext, err = jsonpatch.MergePatch(req.Ext, ext)
+		if err != nil {
+			return err
+		}
+	}
+	req.Ext = ext
+
+	return nil
+}
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 50395567..6b8e875a 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -177,7 +177,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 	}
 
 	if rejectErr := hookexecution.FindFirstRejectOrNil(errL); rejectErr != nil {
-		labels, ao = rejectAuctionRequest(*rejectErr, w, deps.hookExecutor, req.BidRequest, labels, ao)
+		labels, ao = rejectAuctionRequest(*rejectErr, w, deps.hookExecutor, req.BidRequest, account, labels, ao)
 		return
 	}
 
@@ -244,11 +244,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, deps.hookExecutor, req.BidRequest, labels, ao)
+		labels, ao = rejectAuctionRequest(*rejectErr, w, deps.hookExecutor, req.BidRequest, account, labels, ao)
 		return
 	}
 
-	labels, ao = sendAuctionResponse(w, deps.hookExecutor, response, labels, ao)
+	labels, ao = sendAuctionResponse(w, deps.hookExecutor, response, req.BidRequest, account, labels, ao)
 }
 
 func rejectAuctionRequest(
@@ -256,6 +256,7 @@ func rejectAuctionRequest(
 	w http.ResponseWriter,
 	hookExecutor hookexecution.HookStageExecutor,
 	request *openrtb2.BidRequest,
+	account *config.Account,
 	labels metrics.Labels,
 	ao analytics.AuctionObject,
 ) (metrics.Labels, analytics.AuctionObject) {
@@ -267,17 +268,38 @@ func rejectAuctionRequest(
 	ao.Response = response
 	ao.Errors = append(ao.Errors, rejectErr)
 
-	return sendAuctionResponse(w, hookExecutor, response, labels, ao)
+	return sendAuctionResponse(w, hookExecutor, response, request, account, labels, ao)
 }
 
 func sendAuctionResponse(
 	w http.ResponseWriter,
 	hookExecutor hookexecution.HookStageExecutor,
 	response *openrtb2.BidResponse,
+	request *openrtb2.BidRequest,
+	account *config.Account,
 	labels metrics.Labels,
 	ao analytics.AuctionObject,
 ) (metrics.Labels, analytics.AuctionObject) {
 	hookExecutor.ExecuteAuctionResponseStage(response)
+
+	if response != nil {
+		stageOutcomes := hookExecutor.GetOutcomes()
+		ao.HookExecutionOutcome = stageOutcomes
+
+		ext, warns, err := hookexecution.EnrichExtBidResponse(response.Ext, stageOutcomes, request, account)
+		if err != nil {
+			err = fmt.Errorf("Failed to enrich Bid Response with hook debug information: %s", err)
+			glog.Errorf(err.Error())
+			ao.Errors = append(ao.Errors, err)
+		} else {
+			response.Ext = ext
+		}
+
+		if len(warns) > 0 {
+			ao.Errors = append(ao.Errors, warns...)
+		}
+	}
+
 	// Fixes #231
 	enc := json.NewEncoder(w)
 	enc.SetEscapeHTML(false)
diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/addtl-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/addtl-consent-through-query.json
index 7247e2df..d67df50e 100644
--- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/addtl-consent-through-query.json
+++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/addtl-consent-through-query.json
@@ -82,10 +82,17 @@
       "hb_pb": "15.00",
       "hb_pb_appnexus": "15.00"
     },
-    "warnings": {
-      "general": [
-        {"code": 10002, "message": "debug turned off for account"}
-      ]
+    "ortb2": {
+      "ext": {
+        "warnings": {
+          "general": [
+            {
+              "code": 10002,
+              "message": "debug turned off for account"
+            }
+          ]
+        }
+      }
     }
   },
   "expectedReturnCode": 200
diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json
index d420d19a..cbcd5ed7 100644
--- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json
+++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json
@@ -124,13 +124,17 @@
       "hb_pb": "15.00",
       "hb_pb_appnexus": "15.00"
     },
-    "warnings": {
-      "general": [
-        {
-          "code": 10002,
-          "message": "debug turned off for account"
+    "ortb2": {
+      "ext": {
+        "warnings": {
+          "general": [
+            {
+              "code": 10002,
+              "message": "debug turned off for account"
+            }
+          ]
         }
-      ]
+      }
     }
   },
   "expectedReturnCode": 200
diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json
index 2ff32e59..3a3baca9 100644
--- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json
+++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json
@@ -129,13 +129,17 @@
       "hb_pb": "15.00",
       "hb_pb_appnexus": "15.00"
     },
-    "warnings": {
-      "general": [
-        {
-          "code": 10002,
-          "message": "debug turned off for account"
+    "ortb2": {
+      "ext": {
+        "warnings": {
+          "general": [
+            {
+              "code": 10002,
+              "message": "debug turned off for account"
+            }
+          ]
         }
-      ]
+      }
     }
   },
   "expectedReturnCode": 200
diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json
index 2d2d8669..c81dff7a 100644
--- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json
+++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json
@@ -129,13 +129,17 @@
       "hb_pb": "15.00",
       "hb_pb_appnexus": "15.00"
     },
-    "warnings": {
-      "general": [
-        {
-          "code": 10002,
-          "message": "debug turned off for account"
+    "ortb2": {
+      "ext": {
+        "warnings": {
+          "general": [
+            {
+              "code": 10002,
+              "message": "debug turned off for account"
+            }
+          ]
         }
-      ]
+      }
     }
   },
   "expectedReturnCode": 200
diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json
index b1731327..7d3d9931 100644
--- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json
+++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json
@@ -129,13 +129,17 @@
       "hb_pb": "15.00",
       "hb_pb_appnexus": "15.00"
     },
-    "warnings": {
-      "general": [
-        {
-          "code": 10002,
-          "message": "debug turned off for account"
+    "ortb2": {
+      "ext": {
+        "warnings": {
+          "general": [
+            {
+              "code": 10002,
+              "message": "debug turned off for account"
+            }
+          ]
         }
-      ]
+      }
     }
   },
   "expectedReturnCode": 200
diff --git a/endpoints/openrtb2/sample-requests/amp/valid-supplementary/aliased-buyeruids.json b/endpoints/openrtb2/sample-requests/amp/valid-supplementary/aliased-buyeruids.json
index b74faf2d..3d6549fc 100644
--- a/endpoints/openrtb2/sample-requests/amp/valid-supplementary/aliased-buyeruids.json
+++ b/endpoints/openrtb2/sample-requests/amp/valid-supplementary/aliased-buyeruids.json
@@ -67,13 +67,17 @@
       "hb_pb": "2.00",
       "hb_pb_appnexus": "2.00"
     },
-    "warnings": {
-      "general": [
-        {
-          "code": 10002,
-          "message": "debug turned off for account"
+    "ortb2": {
+      "ext": {
+        "warnings": {
+          "general": [
+            {
+              "code": 10002,
+              "message": "debug turned off for account"
+            }
+          ]
         }
-      ]
+      }
     }
   },
   "expectedReturnCode": 200
diff --git a/endpoints/openrtb2/sample-requests/amp/valid-supplementary/aliases.json b/endpoints/openrtb2/sample-requests/amp/valid-supplementary/aliases.json
index 2ada07a1..aebb320a 100644
--- a/endpoints/openrtb2/sample-requests/amp/valid-supplementary/aliases.json
+++ b/endpoints/openrtb2/sample-requests/amp/valid-supplementary/aliases.json
@@ -58,13 +58,17 @@
       "hb_pb": "2.00",
       "hb_pb_unknown": "2.00"
     },
-    "warnings": {
-      "general": [
-        {
-          "code": 10002,
-          "message": "debug turned off for account"
+    "ortb2": {
+      "ext": {
+        "warnings": {
+          "general": [
+            {
+              "code": 10002,
+              "message": "debug turned off for account"
+            }
+          ]
         }
-      ]
+      }
     }
   },
   "expectedReturnCode": 200
diff --git a/endpoints/openrtb2/sample-requests/amp/valid-supplementary/gdpr-no-consentstring.json b/endpoints/openrtb2/sample-requests/amp/valid-supplementary/gdpr-no-consentstring.json
index 833dd6e7..dd961cf2 100644
--- a/endpoints/openrtb2/sample-requests/amp/valid-supplementary/gdpr-no-consentstring.json
+++ b/endpoints/openrtb2/sample-requests/amp/valid-supplementary/gdpr-no-consentstring.json
@@ -56,13 +56,17 @@
       "hb_pb": "3.00",
       "hb_pb_appnexus": "3.00"
     },
-    "warnings": {
-      "general": [
-        {
-          "code": 10002,
-          "message": "debug turned off for account"
+    "ortb2": {
+      "ext": {
+        "warnings": {
+          "general": [
+            {
+              "code": 10002,
+              "message": "debug turned off for account"
+            }
+          ]
         }
-      ]
+      }
     }
   },
   "expectedReturnCode": 200
diff --git a/endpoints/openrtb2/sample-requests/amp/valid-supplementary/gdpr.json b/endpoints/openrtb2/sample-requests/amp/valid-supplementary/gdpr.json
index f692e2bd..137ad150 100644
--- a/endpoints/openrtb2/sample-requests/amp/valid-supplementary/gdpr.json
+++ b/endpoints/openrtb2/sample-requests/amp/valid-supplementary/gdpr.json
@@ -61,13 +61,17 @@
       "hb_pb": "15.00",
       "hb_pb_appnexus": "15.00"
     },
-    "warnings": {
-      "general": [
-        {
-          "code": 10002,
-          "message": "debug turned off for account"
+    "ortb2": {
+      "ext": {
+        "warnings": {
+          "general": [
+            {
+              "code": 10002,
+              "message": "debug turned off for account"
+            }
+          ]
         }
-      ]
+      }
     }
   },
   "expectedReturnCode": 200
diff --git a/endpoints/openrtb2/sample-requests/amp/valid-supplementary/imp-with-stored-resp.json b/endpoints/openrtb2/sample-requests/amp/valid-supplementary/imp-with-stored-resp.json
index eadabcde..80ba523b 100644
--- a/endpoints/openrtb2/sample-requests/amp/valid-supplementary/imp-with-stored-resp.json
+++ b/endpoints/openrtb2/sample-requests/amp/valid-supplementary/imp-with-stored-resp.json
@@ -48,13 +48,17 @@
       "hb_pb": "5.00",
       "hb_pb_appnexus": "5.00"
     },
-    "warnings": {
-      "general": [
-        {
-          "code": 10002,
-          "message": "debug turned off for account"
+    "ortb2": {
+      "ext": {
+        "warnings": {
+          "general": [
+            {
+              "code": 10002,
+              "message": "debug turned off for account"
+            }
+          ]
         }
-      ]
+      }
     }
   },
   "expectedReturnCode": 200
diff --git a/endpoints/openrtb2/sample-requests/hooks/amp.json b/endpoints/openrtb2/sample-requests/hooks/amp.json
new file mode 100644
index 00000000..a865f293
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/hooks/amp.json
@@ -0,0 +1,146 @@
+{
+  "description": "Amp request",
+  "query": "tag_id=101&trace=verbose",
+  "config": {
+    "mockBidders": [
+      {"bidderName": "appnexus", "currency": "USD", "price": 2}
+    ]
+  },
+  "mockBidRequest": {
+    "id": "my-req-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "my-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "prebid": {
+            "bidder": {
+              "appnexus": {
+                "placementId": 12883451
+              }
+            }
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "debug": true,
+        "aliases": {
+          "unknown": "appnexus"
+        }
+      }
+    }
+  },
+  "expectedAmpResponse": {
+    "targeting": {
+      "hb_bidder": "appnexus",
+      "hb_bidder_appnexus": "appnexus",
+      "hb_cache_host": "www.pbcserver.com",
+      "hb_cache_host_appnex": "www.pbcserver.com",
+      "hb_cache_id": "0",
+      "hb_cache_id_appnexus": "0",
+      "hb_cache_path": "/pbcache/endpoint",
+      "hb_cache_path_appnex": "/pbcache/endpoint",
+      "hb_pb": "2.00",
+      "hb_pb_appnexus": "2.00"
+    },
+    "ortb2": {
+      "ext": {
+        "prebid": {
+          "modules": {
+            "errors": {
+              "foobar": {
+                "foo": [
+                  "error 1"
+                ],
+                "bar": [
+                  "error 1"
+                ]
+              }
+            },
+            "warnings": {
+              "foobar": {
+                "bar": [
+                  "warning 1"
+                ],
+                "baz": [
+                  "Endpoint /openrtb2/amp is not supported by hook."
+                ]
+              }
+            },
+            "trace": {
+              "stages": [
+                {
+                  "stage": "entrypoint",
+                  "outcomes": [
+                    {
+                      "entity": "http-request",
+                      "groups": [
+                        {
+                          "invocation_results": [
+                            {
+                              "analytics_tags": {},
+                              "hook_id": {
+                                "module_code": "foobar",
+                                "hook_impl_code": "foo"
+                              },
+                              "status": "success",
+                              "action": "update",
+                              "message": "",
+                              "debug_messages": [
+                                "Hook mutation successfully applied, affected key: header.foo, mutation type: update"
+                              ]
+                            },
+                            {
+                              "analytics_tags": {},
+                              "hook_id": {
+                                "module_code": "foobar",
+                                "hook_impl_code": "bar"
+                              },
+                              "status": "success",
+                              "action": "update",
+                              "message": "",
+                              "debug_messages": [
+                                "Hook mutation successfully applied, affected key: param.foo, mutation type: update"
+                              ]
+                            }
+                          ]
+                        },
+                        {
+                          "invocation_results": [
+                            {
+                              "analytics_tags": {},
+                              "hook_id": {
+                                "module_code": "foobar",
+                                "hook_impl_code": "baz"
+                              },
+                              "status": "success",
+                              "action": "no_action",
+                              "message": ""
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/hooks/amp_bidder_reject.json b/endpoints/openrtb2/sample-requests/hooks/amp_bidder_reject.json
new file mode 100644
index 00000000..c3ce226f
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/hooks/amp_bidder_reject.json
@@ -0,0 +1,135 @@
+{
+  "description": "Amp request",
+  "query": "tag_id=101&trace=verbose",
+  "config": {
+    "mockBidders": [
+      {"bidderName": "appnexus", "currency": "USD", "price": 2},
+      {"bidderName": "applogy", "currency": "USD", "price": 1}
+    ]
+  },
+  "mockBidRequest": {
+    "id": "my-req-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "my-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "prebid": {
+            "bidder": {
+              "appnexus": {
+                "placementId": 12883451
+              },
+              "applogy": {
+                "placementId": 12883451
+              }
+            }
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "debug": true,
+        "aliases": {
+          "unknown": "appnexus",
+          "foo": "applogy"
+        }
+      }
+    }
+  },
+  "expectedAmpResponse": {
+    "targeting": {
+      "hb_bidder": "applogy",
+      "hb_bidder_applogy": "applogy",
+      "hb_cache_host": "www.pbcserver.com",
+      "hb_cache_host_applog": "www.pbcserver.com",
+      "hb_cache_id": "0",
+      "hb_cache_id_applogy": "0",
+      "hb_cache_path": "/pbcache/endpoint",
+      "hb_cache_path_applog": "/pbcache/endpoint",
+      "hb_pb": "1.00",
+      "hb_pb_applogy": "1.00"
+    },
+    "ortb2": {
+      "ext": {
+        "warnings": {
+          "appnexus": [
+            {
+              "code": 11,
+              "message": "Module foobar (hook: foo) rejected request with code 123 at bidder_request stage"
+            }
+          ]
+        },
+        "prebid": {
+          "modules": {
+            "errors": {
+              "foobar": {
+                "foo": [
+                  "Module foobar (hook: foo) rejected request with code 123 at bidder_request stage"
+                ]
+              }
+            },
+            "trace": {
+              "stages": [
+                {
+                  "outcomes": [
+                    {
+                      "entity": "appnexus",
+                      "groups": [
+                        {
+                          "invocation_results": [
+                            {
+                              "action": "reject",
+                              "analytics_tags": {},
+                              "hook_id": {
+                                "hook_impl_code": "foo",
+                                "module_code": "foobar"
+                              },
+                              "message": "",
+                              "status": "success"
+                            }
+                          ]
+                        }
+                      ]
+                    },
+                    {
+                      "entity": "applogy",
+                      "groups": [
+                        {
+                          "invocation_results": [
+                            {
+                              "action": "no_action",
+                              "analytics_tags": {},
+                              "hook_id": {
+                                "hook_impl_code": "foo",
+                                "module_code": "foobar"
+                              },
+                              "message": "",
+                              "status": "success"
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ],
+                  "stage": "bidder_request"
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/hooks/amp_bidder_response_reject.json b/endpoints/openrtb2/sample-requests/hooks/amp_bidder_response_reject.json
new file mode 100644
index 00000000..fe654304
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/hooks/amp_bidder_response_reject.json
@@ -0,0 +1,135 @@
+{
+  "description": "Amp request",
+  "query": "tag_id=101&trace=verbose",
+  "config": {
+    "mockBidders": [
+      {"bidderName": "appnexus", "currency": "USD", "price": 2},
+      {"bidderName": "applogy", "currency": "USD", "price": 1}
+    ]
+  },
+  "mockBidRequest": {
+    "id": "my-req-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "my-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "prebid": {
+            "bidder": {
+              "appnexus": {
+                "placementId": 12883451
+              },
+              "applogy": {
+                "placementId": 12883451
+              }
+            }
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "debug": true,
+        "aliases": {
+          "unknown": "appnexus",
+          "foo": "applogy"
+        }
+      }
+    }
+  },
+  "expectedAmpResponse": {
+    "targeting": {
+      "hb_bidder": "applogy",
+      "hb_bidder_applogy": "applogy",
+      "hb_cache_host": "www.pbcserver.com",
+      "hb_cache_host_applog": "www.pbcserver.com",
+      "hb_cache_id": "0",
+      "hb_cache_id_applogy": "0",
+      "hb_cache_path": "/pbcache/endpoint",
+      "hb_cache_path_applog": "/pbcache/endpoint",
+      "hb_pb": "1.00",
+      "hb_pb_applogy": "1.00"
+    },
+    "ortb2": {
+      "ext": {
+        "warnings": {
+          "appnexus": [
+            {
+              "code": 11,
+              "message": "Module foobar (hook: foo) rejected request with code 123 at raw_bidder_response stage"
+            }
+          ]
+        },
+        "prebid": {
+          "modules": {
+            "errors": {
+              "foobar": {
+                "foo": [
+                  "Module foobar (hook: foo) rejected request with code 123 at raw_bidder_response stage"
+                ]
+              }
+            },
+            "trace": {
+              "stages": [
+                {
+                  "outcomes": [
+                    {
+                      "entity": "appnexus",
+                      "groups": [
+                        {
+                          "invocation_results": [
+                            {
+                              "action": "reject",
+                              "analytics_tags": {},
+                              "hook_id": {
+                                "hook_impl_code": "foo",
+                                "module_code": "foobar"
+                              },
+                              "message": "",
+                              "status": "success"
+                            }
+                          ]
+                        }
+                      ]
+                    },
+                    {
+                      "entity": "applogy",
+                      "groups": [
+                        {
+                          "invocation_results": [
+                            {
+                              "action": "no_action",
+                              "analytics_tags": {},
+                              "hook_id": {
+                                "hook_impl_code": "foo",
+                                "module_code": "foobar"
+                              },
+                              "message": "",
+                              "status": "success"
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ],
+                  "stage": "raw_bidder_response"
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/hooks/amp_entrypoint_reject.json b/endpoints/openrtb2/sample-requests/hooks/amp_entrypoint_reject.json
new file mode 100644
index 00000000..6960cd7b
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/hooks/amp_entrypoint_reject.json
@@ -0,0 +1,85 @@
+{
+  "description": "Amp request rejected",
+  "query": "tag_id=101",
+  "config": {
+    "mockBidders": [
+      {"bidderName": "appnexus", "currency": "USD", "price": 2}
+    ]
+  },
+  "mockBidRequest": {
+    "id": "my-req-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "my-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "prebid": {
+            "bidder": {
+              "appnexus": {
+                "placementId": 12883451
+              }
+            }
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "trace": "verbose",
+        "aliases": {
+          "unknown": "appnexus"
+        }
+      }
+    }
+  },
+  "expectedAmpResponse": {
+    "targeting": {},
+    "ortb2": {
+      "ext": {
+        "prebid": {
+          "modules": {
+            "trace": {
+              "stages": [
+                {
+                  "stage": "entrypoint",
+                  "outcomes": [
+                    {
+                      "entity": "http-request",
+                      "groups": [
+                        {
+                          "invocation_results": [
+                            {
+                              "analytics_tags": {},
+                              "hook_id": {
+                                "module_code": "foobar",
+                                "hook_impl_code": "foo"
+                              },
+                              "status": "success",
+                              "action": "reject",
+                              "message": ""
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/hooks/amp_processed_auction_request_reject.json b/endpoints/openrtb2/sample-requests/hooks/amp_processed_auction_request_reject.json
new file mode 100644
index 00000000..3e78f02e
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/hooks/amp_processed_auction_request_reject.json
@@ -0,0 +1,85 @@
+{
+  "description": "Amp request rejected",
+  "query": "tag_id=101",
+  "config": {
+    "mockBidders": [
+      {"bidderName": "appnexus", "currency": "USD", "price": 2}
+    ]
+  },
+  "mockBidRequest": {
+    "id": "my-req-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "my-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "prebid": {
+            "bidder": {
+              "appnexus": {
+                "placementId": 12883451
+              }
+            }
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "trace": "verbose",
+        "aliases": {
+          "unknown": "appnexus"
+        }
+      }
+    }
+  },
+  "expectedAmpResponse": {
+    "targeting": {},
+    "ortb2": {
+      "ext": {
+        "prebid": {
+          "modules": {
+            "trace": {
+              "stages": [
+                {
+                  "stage": "processed_auction_request",
+                  "outcomes": [
+                    {
+                      "entity": "auction-request",
+                      "groups": [
+                        {
+                          "invocation_results": [
+                            {
+                              "analytics_tags": {},
+                              "hook_id": {
+                                "module_code": "foobar",
+                                "hook_impl_code": "foo"
+                              },
+                              "status": "success",
+                              "action": "reject",
+                              "message": ""
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/hooks/auction.json b/endpoints/openrtb2/sample-requests/hooks/auction.json
new file mode 100644
index 00000000..a2dc92c2
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/hooks/auction.json
@@ -0,0 +1,168 @@
+{
+  "description": "Auction request",
+  "config": {
+    "mockBidders": [
+      {"bidderName": "appnexus", "currency": "USD", "price": 0.00}
+    ]
+  },
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "tmax": 500,
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "trace": "verbose"
+      }
+    }
+  },
+  "expectedBidResponse": {
+    "id": "some-request-id",
+    "bidid": "test bid id",
+    "cur":"USD",
+    "nbr":null,
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "some-impression-id",
+            "price": 0,
+            "ext": {"prebid":{"type":"banner"}}
+          }
+        ],
+        "seat": "appnexus"
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "modules": {
+          "errors": {
+            "foobar": {
+              "foo": [
+                "error 1"
+              ],
+              "bar": [
+                "error 1"
+              ]
+            }
+          },
+          "warnings": {
+            "foobar": {
+              "bar": [
+                "warning 1"
+              ]
+            }
+          },
+          "trace": {
+            "stages": [
+              {
+                "stage": "entrypoint",
+                "outcomes": [
+                  {
+                    "entity": "http-request",
+                    "groups": [
+                      {
+                        "invocation_results": [
+                          {
+                            "analytics_tags": {},
+                            "hook_id": {
+                              "module_code": "foobar",
+                              "hook_impl_code": "foo"
+                            },
+                            "status": "success",
+                            "action": "update",
+                            "message": "",
+                            "debug_messages": [
+                              "Hook mutation successfully applied, affected key: header.foo, mutation type: update"
+                            ]
+                          },
+                          {
+                            "analytics_tags": {},
+                            "hook_id": {
+                              "module_code": "foobar",
+                              "hook_impl_code": "bar"
+                            },
+                            "status": "success",
+                            "action": "update",
+                            "message": "",
+                            "debug_messages": [
+                              "Hook mutation successfully applied, affected key: param.foo, mutation type: update"
+                            ]
+                          }
+                        ]
+                      },
+                      {
+                        "invocation_results": [
+                          {
+                            "analytics_tags": {},
+                            "hook_id": {
+                              "module_code": "foobar",
+                              "hook_impl_code": "baz"
+                            },
+                            "status": "success",
+                            "action": "update",
+                            "message": "",
+                            "debug_messages": [
+                              "Hook mutation successfully applied, affected key: body.tmax, mutation type: update",
+                              "Hook mutation successfully applied, affected key: body.regs.ext.us_privacy, mutation type: add"
+                            ]
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "stage": "raw_auction_request",
+                "outcomes": [
+                  {
+                    "entity": "auction-request",
+                    "groups": [
+                      {
+                        "invocation_results": [
+                          {
+                            "analytics_tags": {},
+                            "hook_id": {
+                              "module_code": "vendor.module",
+                              "hook_impl_code": "foobar"
+                            },
+                            "status": "success",
+                            "action": "no_action",
+                            "message": ""
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
diff --git a/endpoints/openrtb2/sample-requests/hooks/auction_bidder_reject.json b/endpoints/openrtb2/sample-requests/hooks/auction_bidder_reject.json
new file mode 100644
index 00000000..dd209763
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/hooks/auction_bidder_reject.json
@@ -0,0 +1,131 @@
+{
+  "description": "Auction request rejected at bidder-level stage",
+  "config": {
+    "mockBidders": [
+      {"bidderName": "appnexus", "currency": "USD", "price": 0.00},
+      {"bidderName": "applogy", "currency": "USD", "price": 0.00}
+    ]
+  },
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          },
+          "applogy": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "tmax": 50,
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "trace": "verbose"
+      }
+    }
+  },
+  "expectedBidResponse": {
+    "id":"some-request-id",
+    "bidid":"test bid id",
+    "cur":"USD",
+    "nbr":null,
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "applogy-bid",
+            "impid": "some-impression-id",
+            "price": 0,
+            "ext": {"prebid":{"type":"banner"}}
+          }
+        ],
+        "seat": "applogy"
+      }
+    ],
+    "ext": {
+      "warnings": {
+        "appnexus": [
+          {
+            "code": 11,
+            "message": "Module foobar (hook: foo) rejected request with code 123 at bidder_request stage"
+          }
+        ]
+      },
+      "prebid": {
+        "modules": {
+          "errors": {
+            "foobar": {
+              "foo": [
+                "Module foobar (hook: foo) rejected request with code 123 at bidder_request stage"
+              ]
+            }
+          },
+          "trace": {
+            "stages": [
+              {
+                "outcomes": [
+                  {
+                    "entity": "appnexus",
+                    "groups": [
+                      {
+                        "invocation_results": [
+                          {
+                            "action": "reject",
+                            "analytics_tags": {},
+                            "hook_id": {
+                              "hook_impl_code": "foo",
+                              "module_code": "foobar"
+                            },
+                            "message": "",
+                            "status": "success"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "entity": "applogy",
+                    "groups": [
+                      {
+                        "invocation_results": [
+                          {
+                            "action": "no_action",
+                            "analytics_tags": {},
+                            "hook_id": {
+                              "hook_impl_code": "foo",
+                              "module_code": "foobar"
+                            },
+                            "message": "",
+                            "status": "success"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ],
+                "stage": "bidder_request"
+              }
+            ]
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
diff --git a/endpoints/openrtb2/sample-requests/hooks/auction_bidder_response_reject.json b/endpoints/openrtb2/sample-requests/hooks/auction_bidder_response_reject.json
new file mode 100644
index 00000000..ba5c75f0
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/hooks/auction_bidder_response_reject.json
@@ -0,0 +1,131 @@
+{
+  "description": "Auction request rejected at bidder-level stage",
+  "config": {
+    "mockBidders": [
+      {"bidderName": "appnexus", "currency": "USD", "price": 0.00},
+      {"bidderName": "applogy", "currency": "USD", "price": 0.00}
+    ]
+  },
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          },
+          "applogy": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "tmax": 50,
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "trace": "verbose"
+      }
+    }
+  },
+  "expectedBidResponse": {
+    "id":"some-request-id",
+    "bidid":"test bid id",
+    "cur":"USD",
+    "nbr":null,
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "applogy-bid",
+            "impid": "some-impression-id",
+            "price": 0,
+            "ext": {"prebid":{"type":"banner"}}
+          }
+        ],
+        "seat": "applogy"
+      }
+    ],
+    "ext": {
+      "warnings": {
+        "appnexus": [
+          {
+            "code": 11,
+            "message": "Module foobar (hook: foo) rejected request with code 123 at raw_bidder_response stage"
+          }
+        ]
+      },
+      "prebid": {
+        "modules": {
+          "errors": {
+            "foobar": {
+              "foo": [
+                "Module foobar (hook: foo) rejected request with code 123 at raw_bidder_response stage"
+              ]
+            }
+          },
+          "trace": {
+            "stages": [
+              {
+                "outcomes": [
+                  {
+                    "entity": "appnexus",
+                    "groups": [
+                      {
+                        "invocation_results": [
+                          {
+                            "action": "reject",
+                            "analytics_tags": {},
+                            "hook_id": {
+                              "hook_impl_code": "foo",
+                              "module_code": "foobar"
+                            },
+                            "message": "",
+                            "status": "success"
+                          }
+                        ]
+                      }
+                    ]
+                  },
+                  {
+                    "entity": "applogy",
+                    "groups": [
+                      {
+                        "invocation_results": [
+                          {
+                            "action": "no_action",
+                            "analytics_tags": {},
+                            "hook_id": {
+                              "hook_impl_code": "foo",
+                              "module_code": "foobar"
+                            },
+                            "message": "",
+                            "status": "success"
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ],
+                "stage": "raw_bidder_response"
+              }
+            ]
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
diff --git a/endpoints/openrtb2/sample-requests/hooks/auction_entrypoint_reject.json b/endpoints/openrtb2/sample-requests/hooks/auction_entrypoint_reject.json
new file mode 100644
index 00000000..c67b8d4b
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/hooks/auction_entrypoint_reject.json
@@ -0,0 +1,77 @@
+{
+  "description": "Auction request rejected",
+  "config": {
+    "mockBidders": [
+      {"bidderName": "appnexus", "currency": "USD", "price": 0.00}
+    ]
+  },
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "tmax": 50,
+    "ext": {
+      "prebid": {
+        "trace": "verbose"
+      }
+    }
+  },
+  "expectedBidResponse": {
+    "id": "some-request-id",
+    "nbr": 123,
+    "ext": {
+      "prebid": {
+        "modules": {
+          "trace": {
+            "stages": [
+              {
+                "stage": "entrypoint",
+                "outcomes": [
+                  {
+                    "entity": "http-request",
+                    "groups": [
+                      {
+                        "invocation_results": [
+                          {
+                            "analytics_tags": {},
+                            "hook_id": {
+                              "module_code": "foobar",
+                              "hook_impl_code": "foo"
+                            },
+                            "status": "success",
+                            "action": "reject",
+                            "message": ""
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
diff --git a/endpoints/openrtb2/sample-requests/hooks/auction_processed_auction_request_reject.json b/endpoints/openrtb2/sample-requests/hooks/auction_processed_auction_request_reject.json
new file mode 100644
index 00000000..eca136cf
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/hooks/auction_processed_auction_request_reject.json
@@ -0,0 +1,77 @@
+{
+  "description": "Auction request rejected",
+  "config": {
+    "mockBidders": [
+      {"bidderName": "appnexus", "currency": "USD", "price": 0.00}
+    ]
+  },
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "tmax": 50,
+    "ext": {
+      "prebid": {
+        "trace": "verbose"
+      }
+    }
+  },
+  "expectedBidResponse": {
+    "id": "some-request-id",
+    "nbr": 123,
+    "ext": {
+      "prebid": {
+        "modules": {
+          "trace": {
+            "stages": [
+              {
+                "stage": "processed_auction_request",
+                "outcomes": [
+                  {
+                    "entity": "auction-request",
+                    "groups": [
+                      {
+                        "invocation_results": [
+                          {
+                            "analytics_tags": {},
+                            "hook_id": {
+                              "module_code": "foobar",
+                              "hook_impl_code": "foo"
+                            },
+                            "status": "success",
+                            "action": "reject",
+                            "message": ""
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
diff --git a/endpoints/openrtb2/sample-requests/hooks/auction_raw_auction_request_reject.json b/endpoints/openrtb2/sample-requests/hooks/auction_raw_auction_request_reject.json
new file mode 100644
index 00000000..dc192baf
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/hooks/auction_raw_auction_request_reject.json
@@ -0,0 +1,77 @@
+{
+  "description": "Auction request rejected",
+  "config": {
+    "mockBidders": [
+      {"bidderName": "appnexus", "currency": "USD", "price": 0.00}
+    ]
+  },
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "tmax": 50,
+    "ext": {
+      "prebid": {
+        "trace": "verbose"
+      }
+    }
+  },
+  "expectedBidResponse": {
+    "id": "some-request-id",
+    "nbr": 123,
+    "ext": {
+      "prebid": {
+        "modules": {
+          "trace": {
+            "stages": [
+              {
+                "stage": "raw_auction_request",
+                "outcomes": [
+                  {
+                    "entity": "auction-request",
+                    "groups": [
+                      {
+                        "invocation_results": [
+                          {
+                            "analytics_tags": {},
+                            "hook_id": {
+                              "module_code": "foobar",
+                              "hook_impl_code": "foo"
+                            },
+                            "status": "success",
+                            "action": "reject",
+                            "message": ""
+                          }
+                        ]
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/hooks/auction_bidder_reject.json b/endpoints/openrtb2/sample-requests/valid-whole/hooks/auction_bidder_reject.json
deleted file mode 100644
index 29d333ca..00000000
--- a/endpoints/openrtb2/sample-requests/valid-whole/hooks/auction_bidder_reject.json
+++ /dev/null
@@ -1,78 +0,0 @@
-{
-  "description": "Simple request",
-  "config": {
-    "mockBidders": [
-      {"bidderName": "appnexus", "currency": "USD", "price": 0.00},
-      {"bidderName": "applogy", "currency": "USD", "price": 0.00}
-    ]
-  },
-  "mockBidRequest": {
-    "id": "some-request-id",
-    "site": {
-      "page": "prebid.org"
-    },
-    "imp": [
-      {
-        "id": "some-impression-id",
-        "banner": {
-          "format": [
-            {
-              "w": 300,
-              "h": 250
-            },
-            {
-              "w": 300,
-              "h": 600
-            }
-          ]
-        },
-        "ext": {
-          "appnexus": {
-            "placementId": 12883451
-          },
-          "applogy": {
-            "placementId": 12883451
-          }
-        }
-      }
-    ],
-    "tmax": 50,
-    "ext": {}
-  },
-  "expectedBidResponse": {
-    "id":"some-request-id",
-    "bidid":"test bid id",
-    "cur":"USD",
-    "nbr":null,
-    "seatbid": [
-      {
-        "bid": [
-          {
-            "id": "applogy-bid",
-            "impid": "some-impression-id",
-            "price": 0,
-            "ext": {"prebid":{"type":"banner"}}
-          }
-        ],
-        "seat": "applogy"
-      }
-    ],
-    "ext": {
-      "warnings": {
-        "appnexus": [
-          {
-            "code": 11,
-            "message": "Module foobar (hook: foo) rejected request with code 123 at bidder_request stage"
-          }
-        ],
-        "general": [
-          {
-            "code": 10002,
-            "message": "debug turned off for account"
-          }
-        ]
-      }
-    }
-  },
-  "expectedReturnCode": 200
-}
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/hooks/auction_bidder_response_reject.json b/endpoints/openrtb2/sample-requests/valid-whole/hooks/auction_bidder_response_reject.json
deleted file mode 100644
index 4ea7171c..00000000
--- a/endpoints/openrtb2/sample-requests/valid-whole/hooks/auction_bidder_response_reject.json
+++ /dev/null
@@ -1,78 +0,0 @@
-{
-  "description": "Simple request",
-  "config": {
-    "mockBidders": [
-      {"bidderName": "appnexus", "currency": "USD", "price": 0.00},
-      {"bidderName": "applogy", "currency": "USD", "price": 0.00}
-    ]
-  },
-  "mockBidRequest": {
-    "id": "some-request-id",
-    "site": {
-      "page": "prebid.org"
-    },
-    "imp": [
-      {
-        "id": "some-impression-id",
-        "banner": {
-          "format": [
-            {
-              "w": 300,
-              "h": 250
-            },
-            {
-              "w": 300,
-              "h": 600
-            }
-          ]
-        },
-        "ext": {
-          "appnexus": {
-            "placementId": 12883451
-          },
-          "applogy": {
-            "placementId": 12883451
-          }
-        }
-      }
-    ],
-    "tmax": 50,
-    "ext": {}
-  },
-  "expectedBidResponse": {
-    "id":"some-request-id",
-    "bidid":"test bid id",
-    "cur":"USD",
-    "nbr":null,
-    "seatbid": [
-      {
-        "bid": [
-          {
-            "id": "applogy-bid",
-            "impid": "some-impression-id",
-            "price": 0,
-            "ext": {"prebid":{"type":"banner"}}
-          }
-        ],
-        "seat": "applogy"
-      }
-    ],
-    "ext": {
-      "warnings": {
-        "appnexus": [
-          {
-            "code": 11,
-            "message": "Module foobar (hook: foo) rejected request with code 123 at raw_bidder_response stage"
-          }
-        ],
-        "general": [
-          {
-            "code": 10002,
-            "message": "debug turned off for account"
-          }
-        ]
-      }
-    }
-  },
-  "expectedReturnCode": 200
-}
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/hooks/auction_reject.json b/endpoints/openrtb2/sample-requests/valid-whole/hooks/auction_reject.json
deleted file mode 100644
index 9b77ce56..00000000
--- a/endpoints/openrtb2/sample-requests/valid-whole/hooks/auction_reject.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
-  "description": "Simple request",
-  "config": {
-    "mockBidders": [
-      {"bidderName": "appnexus", "currency": "USD", "price": 0.00}
-    ]
-  },
-  "mockBidRequest": {
-    "id": "some-request-id",
-    "site": {
-      "page": "prebid.org"
-    },
-    "imp": [
-      {
-        "id": "some-impression-id",
-        "banner": {
-          "format": [
-            {
-              "w": 300,
-              "h": 250
-            },
-            {
-              "w": 300,
-              "h": 600
-            }
-          ]
-        },
-        "ext": {
-          "appnexus": {
-            "placementId": 12883451
-          }
-        }
-      }
-    ],
-    "tmax": 50,
-    "ext": {}
-  },
-  "expectedBidResponse": {
-    "id": "some-request-id",
-    "nbr": 123
-  },
-  "expectedReturnCode": 200
-}
diff --git a/exchange/exchange.go b/exchange/exchange.go
index d7737a42..9686fc48 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -55,23 +55,24 @@ type IdFetcher interface {
 }
 
 type exchange struct {
-	adapterMap        map[openrtb_ext.BidderName]AdaptedBidder
-	bidderInfo        config.BidderInfos
-	bidderToSyncerKey map[string]string
-	me                metrics.MetricsEngine
-	cache             prebid_cache_client.Client
-	cacheTime         time.Duration
-	gdprPermsBuilder  gdpr.PermissionsBuilder
-	tcf2ConfigBuilder gdpr.TCF2ConfigBuilder
-	currencyConverter *currency.RateConverter
-	externalURL       string
-	gdprDefaultValue  gdpr.Signal
-	privacyConfig     config.Privacy
-	categoriesFetcher stored_requests.CategoryFetcher
-	bidIDGenerator    BidIDGenerator
-	hostSChainNode    *openrtb2.SupplyChainNode
-	adsCertSigner     adscert.Signer
-	server            config.Server
+	adapterMap               map[openrtb_ext.BidderName]AdaptedBidder
+	bidderInfo               config.BidderInfos
+	bidderToSyncerKey        map[string]string
+	me                       metrics.MetricsEngine
+	cache                    prebid_cache_client.Client
+	cacheTime                time.Duration
+	gdprPermsBuilder         gdpr.PermissionsBuilder
+	tcf2ConfigBuilder        gdpr.TCF2ConfigBuilder
+	currencyConverter        *currency.RateConverter
+	externalURL              string
+	gdprDefaultValue         gdpr.Signal
+	privacyConfig            config.Privacy
+	categoriesFetcher        stored_requests.CategoryFetcher
+	bidIDGenerator           BidIDGenerator
+	hostSChainNode           *openrtb2.SupplyChainNode
+	adsCertSigner            adscert.Signer
+	server                   config.Server
+	bidValidationEnforcement config.Validations
 }
 
 // Container to pass out response ext data from the GetAllBids goroutines back into the main thread
@@ -147,10 +148,11 @@ func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid
 			GDPR: cfg.GDPR,
 			LMT:  cfg.LMT,
 		},
-		bidIDGenerator: &bidIDGenerator{cfg.GenerateBidID},
-		hostSChainNode: cfg.HostSChainNode,
-		adsCertSigner:  adsCertSigner,
-		server:         config.Server{ExternalUrl: cfg.ExternalURL, GvlID: cfg.GDPR.HostVendorID, DataCenter: cfg.DataCenter},
+		bidIDGenerator:           &bidIDGenerator{cfg.GenerateBidID},
+		hostSChainNode:           cfg.HostSChainNode,
+		adsCertSigner:            adsCertSigner,
+		server:                   config.Server{ExternalUrl: cfg.ExternalURL, GvlID: cfg.GDPR.HostVendorID, DataCenter: cfg.DataCenter},
+		bidValidationEnforcement: cfg.Validations,
 	}
 }
 
@@ -404,9 +406,10 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 		bidResponseExt.Warnings[openrtb_ext.BidderReservedGeneral] = append(bidResponseExt.Warnings[openrtb_ext.BidderReservedGeneral], generalWarning)
 	}
 
-	// Build the response
-	bidResponse, err := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, errs)
+	e.bidValidationEnforcement.SetBannerCreativeMaxSize(r.Account.Validations)
 
+	// Build the response
+	bidResponse, err := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs)
 	return bidResponse, err
 }
 
@@ -706,7 +709,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 *openrtb2.BidRequest, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, errList []error) (*openrtb2.BidResponse, error) {
+func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb2.BidRequest, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error) (*openrtb2.BidResponse, error) {
 	bidResponse := new(openrtb2.BidResponse)
 	var err error
 
@@ -722,7 +725,7 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_
 	for a, adapterSeatBids := range adapterSeatBids {
 		//while processing every single bib, do we need to handle categories here?
 		if adapterSeatBids != nil && len(adapterSeatBids.Bids) > 0 {
-			sb := e.makeSeatBid(adapterSeatBids, a, adapterExtra, auc, returnCreative, impExtInfoMap)
+			sb := e.makeSeatBid(adapterSeatBids, a, adapterExtra, auc, returnCreative, impExtInfoMap, bidResponseExt, pubID)
 			seatBids = append(seatBids, *sb)
 			bidResponse.Cur = adapterSeatBids.Currency
 		}
@@ -1022,14 +1025,14 @@ 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) *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, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string) *openrtb2.SeatBid {
 	seatBid := &openrtb2.SeatBid{
 		Seat:  adapter.String(),
 		Group: 0, // Prebid cannot support roadblocking
 	}
 
 	var errList []error
-	seatBid.Bid, errList = e.makeBid(adapterBid.Bids, auc, returnCreative, impExtInfoMap)
+	seatBid.Bid, errList = e.makeBid(adapterBid.Bids, auc, returnCreative, impExtInfoMap, bidResponseExt, adapter, pubID)
 	if len(errList) > 0 {
 		adapterExtra[adapter].Errors = append(adapterExtra[adapter].Errors, errsToBidderErrors(errList)...)
 	}
@@ -1037,11 +1040,28 @@ 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) ([]openrtb2.Bid, []error) {
+func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string) ([]openrtb2.Bid, []error) {
 	result := make([]openrtb2.Bid, 0, len(bids))
 	errs := make([]error, 0, 1)
 
 	for _, bid := range bids {
+		if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationEnforce && bid.BidType == openrtb_ext.BidTypeBanner {
+			if !e.validateBannerCreativeSize(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.BannerCreativeMaxSize) {
+				continue // Don't add bid to result
+			}
+		} else if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationWarn && bid.BidType == openrtb_ext.BidTypeBanner {
+			e.validateBannerCreativeSize(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.BannerCreativeMaxSize)
+		}
+		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) {
+					continue // Don't add bid to result
+				}
+			} else if e.bidValidationEnforcement.SecureMarkup == config.ValidationWarn && (bid.BidType == openrtb_ext.BidTypeBanner || bid.BidType == openrtb_ext.BidTypeVideo) {
+				e.validateBidAdM(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.SecureMarkup)
+			}
+
+		}
 		bidExtPrebid := &openrtb_ext.ExtBidPrebid{
 			DealPriority:      bid.DealPriority,
 			DealTierSatisfied: bid.DealTierSatisfied,
@@ -1255,3 +1275,60 @@ func isAdsCertEnabled(experiment *openrtb_ext.Experiment, info config.BidderInfo
 	bidderAdsCertEnabled := info.Experiment.AdsCert.Enabled
 	return requestAdsCertEnabled && bidderAdsCertEnabled
 }
+
+func (e exchange) validateBannerCreativeSize(bid *entities.PbsOrtbBid, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, validationType string) bool {
+	if bid.Bid.W > e.bidValidationEnforcement.MaxCreativeWidth || bid.Bid.H > e.bidValidationEnforcement.MaxCreativeHeight {
+		// Add error to debug array
+		errorMessage := setErrorMessageCreativeSize(validationType)
+		bidCreativeMaxSizeError := openrtb_ext.ExtBidderMessage{
+			Code:    errortypes.BadServerResponseErrorCode,
+			Message: errorMessage,
+		}
+		bidResponseExt.Errors[adapter] = append(bidResponseExt.Errors[adapter], bidCreativeMaxSizeError)
+
+		// Log Metrics
+		e.me.RecordBidValidationCreativeSizeError(adapter, pubID)
+
+		return false
+	}
+	return true
+}
+
+func (e exchange) validateBidAdM(bid *entities.PbsOrtbBid, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, validationType string) bool {
+	invalidAdM := []string{"http:", "http%3A"}
+	requiredAdM := []string{"https:", "https%3A"}
+
+	if (strings.Contains(bid.Bid.AdM, invalidAdM[0]) || strings.Contains(bid.Bid.AdM, invalidAdM[1])) && (!strings.Contains(bid.Bid.AdM, requiredAdM[0]) && !strings.Contains(bid.Bid.AdM, requiredAdM[1])) {
+		// Add error to debug array
+		errorMessage := setErrorMessageSecureMarkup(validationType)
+		bidSecureMarkupError := openrtb_ext.ExtBidderMessage{
+			Code:    errortypes.BadServerResponseErrorCode,
+			Message: errorMessage,
+		}
+		bidResponseExt.Errors[adapter] = append(bidResponseExt.Errors[adapter], bidSecureMarkupError)
+
+		// Log Metrics
+		e.me.RecordBidValidationSecureMarkupError(adapter, pubID)
+
+		return false
+	}
+	return true
+}
+
+func setErrorMessageCreativeSize(validationType string) string {
+	if validationType == config.ValidationEnforce {
+		return "bidResponse rejected: size WxH"
+	} else if validationType == config.ValidationWarn {
+		return "bidResponse creative size warning: size WxH larger than AdUnit sizes"
+	}
+	return ""
+}
+
+func setErrorMessageSecureMarkup(validationType string) string {
+	if validationType == config.ValidationEnforce {
+		return "bidResponse rejected: insecure creative in secure context"
+	} else if validationType == config.ValidationWarn {
+		return "bidResponse secure markup warning: insecure creative in secure contexts"
+	}
+	return ""
+}
diff --git a/hooks/hookanalytics/analytics.go b/hooks/hookanalytics/analytics.go
index f55141fb..7907e57d 100644
--- a/hooks/hookanalytics/analytics.go
+++ b/hooks/hookanalytics/analytics.go
@@ -5,7 +5,7 @@
 package hookanalytics
 
 type Analytics struct {
-	Activities []Activity `json:"activities"`
+	Activities []Activity `json:"activities,omitempty"`
 }
 
 type Activity struct {
diff --git a/hooks/hookexecution/enricher.go b/hooks/hookexecution/enricher.go
new file mode 100644
index 00000000..cbefe201
--- /dev/null
+++ b/hooks/hookexecution/enricher.go
@@ -0,0 +1,204 @@
+package hookexecution
+
+import (
+	"encoding/json"
+
+	"github.com/buger/jsonparser"
+	"github.com/prebid/openrtb/v17/openrtb2"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/hooks/hookanalytics"
+	jsonpatch "gopkg.in/evanphx/json-patch.v4"
+)
+
+const (
+	// traceLevelBasic excludes debug_messages and analytic_tags from output
+	traceLevelBasic trace = "basic"
+	// traceLevelVerbose sets maximum level of output information
+	traceLevelVerbose trace = "verbose"
+)
+
+// Trace controls the level of detail in the output information returned from executing hooks.
+type trace string
+
+func (t trace) isBasicOrHigher() bool {
+	return t == traceLevelBasic || t.isVerbose()
+}
+
+func (t trace) isVerbose() bool {
+	return t == traceLevelVerbose
+}
+
+type extPrebid struct {
+	Prebid extModules `json:"prebid"`
+}
+
+type extModules struct {
+	Modules json.RawMessage `json:"modules"`
+}
+
+// EnrichExtBidResponse adds debug and trace information returned from executing hooks to the ext argument.
+// In response the outcome is visible under the key response.ext.prebid.modules.
+//
+// Debug information is added only if the debug mode is enabled by request and allowed by account (if provided).
+// The details of the trace output depends on the value in the bidRequest.ext.prebid.trace field.
+// Warnings returned if bidRequest contains unexpected types for debug fields controlling debug output.
+func EnrichExtBidResponse(
+	ext json.RawMessage,
+	stageOutcomes []StageOutcome,
+	bidRequest *openrtb2.BidRequest,
+	account *config.Account,
+) (json.RawMessage, []error, error) {
+	modules, warnings, err := GetModulesJSON(stageOutcomes, bidRequest, account)
+	if err != nil || modules == nil {
+		return ext, warnings, err
+	}
+
+	response, err := json.Marshal(extPrebid{Prebid: extModules{Modules: modules}})
+	if err != nil {
+		return ext, warnings, err
+	}
+
+	if ext != nil {
+		response, err = jsonpatch.MergePatch(ext, response)
+	}
+
+	return response, warnings, err
+}
+
+// GetModulesJSON returns debug and trace information produced from executing hooks.
+// Debug information is returned only if the debug mode is enabled by request and allowed by account (if provided).
+// The details of the trace output depends on the value in the bidRequest.ext.prebid.trace field.
+// Warnings returned if bidRequest contains unexpected types for debug fields controlling debug output.
+func GetModulesJSON(
+	stageOutcomes []StageOutcome,
+	bidRequest *openrtb2.BidRequest,
+	account *config.Account,
+) (json.RawMessage, []error, error) {
+	if len(stageOutcomes) == 0 {
+		return nil, nil, nil
+	}
+
+	trace, isDebugEnabled, warnings := getDebugContext(bidRequest, account)
+	modulesOutcome := getModulesOutcome(stageOutcomes, trace, isDebugEnabled)
+	if modulesOutcome == nil {
+		return nil, warnings, nil
+	}
+
+	data, err := json.Marshal(modulesOutcome)
+
+	return data, warnings, err
+}
+
+func getDebugContext(bidRequest *openrtb2.BidRequest, account *config.Account) (trace, bool, []error) {
+	var traceLevel string
+	var isDebugEnabled bool
+	var warnings []error
+	var err error
+
+	if bidRequest != nil {
+		traceLevel, err = jsonparser.GetString(bidRequest.Ext, "prebid", "trace")
+		if err != nil && err != jsonparser.KeyPathNotFoundError {
+			warnings = append(warnings, err)
+		}
+
+		isDebug, err := jsonparser.GetBoolean(bidRequest.Ext, "prebid", "debug")
+		if err != nil && err != jsonparser.KeyPathNotFoundError {
+			warnings = append(warnings, err)
+		}
+
+		isDebugEnabled = bidRequest.Test == 1 || isDebug
+		if account != nil {
+			isDebugEnabled = isDebugEnabled && account.DebugAllow
+		}
+	}
+
+	return trace(traceLevel), isDebugEnabled, warnings
+}
+
+func getModulesOutcome(stageOutcomes []StageOutcome, trace trace, isDebugEnabled bool) *ModulesOutcome {
+	var modulesOutcome ModulesOutcome
+	stages := make(map[string]Stage)
+	stageNames := make([]string, 0)
+
+	for _, stageOutcome := range stageOutcomes {
+		if len(stageOutcome.Groups) == 0 {
+			continue
+		}
+
+		prepareModulesOutcome(&modulesOutcome, stageOutcome.Groups, trace, isDebugEnabled)
+		if !trace.isBasicOrHigher() {
+			continue
+		}
+
+		stage, ok := stages[stageOutcome.Stage]
+		if !ok {
+			stageNames = append(stageNames, stageOutcome.Stage)
+			stage = Stage{
+				Stage:    stageOutcome.Stage,
+				Outcomes: []StageOutcome{},
+			}
+		}
+
+		stage.Outcomes = append(stage.Outcomes, stageOutcome)
+		if stageOutcome.ExecutionTimeMillis > stage.ExecutionTimeMillis {
+			stage.ExecutionTimeMillis = stageOutcome.ExecutionTimeMillis
+		}
+
+		stages[stageOutcome.Stage] = stage
+	}
+
+	if modulesOutcome.Errors == nil && modulesOutcome.Warnings == nil && len(stages) == 0 {
+		return nil
+	}
+
+	if len(stages) > 0 {
+		modulesOutcome.Trace = &TraceOutcome{}
+		modulesOutcome.Trace.Stages = make([]Stage, 0, len(stages))
+
+		for _, stage := range stageNames {
+			modulesOutcome.Trace.ExecutionTimeMillis += stages[stage].ExecutionTimeMillis
+			modulesOutcome.Trace.Stages = append(modulesOutcome.Trace.Stages, stages[stage])
+		}
+	}
+
+	return &modulesOutcome
+}
+
+func prepareModulesOutcome(modulesOutcome *ModulesOutcome, groups []GroupOutcome, trace trace, isDebugEnabled bool) {
+	for _, group := range groups {
+		for i, hookOutcome := range group.InvocationResults {
+			if !trace.isVerbose() {
+				group.InvocationResults[i].DebugMessages = nil
+				group.InvocationResults[i].AnalyticsTags = hookanalytics.Analytics{}
+			}
+
+			if isDebugEnabled {
+				modulesOutcome.Errors = fillMessages(modulesOutcome.Errors, hookOutcome.Errors, hookOutcome.HookID)
+				modulesOutcome.Warnings = fillMessages(modulesOutcome.Warnings, hookOutcome.Warnings, hookOutcome.HookID)
+			}
+		}
+	}
+}
+
+func fillMessages(messages Messages, values []string, hookID HookID) Messages {
+	if len(values) == 0 {
+		return messages
+	}
+
+	if messages == nil {
+		return Messages{hookID.ModuleCode: {hookID.HookImplCode: values}}
+	}
+
+	if _, ok := messages[hookID.ModuleCode]; !ok {
+		messages[hookID.ModuleCode] = map[string][]string{hookID.HookImplCode: values}
+		return messages
+	}
+
+	if prevValues, ok := messages[hookID.ModuleCode][hookID.HookImplCode]; ok {
+		values = append(prevValues, values...)
+	}
+
+	messages[hookID.ModuleCode][hookID.HookImplCode] = values
+
+	return messages
+}
diff --git a/hooks/hookexecution/outcome.go b/hooks/hookexecution/outcome.go
index d02bed9a..3eeb7bce 100644
--- a/hooks/hookexecution/outcome.go
+++ b/hooks/hookexecution/outcome.go
@@ -25,6 +25,37 @@ const (
 	ActionNone   Action = "no_action" // the hook does not want to take any action
 )
 
+// Messages in format: {"module": {"hook": ["msg1", "msg2"]}}
+type Messages map[string]map[string][]string
+
+// ModulesOutcome represents result of hooks execution
+// ready to be added to BidResponse.
+//
+// Errors and Warnings hold the error and warning
+// messages returned from executing individual hooks.
+type ModulesOutcome struct {
+	Errors   Messages      `json:"errors,omitempty"`
+	Warnings Messages      `json:"warnings,omitempty"`
+	Trace    *TraceOutcome `json:"trace,omitempty"`
+}
+
+// TraceOutcome holds the result of executing hooks at all stages.
+type TraceOutcome struct {
+	// ExecutionTime is the sum of ExecutionTime of all stages.
+	ExecutionTime
+	Stages []Stage `json:"stages"`
+}
+
+// Stage holds the result of executing hooks at specific stage.
+// May contain multiple StageOutcome results for stages executed
+// multiple times (ex. for each bidder-request/response stages).
+type Stage struct {
+	// ExecutionTime is set to the longest ExecutionTime of its children.
+	ExecutionTime
+	Stage    string         `json:"stage"`
+	Outcomes []StageOutcome `json:"outcomes"`
+}
+
 // StageOutcome represents the result of executing specific stage.
 type StageOutcome struct {
 	// ExecutionTime is the sum of ExecutionTime of all its groups
@@ -51,7 +82,7 @@ type HookOutcome struct {
 	Status        Status                  `json:"status"`
 	Action        Action                  `json:"action"`
 	Message       string                  `json:"message"` // arbitrary string value returned from hook execution
-	DebugMessages []string                `json:"debug_messages"`
+	DebugMessages []string                `json:"debug_messages,omitempty"`
 	Errors        []string                `json:"-"`
 	Warnings      []string                `json:"-"`
 }
@@ -63,5 +94,5 @@ type HookID struct {
 }
 
 type ExecutionTime struct {
-	ExecutionTimeMillis time.Duration `json:"execution_time_millis"`
+	ExecutionTimeMillis time.Duration `json:"execution_time_millis,omitempty"`
 }
diff --git a/metrics/config/metrics.go b/metrics/config/metrics.go
index 4f232445..f0ac46fa 100644
--- a/metrics/config/metrics.go
+++ b/metrics/config/metrics.go
@@ -279,6 +279,30 @@ func (me *MultiMetricsEngine) RecordAdsCertSignTime(adsCertSignTime time.Duratio
 	}
 }
 
+func (me *MultiMetricsEngine) RecordBidValidationCreativeSizeError(adapter openrtb_ext.BidderName, account string) {
+	for _, thisME := range *me {
+		thisME.RecordBidValidationCreativeSizeError(adapter, account)
+	}
+}
+
+func (me *MultiMetricsEngine) RecordBidValidationCreativeSizeWarn(adapter openrtb_ext.BidderName, account string) {
+	for _, thisME := range *me {
+		thisME.RecordBidValidationCreativeSizeWarn(adapter, account)
+	}
+}
+
+func (me *MultiMetricsEngine) RecordBidValidationSecureMarkupError(adapter openrtb_ext.BidderName, account string) {
+	for _, thisME := range *me {
+		thisME.RecordBidValidationSecureMarkupError(adapter, account)
+	}
+}
+
+func (me *MultiMetricsEngine) RecordBidValidationSecureMarkupWarn(adapter openrtb_ext.BidderName, account string) {
+	for _, thisME := range *me {
+		thisME.RecordBidValidationSecureMarkupWarn(adapter, account)
+	}
+}
+
 func (me *MultiMetricsEngine) RecordModuleCalled(labels metrics.ModuleLabels, duration time.Duration) {
 	for _, thisME := range *me {
 		thisME.RecordModuleCalled(labels, duration)
@@ -448,6 +472,18 @@ func (me *NilMetricsEngine) RecordAdsCertSignTime(adsCertSignTime time.Duration)
 
 }
 
+func (me *NilMetricsEngine) RecordBidValidationCreativeSizeError(adapter openrtb_ext.BidderName, account string) {
+}
+
+func (me *NilMetricsEngine) RecordBidValidationCreativeSizeWarn(adapter openrtb_ext.BidderName, account string) {
+}
+
+func (me *NilMetricsEngine) RecordBidValidationSecureMarkupError(adapter openrtb_ext.BidderName, account string) {
+}
+
+func (me *NilMetricsEngine) RecordBidValidationSecureMarkupWarn(adapter openrtb_ext.BidderName, account string) {
+}
+
 func (me *NilMetricsEngine) RecordModuleCalled(labels metrics.ModuleLabels, duration time.Duration) {
 }
 
diff --git a/metrics/go_metrics.go b/metrics/go_metrics.go
index 048eb8a7..1897a1ef 100644
--- a/metrics/go_metrics.go
+++ b/metrics/go_metrics.go
@@ -96,6 +96,12 @@ type AdapterMetrics struct {
 	ConnReused         metrics.Counter
 	ConnWaitTime       metrics.Timer
 	GDPRRequestBlocked metrics.Meter
+
+	BidValidationCreativeSizeErrorMeter metrics.Meter
+	BidValidationCreativeSizeWarnMeter  metrics.Meter
+
+	BidValidationSecureMarkupErrorMeter metrics.Meter
+	BidValidationSecureMarkupWarnMeter  metrics.Meter
 }
 
 type MarkupDeliveryMetrics struct {
@@ -112,6 +118,12 @@ type accountMetrics struct {
 	adapterMetrics       map[openrtb_ext.BidderName]*AdapterMetrics
 	moduleMetrics        map[string]*ModuleMetrics
 	storedResponsesMeter metrics.Meter
+
+	bidValidationCreativeSizeMeter     metrics.Meter
+	bidValidationCreativeSizeWarnMeter metrics.Meter
+
+	bidValidationSecureMarkupMeter     metrics.Meter
+	bidValidationSecureMarkupWarnMeter metrics.Meter
 }
 
 type ModuleMetrics struct {
@@ -442,6 +454,12 @@ func registerAdapterMetrics(registry metrics.Registry, adapterOrAccount string,
 	}
 	am.PanicMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("%[1]s.%[2]s.requests.panic", adapterOrAccount, exchange), registry)
 	am.GDPRRequestBlocked = metrics.GetOrRegisterMeter(fmt.Sprintf("%[1]s.%[2]s.gdpr_request_blocked", adapterOrAccount, exchange), registry)
+
+	am.BidValidationCreativeSizeErrorMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("%[1]s.%[2]s.response.validation.size.err", adapterOrAccount, exchange), registry)
+	am.BidValidationCreativeSizeWarnMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("%[1]s.%[2]s.response.validation.size.warn", adapterOrAccount, exchange), registry)
+
+	am.BidValidationSecureMarkupErrorMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("%[1]s.%[2]s.response.validation.secure.err", adapterOrAccount, exchange), registry)
+	am.BidValidationSecureMarkupWarnMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("%[1]s.%[2]s.response.validation.secure.warn", adapterOrAccount, exchange), registry)
 }
 
 func registerModuleMetrics(registry metrics.Registry, module string, stages []string, mm map[string]*ModuleMetrics) {
@@ -512,6 +530,12 @@ func (me *Metrics) getAccountMetrics(id string) *accountMetrics {
 			registerAdapterMetrics(me.MetricsRegistry, fmt.Sprintf("account.%s", id), string(a), am.adapterMetrics[a])
 		}
 	}
+	am.bidValidationCreativeSizeMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("account.%s.response.validation.size.err", id), me.MetricsRegistry)
+	am.bidValidationCreativeSizeWarnMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("account.%s.response.validation.size.warn", id), me.MetricsRegistry)
+
+	am.bidValidationSecureMarkupMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("account.%s.response.validation.secure.err", id), me.MetricsRegistry)
+	am.bidValidationSecureMarkupWarnMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("account.%s.response.validation.secure.warn", id), me.MetricsRegistry)
+
 	if !me.MetricsDisabled.AccountModulesMetrics {
 		for _, mod := range me.modules {
 			am.moduleMetrics[mod] = makeBlankModuleMetrics()
@@ -874,6 +898,62 @@ func (me *Metrics) RecordAdsCertSignTime(adsCertSignTime time.Duration) {
 	me.adsCertSignTimer.Update(adsCertSignTime)
 }
 
+func (me *Metrics) RecordBidValidationCreativeSizeError(adapter openrtb_ext.BidderName, pubID string) {
+	am, ok := me.AdapterMetrics[adapter]
+	if !ok {
+		glog.Errorf("Trying to run adapter metrics on %s: adapter metrics not found", string(adapter))
+		return
+	}
+	am.BidValidationCreativeSizeErrorMeter.Mark(1)
+
+	aam := me.getAccountMetrics(pubID)
+	if !me.MetricsDisabled.AccountAdapterDetails {
+		aam.bidValidationCreativeSizeMeter.Mark(1)
+	}
+}
+
+func (me *Metrics) RecordBidValidationCreativeSizeWarn(adapter openrtb_ext.BidderName, pubID string) {
+	am, ok := me.AdapterMetrics[adapter]
+	if !ok {
+		glog.Errorf("Trying to run adapter metrics on %s: adapter metrics not found", string(adapter))
+		return
+	}
+	am.BidValidationCreativeSizeWarnMeter.Mark(1)
+
+	aam := me.getAccountMetrics(pubID)
+	if !me.MetricsDisabled.AccountAdapterDetails {
+		aam.bidValidationCreativeSizeWarnMeter.Mark(1)
+	}
+}
+
+func (me *Metrics) RecordBidValidationSecureMarkupError(adapter openrtb_ext.BidderName, pubID string) {
+	am, ok := me.AdapterMetrics[adapter]
+	if !ok {
+		glog.Errorf("Trying to run adapter metrics on %s: adapter metrics not found", string(adapter))
+		return
+	}
+	am.BidValidationSecureMarkupErrorMeter.Mark(1)
+
+	aam := me.getAccountMetrics(pubID)
+	if !me.MetricsDisabled.AccountAdapterDetails {
+		aam.bidValidationSecureMarkupMeter.Mark(1)
+	}
+}
+
+func (me *Metrics) RecordBidValidationSecureMarkupWarn(adapter openrtb_ext.BidderName, pubID string) {
+	am, ok := me.AdapterMetrics[adapter]
+	if !ok {
+		glog.Errorf("Trying to run adapter metrics on %s: adapter metrics not found", string(adapter))
+		return
+	}
+	am.BidValidationSecureMarkupWarnMeter.Mark(1)
+
+	aam := me.getAccountMetrics(pubID)
+	if !me.MetricsDisabled.AccountAdapterDetails {
+		aam.bidValidationSecureMarkupWarnMeter.Mark(1)
+	}
+}
+
 func (me *Metrics) RecordModuleCalled(labels ModuleLabels, duration time.Duration) {
 	mm, err := me.getModuleMetric(labels)
 	if err != nil {
diff --git a/metrics/metrics.go b/metrics/metrics.go
index 4acee77f..d81026d5 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -435,6 +435,10 @@ type MetricsEngine interface {
 	RecordStoredResponse(pubId string)
 	RecordAdsCertReq(success bool)
 	RecordAdsCertSignTime(adsCertSignTime time.Duration)
+	RecordBidValidationCreativeSizeError(adapter openrtb_ext.BidderName, account string)
+	RecordBidValidationCreativeSizeWarn(adapter openrtb_ext.BidderName, account string)
+	RecordBidValidationSecureMarkupError(adapter openrtb_ext.BidderName, account string)
+	RecordBidValidationSecureMarkupWarn(adapter openrtb_ext.BidderName, account string)
 	RecordModuleCalled(labels ModuleLabels, duration time.Duration)
 	RecordModuleFailed(labels ModuleLabels)
 	RecordModuleSuccessNooped(labels ModuleLabels)
diff --git a/metrics/metrics_mock.go b/metrics/metrics_mock.go
index 7bdd0562..5184f33c 100644
--- a/metrics/metrics_mock.go
+++ b/metrics/metrics_mock.go
@@ -163,6 +163,22 @@ func (me *MetricsEngineMock) RecordAdsCertSignTime(adsCertSignTime time.Duration
 	me.Called(adsCertSignTime)
 }
 
+func (me *MetricsEngineMock) RecordBidValidationCreativeSizeError(adapter openrtb_ext.BidderName, account string) {
+	me.Called(adapter, account)
+}
+
+func (me *MetricsEngineMock) RecordBidValidationCreativeSizeWarn(adapter openrtb_ext.BidderName, account string) {
+	me.Called(adapter, account)
+}
+
+func (me *MetricsEngineMock) RecordBidValidationSecureMarkupError(adapter openrtb_ext.BidderName, account string) {
+	me.Called(adapter, account)
+}
+
+func (me *MetricsEngineMock) RecordBidValidationSecureMarkupWarn(adapter openrtb_ext.BidderName, account string) {
+	me.Called(adapter, account)
+}
+
 func (me *MetricsEngineMock) RecordModuleCalled(labels ModuleLabels, duration time.Duration) {
 	me.Called(labels, duration)
 }
diff --git a/metrics/prometheus/preload.go b/metrics/prometheus/preload.go
index a61f26c3..61ae568d 100644
--- a/metrics/prometheus/preload.go
+++ b/metrics/prometheus/preload.go
@@ -135,6 +135,26 @@ func preloadLabelValues(m *Metrics, syncerKeys []string, moduleStageNames map[st
 		adapterLabel: adapterValues,
 	})
 
+	preloadLabelValuesForCounter(m.adapterBidResponseSecureMarkupError, map[string][]string{
+		adapterLabel: adapterValues,
+		successLabel: boolValues,
+	})
+
+	preloadLabelValuesForCounter(m.adapterBidResponseSecureMarkupWarn, map[string][]string{
+		adapterLabel: adapterValues,
+		successLabel: boolValues,
+	})
+
+	preloadLabelValuesForCounter(m.adapterBidResponseValidationSizeError, map[string][]string{
+		adapterLabel: adapterValues,
+		successLabel: boolValues,
+	})
+
+	preloadLabelValuesForCounter(m.adapterBidResponseValidationSizeWarn, map[string][]string{
+		adapterLabel: adapterValues,
+		successLabel: boolValues,
+	})
+
 	preloadLabelValuesForHistogram(m.adapterPrices, map[string][]string{
 		adapterLabel: adapterValues,
 	})
diff --git a/metrics/prometheus/prometheus.go b/metrics/prometheus/prometheus.go
index ca5166cd..fad0b873 100644
--- a/metrics/prometheus/prometheus.go
+++ b/metrics/prometheus/prometheus.go
@@ -58,25 +58,34 @@ type Metrics struct {
 	adsCertSignTimer             prometheus.Histogram
 
 	// Adapter Metrics
-	adapterBids                *prometheus.CounterVec
-	adapterErrors              *prometheus.CounterVec
-	adapterPanics              *prometheus.CounterVec
-	adapterPrices              *prometheus.HistogramVec
-	adapterRequests            *prometheus.CounterVec
-	adapterRequestsTimer       *prometheus.HistogramVec
-	adapterReusedConnections   *prometheus.CounterVec
-	adapterCreatedConnections  *prometheus.CounterVec
-	adapterConnectionWaitTime  *prometheus.HistogramVec
-	adapterGDPRBlockedRequests *prometheus.CounterVec
+	adapterBids                           *prometheus.CounterVec
+	adapterErrors                         *prometheus.CounterVec
+	adapterPanics                         *prometheus.CounterVec
+	adapterPrices                         *prometheus.HistogramVec
+	adapterRequests                       *prometheus.CounterVec
+	adapterRequestsTimer                  *prometheus.HistogramVec
+	adapterReusedConnections              *prometheus.CounterVec
+	adapterCreatedConnections             *prometheus.CounterVec
+	adapterConnectionWaitTime             *prometheus.HistogramVec
+	adapterGDPRBlockedRequests            *prometheus.CounterVec
+	adapterBidResponseValidationSizeError *prometheus.CounterVec
+	adapterBidResponseValidationSizeWarn  *prometheus.CounterVec
+	adapterBidResponseSecureMarkupError   *prometheus.CounterVec
+	adapterBidResponseSecureMarkupWarn    *prometheus.CounterVec
 
 	// Syncer Metrics
 	syncerRequests *prometheus.CounterVec
 	syncerSets     *prometheus.CounterVec
 
 	// Account Metrics
-	accountRequests        *prometheus.CounterVec
-	accountDebugRequests   *prometheus.CounterVec
-	accountStoredResponses *prometheus.CounterVec
+	accountRequests                       *prometheus.CounterVec
+	accountDebugRequests                  *prometheus.CounterVec
+	accountStoredResponses                *prometheus.CounterVec
+	accountBidResponseValidationSizeError *prometheus.CounterVec
+	accountBidResponseValidationSizeWarn  *prometheus.CounterVec
+
+	accountBidResponseSecureMarkupError *prometheus.CounterVec
+	accountBidResponseSecureMarkupWarn  *prometheus.CounterVec
 
 	// Module Metrics as a map where the key is the module name
 	moduleDuration        map[string]*prometheus.HistogramVec
@@ -387,6 +396,26 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet
 			standardTimeBuckets)
 	}
 
+	metrics.adapterBidResponseValidationSizeError = newCounter(cfg, reg,
+		"adapter_response_validation_size_err",
+		"Count that tracks number of bids removed from bid response that had a creative size greater than maxWidth/maxHeight",
+		[]string{adapterLabel, successLabel})
+
+	metrics.adapterBidResponseValidationSizeWarn = newCounter(cfg, reg,
+		"adapter_response_validation_size_warn",
+		"Count that tracks number of bids removed from bid response that had a creative size greater than maxWidth/maxHeight (warn)",
+		[]string{adapterLabel, successLabel})
+
+	metrics.adapterBidResponseSecureMarkupError = newCounter(cfg, reg,
+		"adapter_response_validation_secure_err",
+		"Count that tracks number of bids removed from bid response that had a invalid bidAdm",
+		[]string{adapterLabel, successLabel})
+
+	metrics.adapterBidResponseSecureMarkupWarn = newCounter(cfg, reg,
+		"adapter_response_validation_secure_warn",
+		"Count that tracks number of bids removed from bid response that had a invalid bidAdm (warn)",
+		[]string{adapterLabel, successLabel})
+
 	metrics.adapterRequestsTimer = newHistogramVec(cfg, reg,
 		"adapter_request_time_seconds",
 		"Seconds to resolve each successful request labeled by adapter.",
@@ -413,6 +442,26 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet
 		"Count of total requests to Prebid Server that have debug enabled labled by account",
 		[]string{accountLabel})
 
+	metrics.accountBidResponseValidationSizeError = newCounter(cfg, reg,
+		"account_response_validation_size_err",
+		"Count that tracks number of bids removed from bid response that had a creative size greater than maxWidth/maxHeight labeled by account (enforce) ",
+		[]string{accountLabel, successLabel})
+
+	metrics.accountBidResponseValidationSizeWarn = newCounter(cfg, reg,
+		"account_response_validation_size_warn",
+		"Count that tracks number of bids removed from bid response that had a creative size greater than maxWidth/maxHeight labeled by account (warn)",
+		[]string{accountLabel, successLabel})
+
+	metrics.accountBidResponseSecureMarkupError = newCounter(cfg, reg,
+		"account_response_validation_secure_err",
+		"Count that tracks number of bids removed from bid response that had a invalid bidAdm labeled by account (enforce) ",
+		[]string{accountLabel, successLabel})
+
+	metrics.accountBidResponseSecureMarkupWarn = newCounter(cfg, reg,
+		"account_response_validation_secure_warn",
+		"Count that tracks number of bids removed from bid response that had a invalid bidAdm labeled by account (warn)",
+		[]string{accountLabel, successLabel})
+
 	metrics.requestsQueueTimer = newHistogramVec(cfg, reg,
 		"request_queue_time",
 		"Seconds request was waiting in queue",
@@ -897,6 +946,54 @@ func (m *Metrics) RecordAdsCertSignTime(adsCertSignTime time.Duration) {
 	m.adsCertSignTimer.Observe(adsCertSignTime.Seconds())
 }
 
+func (m *Metrics) RecordBidValidationCreativeSizeError(adapter openrtb_ext.BidderName, account string) {
+	m.adapterBidResponseValidationSizeError.With(prometheus.Labels{
+		adapterLabel: string(adapter), successLabel: successLabel,
+	}).Inc()
+
+	if !m.metricsDisabled.AccountAdapterDetails && account != metrics.PublisherUnknown {
+		m.accountBidResponseValidationSizeError.With(prometheus.Labels{
+			accountLabel: account, successLabel: successLabel,
+		}).Inc()
+	}
+}
+
+func (m *Metrics) RecordBidValidationCreativeSizeWarn(adapter openrtb_ext.BidderName, account string) {
+	m.adapterBidResponseValidationSizeWarn.With(prometheus.Labels{
+		adapterLabel: string(adapter), successLabel: successLabel,
+	}).Inc()
+
+	if !m.metricsDisabled.AccountAdapterDetails && account != metrics.PublisherUnknown {
+		m.accountBidResponseValidationSizeWarn.With(prometheus.Labels{
+			accountLabel: account, successLabel: successLabel,
+		}).Inc()
+	}
+}
+
+func (m *Metrics) RecordBidValidationSecureMarkupError(adapter openrtb_ext.BidderName, account string) {
+	m.adapterBidResponseSecureMarkupError.With(prometheus.Labels{
+		adapterLabel: string(adapter), successLabel: successLabel,
+	}).Inc()
+
+	if !m.metricsDisabled.AccountAdapterDetails && account != metrics.PublisherUnknown {
+		m.accountBidResponseSecureMarkupError.With(prometheus.Labels{
+			accountLabel: account, successLabel: successLabel,
+		}).Inc()
+	}
+}
+
+func (m *Metrics) RecordBidValidationSecureMarkupWarn(adapter openrtb_ext.BidderName, account string) {
+	m.adapterBidResponseSecureMarkupWarn.With(prometheus.Labels{
+		adapterLabel: string(adapter), successLabel: successLabel,
+	}).Inc()
+
+	if !m.metricsDisabled.AccountAdapterDetails && account != metrics.PublisherUnknown {
+		m.accountBidResponseSecureMarkupWarn.With(prometheus.Labels{
+			accountLabel: account, successLabel: successLabel,
+		}).Inc()
+	}
+}
+
 func (m *Metrics) RecordModuleCalled(labels metrics.ModuleLabels, duration time.Duration) {
 	m.moduleCalls[labels.Module].With(prometheus.Labels{
 		stageLabel: labels.Stage,
diff --git a/openrtb_ext/imp_bliink.go b/openrtb_ext/imp_bliink.go
new file mode 100644
index 00000000..71745dee
--- /dev/null
+++ b/openrtb_ext/imp_bliink.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+type ImpExtBliink struct {
+	TagID    string `json:"tagId"`
+	ImageUrl string `json:"imageUrl"`
+}
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index 47b66134..22f6b127 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -62,6 +62,13 @@ type ExtRequestPrebid struct {
 
 	//AlternateBidderCodes is populated with host's AlternateBidderCodes config if not defined in request
 	AlternateBidderCodes *ExtAlternateBidderCodes `json:"alternatebiddercodes,omitempty"`
+
+	// Trace controls the level of detail in the output information returned from executing hooks.
+	// There are two options:
+	// - verbose: sets maximum level of output information
+	// - basic: excludes debugmessages and analytic_tags from output
+	// any other value or an empty string disables trace output at all.
+	Trace string `json:"trace,omitempty"`
 }
 
 // Experiment defines if experimental features are available for the request
diff --git a/openrtb_ext/response.go b/openrtb_ext/response.go
index b20e741c..68097504 100644
--- a/openrtb_ext/response.go
+++ b/openrtb_ext/response.go
@@ -41,6 +41,7 @@ type ExtResponseSyncData struct {
 type ExtResponsePrebid struct {
 	AuctionTimestamp int64           `json:"auctiontimestamp,omitempty"`
 	Passthrough      json.RawMessage `json:"passthrough,omitempty"`
+	Modules          json.RawMessage `json:"modules,omitempty"`
 }
 
 // ExtUserSync defines the contract for bidresponse.ext.usersync.{bidder}.syncs[i]
diff --git a/static/bidder-info/outbrain.yaml b/static/bidder-info/outbrain.yaml
index 69a12ca6..09b73c21 100644
--- a/static/bidder-info/outbrain.yaml
+++ b/static/bidder-info/outbrain.yaml
@@ -7,10 +7,12 @@ capabilities:
     mediaTypes:
       - banner
       - native
+      - video
   site:
     mediaTypes:
       - banner
       - native
+      - video
 userSync:
   redirect:
     url: "https://prebidtest.zemanta.com/usersync/prebidtest?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&cb={{.RedirectURL}}"
diff --git a/static/bidder-params/bliink.json b/static/bidder-params/bliink.json
index 00621dee..831f71d0 100644
--- a/static/bidder-params/bliink.json
+++ b/static/bidder-params/bliink.json
@@ -4,5 +4,15 @@
   "description": "A schema which validates params accepted by the Bliink adapter",
   "type": "object",
 
-  "properties": {}
+  "properties": {
+    "tagId": {
+      "type": "string",
+      "description": "The TagID from BLIINK"
+    },
+    "imageUrl": {
+      "type": "string",
+      "description": "The image url on which the ad is displayed in case of in-image ad"
+    }
+  },
+  "required": ["tagId"]
 }
\ No newline at end of file


Test patch:
diff --git a/adapters/bliink/params_test.go b/adapters/bliink/params_test.go
new file mode 100644
index 00000000..dc45a324
--- /dev/null
+++ b/adapters/bliink/params_test.go
@@ -0,0 +1,44 @@
+package bliink
+
+import (
+	"encoding/json"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"testing"
+)
+
+var validParams = []string{
+	`{"tagId": "hash", "imageUrl": "https://www.test.com"}`,
+}
+
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, validParam := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderBliink, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected valid params: %s", validParam)
+		}
+	}
+}
+
+var invalidParams = []string{
+	``,
+	`null`,
+	`{"tagId": 32}`,
+	`{"imageUrl": 32}`,
+}
+
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, invalidParam := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderBliink, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed invalid params: %s", invalidParam)
+		}
+	}
+}
diff --git a/adapters/outbrain/outbraintest/exemplary/video.json b/adapters/outbrain/outbraintest/exemplary/video.json
new file mode 100644
index 00000000..550f56a6
--- /dev/null
+++ b/adapters/outbrain/outbraintest/exemplary/video.json
@@ -0,0 +1,131 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 640,
+          "h": 480,
+          "protocols": [1,2,3,4,5,6,7,8],
+          "playbackmethod": [1],
+          "mimes": ["video/mp4"],
+          "skip": 1,
+          "api": [2],
+          "maxbitrate": 3000
+        },
+        "ext": {
+          "bidder": {
+            "publisher": {
+              "id": "publisher-id"
+            }
+          }
+        }
+      }
+    ],
+    "site": {
+      "page": "http://example.com"
+    },
+    "device": {
+      "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36",
+      "h": 500,
+      "w": 1000
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://example.com/bid",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "w": 640,
+                "h": 480,
+                "protocols": [1,2,3,4,5,6,7,8],
+                "playbackmethod": [1],
+                "mimes": ["video/mp4"],
+                "skip": 1,
+                "api": [2],
+                "maxbitrate": 3000
+              },
+              "ext": {
+                "bidder": {
+                  "publisher": {
+                    "id": "publisher-id"
+                  }
+                }
+              }
+            }
+          ],
+          "site": {
+            "page": "http://example.com",
+            "publisher": {
+              "id": "publisher-id"
+            }
+          },
+          "device": {
+            "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36",
+            "h": 500,
+            "w": 1000
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "5095a742-1c27-402b-ab6f-66b1bd53383b",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "test-request-id",
+                  "impid": "test-imp-id",
+                  "price": 2.549447,
+                  "adm": "<vast ad>",
+                  "adomain": [
+                    "example.com"
+                  ],
+                  "cid": "test-cid",
+                  "crid": "test-crid",
+                  "cat": [
+                    "IAB13-4"
+                  ]
+                }
+              ],
+              "seat": "acc-1876"
+            }
+          ],
+          "bidid": "43ccadf2-8b2e-11eb-b294-de4c49e99ff6",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "test-request-id",
+            "impid": "test-imp-id",
+            "price": 2.549447,
+            "adm": "<vast ad>",
+            "adomain": [
+              "example.com"
+            ],
+            "cid": "test-cid",
+            "crid": "test-crid",
+            "cat": [
+              "IAB13-4"
+            ]
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/amp/parse_test.go b/amp/parse_test.go
index d2a9c5ee..6bff5b75 100644
--- a/amp/parse_test.go
+++ b/amp/parse_test.go
@@ -30,7 +30,7 @@ func TestParseParams(t *testing.T) {
 			description: "All Fields",
 			// targeting data is encoded string that looks like this: {"gam-key1":"val1","gam-key2":"val2"}
 			query: "tag_id=anyTagID&account=anyAccount&curl=anyCurl&consent_string=anyConsent&debug=1&__amp_source_origin=anyOrigin" +
-				"&slot=anySlot&timeout=42&h=1&w=2&oh=3&ow=4&ms=10x11,12x13&targeting=%7B%22gam-key1%22%3A%22val1%22%2C%22gam-key2%22%3A%22val2%22%7D",
+				"&slot=anySlot&timeout=42&h=1&w=2&oh=3&ow=4&ms=10x11,12x13&targeting=%7B%22gam-key1%22%3A%22val1%22%2C%22gam-key2%22%3A%22val2%22%7D&trace=basic",
 			expectedParams: Params{
 				Account:         "anyAccount",
 				CanonicalURL:    "anyCurl",
@@ -50,6 +50,7 @@ func TestParseParams(t *testing.T) {
 					},
 				},
 				Targeting: `{"gam-key1":"val1","gam-key2":"val2"}`,
+				Trace:     "basic",
 			},
 		},
 		{
diff --git a/config/config_test.go b/config/config_test.go
index 3298bec9..43ab9273 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -175,6 +175,10 @@ func TestDefaults(t *testing.T) {
 	cmpNils(t, "host_schain_node", cfg.HostSChainNode)
 	cmpStrings(t, "datacenter", cfg.DataCenter, "")
 	cmpBools(t, "hooks.enabled", cfg.Hooks.Enabled, false)
+	cmpStrings(t, "validations.banner_creative_max_size", cfg.Validations.BannerCreativeMaxSize, "skip")
+	cmpStrings(t, "validations.secure_markup", cfg.Validations.SecureMarkup, "skip")
+	cmpInts(t, "validations.max_creative_width", int(cfg.Validations.MaxCreativeWidth), 0)
+	cmpInts(t, "validations.max_creative_height", int(cfg.Validations.MaxCreativeHeight), 0)
 	cmpBools(t, "account_modules_metrics", cfg.Metrics.Disabled.AccountModulesMetrics, false)
 
 	//Assert purpose VendorExceptionMap hash tables were built correctly
@@ -409,6 +413,11 @@ host_schain_node:
     sid: "00001"
     rid: "BidRequest"
     hp: 1
+validations:
+    banner_creative_max_size: "skip"
+    secure_markup: "skip"
+    max_creative_width: 0
+    max_creative_height: 0
 experiment:
     adscert:
         mode: inprocess
@@ -497,6 +506,10 @@ func TestFullConfig(t *testing.T) {
 	cmpStrings(t, "host_schain_node.rid", cfg.HostSChainNode.RID, "BidRequest")
 	cmpInt8s(t, "host_schain_node.hp", cfg.HostSChainNode.HP, &int8One)
 	cmpStrings(t, "datacenter", cfg.DataCenter, "1")
+	cmpStrings(t, "validations.banner_creative_max_size", cfg.Validations.BannerCreativeMaxSize, "skip")
+	cmpStrings(t, "validations.secure_markup", cfg.Validations.SecureMarkup, "skip")
+	cmpInts(t, "validations.max_creative_width", int(cfg.Validations.MaxCreativeWidth), 0)
+	cmpInts(t, "validations.max_creative_height", int(cfg.Validations.MaxCreativeHeight), 0)
 
 	//Assert the NonStandardPublishers was correctly unmarshalled
 	assert.Equal(t, []string{"pub1", "pub2"}, cfg.GDPR.NonStandardPublishers, "gdpr.non_standard_publishers")
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 187a41b0..58c13963 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -3,6 +3,7 @@ package openrtb2
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net/http"
 	"net/http/httptest"
@@ -11,6 +12,7 @@ import (
 	"reflect"
 	"strconv"
 	"testing"
+	"time"
 
 	"github.com/julienschmidt/httprouter"
 	"github.com/prebid/openrtb/v17/openrtb2"
@@ -20,7 +22,9 @@ import (
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/exchange"
 	"github.com/prebid/prebid-server/hooks"
+	"github.com/prebid/prebid-server/hooks/hookexecution"
 	"github.com/prebid/prebid-server/hooks/hookstage"
+	"github.com/prebid/prebid-server/metrics"
 	metricsConfig "github.com/prebid/prebid-server/metrics/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
@@ -116,9 +120,7 @@ func TestGoodAmpRequests(t *testing.T) {
 			// Assertions
 			if assert.Equal(t, test.ExpectedReturnCode, recorder.Code, "Expected status %d. Got %d. Amp test file: %s", http.StatusOK, recorder.Code, filename) {
 				if test.ExpectedReturnCode == http.StatusOK {
-					var ampResponse AmpResponse
-					assert.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &ampResponse), "Error unmarshalling ampResponse: %v", err)
-					assert.Equal(t, test.ExpectedAmpResponse, ampResponse, "Not the expected response. Test file: %s", filename)
+					assert.JSONEq(t, string(test.ExpectedAmpResponse), string(recorder.Body.Bytes()), "Not the expected response. Test file: %s", filename)
 				} else {
 					assert.Equal(t, test.ExpectedErrorMessage, recorder.Body.String(), filename)
 				}
@@ -346,8 +348,8 @@ func TestGDPRConsent(t *testing.T) {
 			return
 		}
 		assert.Equal(t, test.expectedUserExt, ue, test.description)
-		assert.Equal(t, expectedErrorsFromHoldAuction, response.Errors, test.description+":errors")
-		assert.Empty(t, response.Warnings, test.description+":warnings")
+		assert.Equal(t, expectedErrorsFromHoldAuction, response.ORTB2.Ext.Errors, test.description+":errors")
+		assert.Empty(t, response.ORTB2.Ext.Warnings, test.description+":warnings")
 
 		// Invoke Endpoint With Legacy Param
 		requestLegacy := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&consent_type=2&gdpr_consent=%s", test.consent), nil)
@@ -377,8 +379,8 @@ func TestGDPRConsent(t *testing.T) {
 			return
 		}
 		assert.Equal(t, test.expectedUserExt, ueLegacy, test.description+":legacy")
-		assert.Equal(t, expectedErrorsFromHoldAuction, responseLegacy.Errors, test.description+":legacy:errors")
-		assert.Empty(t, responseLegacy.Warnings, test.description+":legacy:warnings")
+		assert.Equal(t, expectedErrorsFromHoldAuction, responseLegacy.ORTB2.Ext.Errors, test.description+":legacy:errors")
+		assert.Empty(t, responseLegacy.ORTB2.Ext.Warnings, test.description+":legacy:warnings")
 	}
 }
 
@@ -470,6 +472,39 @@ func TestOverrideWithParams(t *testing.T) {
 				errorMsgs: nil,
 			},
 		},
+		{
+			desc: "amp.Params with Trace field - expect ext.prebid.trace to be added",
+			given: testInput{
+				ampParams:  amp.Params{Trace: "verbose"},
+				bidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}}},
+			},
+			expected: testOutput{
+				bidRequest: &openrtb2.BidRequest{
+					Imp:  []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}},
+					Site: &openrtb2.Site{Ext: json.RawMessage(`{"amp":1}`)},
+					Ext:  json.RawMessage(`{"prebid":{"trace":"verbose"}}`),
+				},
+				errorMsgs: nil,
+			},
+		},
+		{
+			desc: "amp.Params with Trace field - expect ext.prebid.trace to be merged with existing ext fields",
+			given: testInput{
+				ampParams: amp.Params{Trace: "verbose"},
+				bidRequest: &openrtb2.BidRequest{
+					Imp: []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}},
+					Ext: json.RawMessage(`{"prebid":{"debug":true}}`),
+				},
+			},
+			expected: testOutput{
+				bidRequest: &openrtb2.BidRequest{
+					Imp:  []openrtb2.Imp{{Banner: &openrtb2.Banner{Format: []openrtb2.Format{}}}},
+					Site: &openrtb2.Site{Ext: json.RawMessage(`{"amp":1}`)},
+					Ext:  json.RawMessage(`{"prebid":{"debug":true,"trace":"verbose"}}`),
+				},
+				errorMsgs: nil,
+			},
+		},
 		{
 			desc: "bid request with malformed User.Ext - amp.Params with AdditionalConsent - expect error",
 			given: testInput{
@@ -732,8 +767,8 @@ func TestCCPAConsent(t *testing.T) {
 			return
 		}
 		assert.Equal(t, test.expectedRegExt, re, test.description)
-		assert.Equal(t, expectedErrorsFromHoldAuction, response.Errors)
-		assert.Empty(t, response.Warnings)
+		assert.Equal(t, expectedErrorsFromHoldAuction, response.ORTB2.Ext.Errors)
+		assert.Empty(t, response.ORTB2.Ext.Warnings)
 	}
 }
 
@@ -842,15 +877,15 @@ func TestConsentWarnings(t *testing.T) {
 			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, expectedErrorsFromHoldAuction, response.ORTB2.Ext.Errors)
 			if testCase.invalidConsentURL {
-				assert.Equal(t, testCase.expectedWarnings, response.Warnings)
+				assert.Equal(t, testCase.expectedWarnings, response.ORTB2.Ext.Warnings)
 			} else {
-				assert.Empty(t, response.Warnings)
+				assert.Empty(t, response.ORTB2.Ext.Warnings)
 			}
 
 		} else {
-			assert.Equal(t, testCase.expectedWarnings, response.Warnings)
+			assert.Equal(t, testCase.expectedWarnings, response.ORTB2.Ext.Warnings)
 		}
 	}
 }
@@ -943,8 +978,8 @@ func TestNewAndLegacyConsentBothProvided(t *testing.T) {
 			return
 		}
 		assert.Equal(t, test.expectedUserExt, ue, test.description)
-		assert.Equal(t, expectedErrorsFromHoldAuction, response.Errors)
-		assert.Empty(t, response.Warnings)
+		assert.Equal(t, expectedErrorsFromHoldAuction, response.ORTB2.Ext.Errors)
+		assert.Empty(t, response.ORTB2.Ext.Warnings)
 	}
 }
 
@@ -1067,7 +1102,7 @@ func TestAmpDebug(t *testing.T) {
 			t.Errorf("Bad targeting data. Expected 3 keys, got %d.", len(response.Targeting))
 		}
 
-		if response.Debug == nil {
+		if response.ORTB2.Ext.Debug == nil {
 			t.Errorf("Debug requested but not present")
 		}
 	}
@@ -1143,7 +1178,7 @@ func TestQueryParamOverrides(t *testing.T) {
 	}
 
 	var resolvedRequest openrtb2.BidRequest
-	err := json.Unmarshal(response.Debug.ResolvedRequest, &resolvedRequest)
+	err := json.Unmarshal(response.ORTB2.Ext.Debug.ResolvedRequest, &resolvedRequest)
 	assert.NoError(t, err, "resolved request should have a correct format")
 	if resolvedRequest.TMax != timeout {
 		t.Errorf("Expected TMax to equal timeout (%d), got: %d", timeout, resolvedRequest.TMax)
@@ -1292,7 +1327,7 @@ func (s formatOverrideSpec) execute(t *testing.T) {
 		t.Fatalf("Error unmarshalling response: %s", err.Error())
 	}
 	var resolvedRequest openrtb2.BidRequest
-	err := json.Unmarshal(response.Debug.ResolvedRequest, &resolvedRequest)
+	err := json.Unmarshal(response.ORTB2.Ext.Debug.ResolvedRequest, &resolvedRequest)
 	assert.NoError(t, err, "resolved request should have the correct format")
 	formats := resolvedRequest.Imp[0].Banner.Format
 	if len(formats) != len(s.expect) {
@@ -1934,84 +1969,82 @@ func TestSetTargeting(t *testing.T) {
 	}
 }
 
-func TestValidAmpResponseWhenRequestStagesRejected(t *testing.T) {
+func TestValidAmpResponseWhenRequestRejected(t *testing.T) {
 	const nbr int = 123
-	const file string = "sample-requests/amp/valid-supplementary/aliased-buyeruids.json"
-
-	var test testCase
-	fileData, err := os.ReadFile(file)
-	assert.NoError(t, err, "Failed to read test file.")
-	if err := json.Unmarshal(fileData, &test); err != nil {
-		t.Fatal("Failed to unmarshal test file.")
-	}
 
 	testCases := []struct {
-		description         string
-		file                string
-		planBuilder         hooks.ExecutionPlanBuilder
-		expectedAmpResponse AmpResponse
+		description string
+		file        string
+		planBuilder hooks.ExecutionPlanBuilder
 	}{
 		{
-			description:         "Assert correct BidResponse when request rejected at entrypoint stage",
-			file:                file,
-			planBuilder:         mockPlanBuilder{entrypointPlan: makeRejectPlan[hookstage.Entrypoint](mockRejectionHook{nbr})},
-			expectedAmpResponse: AmpResponse{Targeting: map[string]string{}},
+			description: "Assert correct AmpResponse when request rejected at entrypoint stage",
+			file:        "sample-requests/hooks/amp_entrypoint_reject.json",
+			planBuilder: mockPlanBuilder{entrypointPlan: makePlan[hookstage.Entrypoint](mockRejectionHook{nbr})},
 		},
 		{
-			description:         "Assert correct BidResponse when request rejected at raw-auction stage",
-			file:                file,
-			planBuilder:         mockPlanBuilder{rawAuctionPlan: makeRejectPlan[hookstage.RawAuctionRequest](mockRejectionHook{nbr})},
-			expectedAmpResponse: test.ExpectedAmpResponse,
+			// raw_auction stage not executed for AMP endpoint, so we expect full response
+			description: "Assert correct AmpResponse when request rejected at raw_auction stage",
+			file:        "sample-requests/amp/valid-supplementary/aliased-buyeruids.json",
+			planBuilder: mockPlanBuilder{rawAuctionPlan: makePlan[hookstage.RawAuctionRequest](mockRejectionHook{nbr})},
 		},
 		{
-			description:         "Assert correct AmpResponse when request rejected at processed-auction stage",
-			file:                file,
-			planBuilder:         mockPlanBuilder{processedAuctionPlan: makeRejectPlan[hookstage.ProcessedAuctionRequest](mockRejectionHook{nbr})},
-			expectedAmpResponse: AmpResponse{Targeting: map[string]string{}},
+			description: "Assert correct AmpResponse when request rejected at processed_auction stage",
+			file:        "sample-requests/hooks/amp_processed_auction_request_reject.json",
+			planBuilder: mockPlanBuilder{processedAuctionPlan: makePlan[hookstage.ProcessedAuctionRequest](mockRejectionHook{nbr})},
 		},
 		{
-			// bidder-request stage rejects only bidder, so we expect bidder rejection warning added
-			description: "Assert correct BidResponse when request rejected at bidder-request stage",
-			file:        file,
-			planBuilder: mockPlanBuilder{bidderRequestPlan: makeRejectPlan[hookstage.BidderRequest](mockRejectionHook{nbr})},
-			expectedAmpResponse: AmpResponse{Targeting: map[string]string{}, Warnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{
-				"appnexus": {
-					{
-						Code:    11,
-						Message: "Module foobar (hook: foo) rejected request with code 123 at bidder_request stage",
-					},
-				},
-				"general": {
-					{
-						Code:    10002,
-						Message: "debug turned off for account",
-					},
-				},
-			}},
+			// bidder_request stage rejects only bidder, so we expect bidder rejection warning added
+			description: "Assert correct AmpResponse when request rejected at bidder-request stage",
+			file:        "sample-requests/hooks/amp_bidder_reject.json",
+			planBuilder: mockPlanBuilder{bidderRequestPlan: makePlan[hookstage.BidderRequest](mockRejectionHook{nbr})},
+		},
+		{
+			// raw_bidder_response stage rejects only bidder, so we expect bidder rejection warning added
+			description: "Assert correct AmpResponse when request rejected at raw_bidder_response stage",
+			file:        "sample-requests/hooks/amp_bidder_response_reject.json",
+			planBuilder: mockPlanBuilder{rawBidderResponsePlan: makePlan[hookstage.RawBidderResponse](mockRejectionHook{nbr})},
 		},
 		{
-			description: "Assert correct AmpResponse when request rejected at raw-bidder-response stage",
-			file:        file,
-			planBuilder: mockPlanBuilder{rawBidderResponsePlan: makeRejectPlan[hookstage.RawBidderResponse](mockRejectionHook{nbr})},
-			expectedAmpResponse: AmpResponse{Targeting: map[string]string{}, Warnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{
-				"appnexus": {
+			// no debug information should be added for raw_auction stage because it's not executed for amp endpoint
+			description: "Assert correct AmpResponse with debug information from modules added to ext.prebid.modules",
+			file:        "sample-requests/hooks/amp.json",
+			planBuilder: mockPlanBuilder{
+				entrypointPlan: hooks.Plan[hookstage.Entrypoint]{
 					{
-						Code:    11,
-						Message: "Module foobar (hook: foo) rejected request with code 123 at raw_bidder_response stage",
+						Timeout: 5 * time.Millisecond,
+						Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
+							entryPointHookUpdateWithErrors,
+							entryPointHookUpdateWithErrorsAndWarnings,
+						},
+					},
+					{
+						Timeout: 5 * time.Millisecond,
+						Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
+							entryPointHookUpdate,
+						},
 					},
 				},
-				"general": {
+				rawAuctionPlan: hooks.Plan[hookstage.RawAuctionRequest]{
 					{
-						Code:    10002,
-						Message: "debug turned off for account",
+						Timeout: 5 * time.Millisecond,
+						Hooks: []hooks.HookWrapper[hookstage.RawAuctionRequest]{
+							rawAuctionHookNone,
+						},
 					},
 				},
-			}},
+			},
 		},
 	}
 
 	for _, tc := range testCases {
 		t.Run(tc.description, func(t *testing.T) {
+			fileData, err := os.ReadFile(tc.file)
+			assert.NoError(t, err, "Failed to read test file.")
+
+			test := testCase{}
+			assert.NoError(t, json.Unmarshal(fileData, &test), "Failed to parse test file.")
+
 			request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?%s", test.Query), nil)
 			recorder := httptest.NewRecorder()
 			query := request.URL.Query()
@@ -2021,15 +2054,29 @@ func TestValidAmpResponseWhenRequestStagesRejected(t *testing.T) {
 			test.planBuilder = tc.planBuilder
 			test.endpointType = AMP_ENDPOINT
 
-			ampEndpointHandler, _, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, &config.Configuration{MaxRequestSize: maxSize})
+			cfg := &config.Configuration{MaxRequestSize: maxSize, AccountDefaults: config.Account{DebugAllow: true}}
+			ampEndpointHandler, _, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, cfg)
 			assert.NoError(t, err, "Failed to build test endpoint.")
 
 			ampEndpointHandler(recorder, request, nil)
 			assert.Equal(t, recorder.Code, http.StatusOK, "Endpoint should return 200 OK.")
 
-			var actualResp AmpResponse
-			assert.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &actualResp), "Unable to unmarshal actual BidResponse.")
-			assert.Equal(t, tc.expectedAmpResponse, actualResp, "Invalid AMP Response.")
+			var actualAmpResp AmpResponse
+			var expectedAmpResp AmpResponse
+			assert.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &actualAmpResp), "Unable to unmarshal actual AmpResponse.")
+			assert.NoError(t, json.Unmarshal(test.ExpectedAmpResponse, &expectedAmpResp), "Unable to unmarshal expected AmpResponse.")
+
+			// validate modules data separately, because it has dynamic data
+			if expectedAmpResp.ORTB2.Ext.Prebid == nil {
+				assert.Nil(t, actualAmpResp.ORTB2.Ext.Prebid, "AmpResponse.ortb2.ext.prebid expected to be nil.")
+			} else {
+				hookexecution.AssertEqualModulesData(t, expectedAmpResp.ORTB2.Ext.Prebid.Modules, actualAmpResp.ORTB2.Ext.Prebid.Modules)
+			}
+
+			// reset modules to validate amp responses
+			actualAmpResp.ORTB2.Ext.Prebid = nil
+			expectedAmpResp.ORTB2.Ext.Prebid = nil
+			assert.Equal(t, expectedAmpResp, actualAmpResp, "Invalid AMP Response.")
 
 			// Close servers regardless if the test case was run or not
 			for _, mockBidServer := range mockBidServers {
@@ -2039,3 +2086,111 @@ func TestValidAmpResponseWhenRequestStagesRejected(t *testing.T) {
 		})
 	}
 }
+
+func TestSendAmpResponse_LogsErrors(t *testing.T) {
+	testCases := []struct {
+		description    string
+		expectedErrors []error
+		expectedStatus int
+		writer         http.ResponseWriter
+		request        *openrtb2.BidRequest
+		response       *openrtb2.BidResponse
+		hookExecutor   hookexecution.HookStageExecutor
+	}{
+		{
+			description: "Error logged when bid.ext unmarshal fails",
+			expectedErrors: []error{
+				errors.New("Critical error while unpacking AMP targets: unexpected end of JSON input"),
+			},
+			expectedStatus: http.StatusInternalServerError,
+			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{},
+		},
+		{
+			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"),
+			},
+			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{},
+		},
+		{
+			description: "Error logged when response encoding fails",
+			expectedErrors: []error{
+				errors.New("/openrtb2/amp Failed to send response: failed writing response"),
+			},
+			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{},
+		},
+		{
+			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("{}")},
+			hookExecutor: &mockStageExecutor{
+				outcomes: []hookexecution.StageOutcome{
+					{
+						Entity: "bid-request",
+						Stage:  hooks.StageBidderRequest.String(),
+						Groups: []hookexecution.GroupOutcome{
+							{
+								InvocationResults: []hookexecution.HookOutcome{
+									{
+										HookID: hookexecution.HookID{
+											ModuleCode:   "foobar",
+											HookImplCode: "foo",
+										},
+										Status:   hookexecution.StatusSuccess,
+										Action:   hookexecution.ActionNone,
+										Warnings: []string{"warning message"},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.description, func(t *testing.T) {
+			labels := metrics.Labels{}
+			ao := analytics.AmpObject{}
+			account := &config.Account{DebugAllow: true}
+			reqWrapper := openrtb_ext.RequestWrapper{BidRequest: test.request}
+
+			labels, ao = sendAmpResponse(test.writer, test.hookExecutor, test.response, &reqWrapper, account, labels, ao, nil)
+
+			assert.Equal(t, ao.Errors, test.expectedErrors, "Invalid errors.")
+			assert.Equal(t, test.expectedStatus, ao.Status, "Invalid HTTP response status.")
+		})
+	}
+}
+
+type errorResponseWriter struct{}
+
+func (e errorResponseWriter) Header() http.Header {
+	return http.Header{}
+}
+
+func (e errorResponseWriter) Write(bytes []byte) (int, error) {
+	return 0, errors.New("failed writing response")
+}
+
+func (e errorResponseWriter) WriteHeader(statusCode int) {}
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 72314c91..b32f1349 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -20,6 +20,7 @@ import (
 	"github.com/prebid/openrtb/v17/native1"
 	nativeRequests "github.com/prebid/openrtb/v17/native1/request"
 	"github.com/prebid/openrtb/v17/openrtb2"
+	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/hooks"
 	"github.com/prebid/prebid-server/hooks/hookexecution"
 	"github.com/prebid/prebid-server/hooks/hookstage"
@@ -4656,9 +4657,34 @@ func TestValidateStoredResp(t *testing.T) {
 	}
 }
 
-func TestValidResponseWhenRequestRejected(t *testing.T) {
+func TestValidResponseAfterExecutingStages(t *testing.T) {
 	const nbr int = 123
-	const file string = "sample-requests/valid-whole/hooks/auction_reject.json"
+
+	hooksPlanBuilder := mockPlanBuilder{
+		entrypointPlan: hooks.Plan[hookstage.Entrypoint]{
+			{
+				Timeout: 5 * time.Millisecond,
+				Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
+					entryPointHookUpdateWithErrors,
+					entryPointHookUpdateWithErrorsAndWarnings,
+				},
+			},
+			{
+				Timeout: 5 * time.Millisecond,
+				Hooks: []hooks.HookWrapper[hookstage.Entrypoint]{
+					entryPointHookUpdate,
+				},
+			},
+		},
+		rawAuctionPlan: hooks.Plan[hookstage.RawAuctionRequest]{
+			{
+				Timeout: 5 * time.Millisecond,
+				Hooks: []hooks.HookWrapper[hookstage.RawAuctionRequest]{
+					rawAuctionHookNone,
+				},
+			},
+		},
+	}
 
 	testCases := []struct {
 		description string
@@ -4667,29 +4693,34 @@ func TestValidResponseWhenRequestRejected(t *testing.T) {
 	}{
 		{
 			description: "Assert correct BidResponse when request rejected at entrypoint stage",
-			file:        file,
-			planBuilder: mockPlanBuilder{entrypointPlan: makeRejectPlan[hookstage.Entrypoint](mockRejectionHook{nbr})},
+			file:        "sample-requests/hooks/auction_entrypoint_reject.json",
+			planBuilder: mockPlanBuilder{entrypointPlan: makePlan[hookstage.Entrypoint](mockRejectionHook{nbr})},
 		},
 		{
 			description: "Assert correct BidResponse when request rejected at raw-auction stage",
-			file:        file,
-			planBuilder: mockPlanBuilder{rawAuctionPlan: makeRejectPlan[hookstage.RawAuctionRequest](mockRejectionHook{nbr})},
+			file:        "sample-requests/hooks/auction_raw_auction_request_reject.json",
+			planBuilder: mockPlanBuilder{rawAuctionPlan: makePlan[hookstage.RawAuctionRequest](mockRejectionHook{nbr})},
 		},
 		{
 			description: "Assert correct BidResponse when request rejected at processed-auction stage",
-			file:        file,
-			planBuilder: mockPlanBuilder{processedAuctionPlan: makeRejectPlan[hookstage.ProcessedAuctionRequest](mockRejectionHook{nbr})},
+			file:        "sample-requests/hooks/auction_processed_auction_request_reject.json",
+			planBuilder: mockPlanBuilder{processedAuctionPlan: makePlan[hookstage.ProcessedAuctionRequest](mockRejectionHook{nbr})},
 		},
 		{
 			// bidder-request stage doesn't reject whole request, so we do not expect NBR code in response
 			description: "Assert correct BidResponse when request rejected at bidder-request stage",
-			file:        "sample-requests/valid-whole/hooks/auction_bidder_reject.json",
-			planBuilder: mockPlanBuilder{bidderRequestPlan: makeRejectPlan[hookstage.BidderRequest](mockRejectionHook{nbr})},
+			file:        "sample-requests/hooks/auction_bidder_reject.json",
+			planBuilder: mockPlanBuilder{bidderRequestPlan: makePlan[hookstage.BidderRequest](mockRejectionHook{nbr})},
 		},
 		{
 			description: "Assert correct BidResponse when request rejected at raw-bidder-response stage",
-			file:        "sample-requests/valid-whole/hooks/auction_bidder_response_reject.json",
-			planBuilder: mockPlanBuilder{rawBidderResponsePlan: makeRejectPlan[hookstage.RawBidderResponse](mockRejectionHook{nbr})},
+			file:        "sample-requests/hooks/auction_bidder_response_reject.json",
+			planBuilder: mockPlanBuilder{rawBidderResponsePlan: makePlan[hookstage.RawBidderResponse](mockRejectionHook{nbr})},
+		},
+		{
+			description: "Assert correct BidResponse with debug information from modules added to ext.prebid.modules",
+			file:        "sample-requests/hooks/auction.json",
+			planBuilder: hooksPlanBuilder,
 		},
 	}
 
@@ -4703,7 +4734,8 @@ func TestValidResponseWhenRequestRejected(t *testing.T) {
 			test.planBuilder = tc.planBuilder
 			test.endpointType = OPENRTB_ENDPOINT
 
-			auctionEndpointHandler, _, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, &config.Configuration{MaxRequestSize: maxSize})
+			cfg := &config.Configuration{MaxRequestSize: maxSize, AccountDefaults: config.Account{DebugAllow: true}}
+			auctionEndpointHandler, _, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, cfg)
 			assert.NoError(t, err, "Failed to build test endpoint.")
 
 			recorder := httptest.NewRecorder()
@@ -4725,7 +4757,13 @@ func TestValidResponseWhenRequestRejected(t *testing.T) {
 
 			assertBidResponseEqual(t, tc.file, expectedResp, actualResp)
 			assert.Equal(t, expectedResp.NBR, actualResp.NBR, "Invalid NBR.")
-			assert.Equal(t, expectedExt.Warnings, actualExt.Warnings, "Wrong bidResponse.ext.")
+			assert.Equal(t, expectedExt.Warnings, actualExt.Warnings, "Wrong bidResponse.ext.warnings.")
+
+			if expectedExt.Prebid != nil {
+				hookexecution.AssertEqualModulesData(t, expectedExt.Prebid.Modules, actualExt.Prebid.Modules)
+			} else {
+				assert.Nil(t, actualExt.Prebid, "Invalid BidResponse.ext.prebid")
+			}
 
 			// Close servers regardless if the test case was run or not
 			for _, mockBidServer := range mockBidServers {
@@ -4736,6 +4774,78 @@ func TestValidResponseWhenRequestRejected(t *testing.T) {
 	}
 }
 
+func TestSendAuctionResponse_LogsErrors(t *testing.T) {
+	hookExecutor := &mockStageExecutor{
+		outcomes: []hookexecution.StageOutcome{
+			{
+				Entity: "bid-request",
+				Stage:  hooks.StageBidderRequest.String(),
+				Groups: []hookexecution.GroupOutcome{
+					{
+						InvocationResults: []hookexecution.HookOutcome{
+							{
+								HookID: hookexecution.HookID{
+									ModuleCode:   "foobar",
+									HookImplCode: "foo",
+								},
+								Status:   hookexecution.StatusSuccess,
+								Action:   hookexecution.ActionNone,
+								Warnings: []string{"warning message"},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	testCases := []struct {
+		description    string
+		expectedErrors []error
+		expectedStatus int
+		request        *openrtb2.BidRequest
+		response       *openrtb2.BidResponse
+		hookExecutor   hookexecution.HookStageExecutor
+	}{
+		{
+			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"),
+			},
+			expectedStatus: 0,
+			request:        &openrtb2.BidRequest{ID: "some-id", Test: 1},
+			response:       &openrtb2.BidResponse{ID: "some-id", Ext: json.RawMessage("...")},
+			hookExecutor:   hookExecutor,
+		},
+		{
+			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,
+			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,
+		},
+	}
+
+	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}
+
+			labels, ao = sendAuctionResponse(writer, test.hookExecutor, test.response, test.request, account, labels, ao)
+
+			assert.Equal(t, ao.Errors, test.expectedErrors, "Invalid errors.")
+			assert.Equal(t, test.expectedStatus, ao.Status, "Invalid HTTP response status.")
+		})
+	}
+}
+
 type mockStoredResponseFetcher struct {
 	data map[string]json.RawMessage
 }
@@ -4772,3 +4882,13 @@ func getIntegrationFromRequest(req *openrtb_ext.RequestWrapper) (string, error)
 	reqPrebid := reqExt.GetPrebid()
 	return reqPrebid.Integration, nil
 }
+
+type mockStageExecutor struct {
+	hookexecution.EmptyHookExecutor
+
+	outcomes []hookexecution.StageOutcome
+}
+
+func (e mockStageExecutor) GetOutcomes() []hookexecution.StageOutcome {
+	return e.outcomes
+}
diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go
index 854ca108..cf803a34 100644
--- a/endpoints/openrtb2/test_utils.go
+++ b/endpoints/openrtb2/test_utils.go
@@ -29,6 +29,7 @@ import (
 	"github.com/prebid/prebid-server/experiment/adscert"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/hooks"
+	"github.com/prebid/prebid-server/hooks/hookexecution"
 	"github.com/prebid/prebid-server/hooks/hookstage"
 	"github.com/prebid/prebid-server/metrics"
 	metricsConfig "github.com/prebid/prebid-server/metrics/config"
@@ -38,6 +39,7 @@ import (
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
 	"github.com/prebid/prebid-server/util/iputil"
 	"github.com/prebid/prebid-server/util/uuidutil"
+	jsonpatch "gopkg.in/evanphx/json-patch.v4"
 )
 
 // In this file we define:
@@ -76,7 +78,7 @@ type testCase struct {
 	// "/openrtb2/amp" endpoint JSON test info
 	storedRequest       map[string]json.RawMessage `json:"mockAmpStoredRequest"`
 	StoredResponse      map[string]json.RawMessage `json:"mockAmpStoredResponse"`
-	ExpectedAmpResponse AmpResponse                `json:"expectedAmpResponse"`
+	ExpectedAmpResponse json.RawMessage            `json:"expectedAmpResponse"`
 }
 
 type testConfigValues struct {
@@ -1474,7 +1476,7 @@ func (m mockPlanBuilder) PlanForAuctionResponseStage(_ string, _ *config.Account
 	return m.auctionResponsePlan
 }
 
-func makeRejectPlan[H any](hook H) hooks.Plan[H] {
+func makePlan[H any](hook H) hooks.Plan[H] {
 	return hooks.Plan[H]{
 		{
 			Timeout: 5 * time.Millisecond,
@@ -1544,3 +1546,115 @@ func (m mockRejectionHook) HandleRawBidderResponseHook(
 
 	return result, nil
 }
+
+var entryPointHookUpdateWithErrors = hooks.HookWrapper[hookstage.Entrypoint]{
+	Module: "foobar",
+	Code:   "foo",
+	Hook: mockUpdateHook{
+		entrypointHandler: func(
+			_ hookstage.ModuleInvocationContext,
+			payload hookstage.EntrypointPayload,
+		) (hookstage.HookResult[hookstage.EntrypointPayload], error) {
+			ch := &hookstage.ChangeSet[hookstage.EntrypointPayload]{}
+			ch.AddMutation(func(payload hookstage.EntrypointPayload) (hookstage.EntrypointPayload, error) {
+				payload.Request.Header.Add("foo", "bar")
+				return payload, nil
+			}, hookstage.MutationUpdate, "header", "foo")
+
+			return hookstage.HookResult[hookstage.EntrypointPayload]{
+				ChangeSet: ch,
+				Errors:    []string{"error 1"},
+			}, nil
+		},
+	},
+}
+
+var entryPointHookUpdateWithErrorsAndWarnings = hooks.HookWrapper[hookstage.Entrypoint]{
+	Module: "foobar",
+	Code:   "bar",
+	Hook: mockUpdateHook{
+		entrypointHandler: func(
+			_ hookstage.ModuleInvocationContext,
+			payload hookstage.EntrypointPayload,
+		) (hookstage.HookResult[hookstage.EntrypointPayload], error) {
+			ch := &hookstage.ChangeSet[hookstage.EntrypointPayload]{}
+			ch.AddMutation(func(payload hookstage.EntrypointPayload) (hookstage.EntrypointPayload, error) {
+				params := payload.Request.URL.Query()
+				params.Add("foo", "baz")
+				payload.Request.URL.RawQuery = params.Encode()
+				return payload, nil
+			}, hookstage.MutationUpdate, "param", "foo")
+
+			return hookstage.HookResult[hookstage.EntrypointPayload]{
+				ChangeSet: ch,
+				Errors:    []string{"error 1"},
+				Warnings:  []string{"warning 1"},
+			}, nil
+		},
+	},
+}
+
+var entryPointHookUpdate = hooks.HookWrapper[hookstage.Entrypoint]{
+	Module: "foobar",
+	Code:   "baz",
+	Hook: mockUpdateHook{
+		entrypointHandler: func(
+			ctx hookstage.ModuleInvocationContext,
+			payload hookstage.EntrypointPayload,
+		) (hookstage.HookResult[hookstage.EntrypointPayload], error) {
+			result := hookstage.HookResult[hookstage.EntrypointPayload]{}
+			if ctx.Endpoint != hookexecution.EndpointAuction {
+				result.Warnings = []string{fmt.Sprintf("Endpoint %s is not supported by hook.", ctx.Endpoint)}
+				return result, nil
+			}
+
+			ch := &hookstage.ChangeSet[hookstage.EntrypointPayload]{}
+			ch.AddMutation(func(payload hookstage.EntrypointPayload) (hookstage.EntrypointPayload, error) {
+				body, err := jsonpatch.MergePatch(payload.Body, []byte(`{"tmax":50}`))
+				if err == nil {
+					payload.Body = body
+				}
+				return payload, err
+			}, hookstage.MutationUpdate, "body", "tmax")
+			ch.AddMutation(func(payload hookstage.EntrypointPayload) (hookstage.EntrypointPayload, error) {
+				body, err := jsonpatch.MergePatch(payload.Body, []byte(`{"regs": {"ext": {"gdpr": 1, "us_privacy": "1NYN"}}}`))
+				if err == nil {
+					payload.Body = body
+				}
+				return payload, err
+			}, hookstage.MutationAdd, "body", "regs", "ext", "us_privacy")
+			result.ChangeSet = ch
+
+			return result, nil
+		},
+	},
+}
+
+var rawAuctionHookNone = hooks.HookWrapper[hookstage.RawAuctionRequest]{
+	Module: "vendor.module",
+	Code:   "foobar",
+	Hook:   mockUpdateHook{},
+}
+
+type mockUpdateHook struct {
+	entrypointHandler func(
+		hookstage.ModuleInvocationContext,
+		hookstage.EntrypointPayload,
+	) (hookstage.HookResult[hookstage.EntrypointPayload], error)
+}
+
+func (m mockUpdateHook) HandleEntrypointHook(
+	_ context.Context,
+	miCtx hookstage.ModuleInvocationContext,
+	payload hookstage.EntrypointPayload,
+) (hookstage.HookResult[hookstage.EntrypointPayload], error) {
+	return m.entrypointHandler(miCtx, payload)
+}
+
+func (m mockUpdateHook) HandleRawAuctionHook(
+	_ context.Context,
+	_ hookstage.ModuleInvocationContext,
+	_ hookstage.RawAuctionRequestPayload,
+) (hookstage.HookResult[hookstage.RawAuctionRequestPayload], error) {
+	return hookstage.HookResult[hookstage.RawAuctionRequestPayload]{}, nil
+}
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index d63fa6f6..88c56a1d 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -171,7 +171,7 @@ func TestCharacterEscape(t *testing.T) {
 	var errList []error
 
 	// 	4) Build bid response
-	bidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, nil, errList)
+	bidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, nil, "", errList)
 
 	// 	5) Assert we have no errors and one '&' character as we are supposed to
 	if err != nil {
@@ -1359,7 +1359,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) {
 	var errList []error
 
 	// 	4) Build bid response
-	bid_resp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, nil, errList)
+	bid_resp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, nil, "", errList)
 
 	// 	5) Assert we have no errors and the bid response we expected
 	assert.NoError(t, err, "[TestGetBidCacheInfo] buildBidResponse() threw an error")
@@ -1452,7 +1452,7 @@ func TestBidReturnsCreative(t *testing.T) {
 
 	//Run tests
 	for _, test := range testCases {
-		resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil)
+		resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil, nil, "", "")
 
 		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)
@@ -1746,7 +1746,7 @@ func TestBidResponseCurrency(t *testing.T) {
 	}
 	// Run tests
 	for i := range testCases {
-		actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, nil, errList)
+		actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, nil, "", errList)
 		assert.NoError(t, err, fmt.Sprintf("[TEST_FAILED] e.buildBidResponse resturns error in test: %s Error message: %s \n", testCases[i].description, err))
 		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))
 	}
@@ -1816,7 +1816,7 @@ func TestBidResponseImpExtInfo(t *testing.T) {
 
 	expectedBidResponseExt := `{"origbidcpm":0,"prebid":{"type":"video","passthrough":{"imp_passthrough_val":1}},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]}}`
 
-	actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, nil, nil, nil, true, impExtInfo, errList)
+	actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, nil, nil, nil, true, impExtInfo, "", errList)
 	assert.NoError(t, err, fmt.Sprintf("imp ext info was not passed through correctly: %s", err))
 
 	resBidExt := string(actualBidResp.SeatBid[0].Bid[0].Ext)
@@ -2179,7 +2179,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 	if spec.BidIDGenerator != nil {
 		*bidIdGenerator = *spec.BidIDGenerator
 	}
-	ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig, bidIdGenerator, spec.HostSChainFlag)
+	ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig, bidIdGenerator, spec.HostSChainFlag, spec.HostConfigBidValidation)
 	biddersInAuction := findBiddersInAuction(t, filename, &spec.IncomingRequest.OrtbRequest)
 	debugLog := &DebugLog{}
 	if spec.DebugLog != nil {
@@ -2197,12 +2197,22 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 		impExtInfoMap[impID] = ImpExtInfo{Passthrough: impPassthrough}
 	}
 
+	// Imp Setting for Bid Validation
+	if spec.HostConfigBidValidation.SecureMarkup == config.ValidationEnforce || spec.HostConfigBidValidation.SecureMarkup == config.ValidationWarn {
+		_, impID, err := getInfoFromImp(&openrtb_ext.RequestWrapper{BidRequest: &spec.IncomingRequest.OrtbRequest})
+		if err != nil {
+			t.Errorf("%s: Exchange returned an unexpected error. Got %s", filename, err.Error())
+		}
+		impExtInfoMap[impID] = ImpExtInfo{}
+	}
+
 	auctionRequest := AuctionRequest{
 		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &spec.IncomingRequest.OrtbRequest},
 		Account: config.Account{
 			ID:            "testaccount",
 			EventsEnabled: spec.EventsEnabled,
 			DebugAllow:    true,
+			Validations:   spec.AccountConfigBidValidation,
 		},
 		UserSyncs:     mockIdFetcher(spec.IncomingRequest.Usersyncs),
 		ImpExtInfoMap: impExtInfoMap,
@@ -2288,6 +2298,21 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 		}
 
 	}
+
+	if spec.BidValidationEnforcement == config.ValidationEnforce {
+		actualBidRespExt := &openrtb_ext.ExtBidResponse{}
+		expectedBidRespExt := &openrtb_ext.ExtBidResponse{}
+		if bid.Ext != nil {
+			if err := json.Unmarshal(bid.Ext, actualBidRespExt); err != nil {
+				assert.NoError(t, err, fmt.Sprintf("Error when unmarshalling: %s", err))
+			}
+		}
+		if err := json.Unmarshal(spec.Response.Ext, expectedBidRespExt); err != nil {
+			assert.NoError(t, err, fmt.Sprintf("Error when unmarshalling: %s", err))
+		}
+
+		assert.Equal(t, expectedBidRespExt.Errors, actualBidRespExt.Errors, "Oh no")
+	}
 }
 
 func findBiddersInAuction(t *testing.T, context string, req *openrtb2.BidRequest) []string {
@@ -2327,7 +2352,7 @@ func extractResponseTimes(t *testing.T, context string, bid *openrtb2.BidRespons
 	}
 }
 
-func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, privacyConfig config.Privacy, bidIDGenerator BidIDGenerator, hostSChainFlag bool) Exchange {
+func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, privacyConfig config.Privacy, bidIDGenerator BidIDGenerator, hostSChainFlag bool, hostBidValidation config.Validations) Exchange {
 	bidderAdapters := make(map[openrtb_ext.BidderName]AdaptedBidder, len(expectations))
 	bidderInfos := make(config.BidderInfos, len(expectations))
 	for _, bidderName := range openrtb_ext.CoreBidderNames() {
@@ -2394,22 +2419,23 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
 	}
 
 	return &exchange{
-		adapterMap:        bidderAdapters,
-		me:                metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames(), nil, nil),
-		cache:             &wellBehavedCache{},
-		cacheTime:         0,
-		currencyConverter: currency.NewRateConverter(&http.Client{}, "", time.Duration(0)),
-		gdprDefaultValue:  gdprDefaultValue,
-		gdprPermsBuilder:  gdprPermsBuilder,
-		tcf2ConfigBuilder: tcf2ConfigBuilder,
-		privacyConfig:     privacyConfig,
-		categoriesFetcher: categoriesFetcher,
-		bidderInfo:        bidderInfos,
-		bidderToSyncerKey: bidderToSyncerKey,
-		externalURL:       "http://localhost",
-		bidIDGenerator:    bidIDGenerator,
-		hostSChainNode:    hostSChainNode,
-		server:            config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "Datacenter"},
+		adapterMap:               bidderAdapters,
+		me:                       metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames(), nil, nil),
+		cache:                    &wellBehavedCache{},
+		cacheTime:                0,
+		currencyConverter:        currency.NewRateConverter(&http.Client{}, "", time.Duration(0)),
+		gdprDefaultValue:         gdprDefaultValue,
+		gdprPermsBuilder:         gdprPermsBuilder,
+		tcf2ConfigBuilder:        tcf2ConfigBuilder,
+		privacyConfig:            privacyConfig,
+		categoriesFetcher:        categoriesFetcher,
+		bidderInfo:               bidderInfos,
+		bidderToSyncerKey:        bidderToSyncerKey,
+		externalURL:              "http://localhost",
+		bidIDGenerator:           bidIDGenerator,
+		hostSChainNode:           hostSChainNode,
+		server:                   config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "Datacenter"},
+		bidValidationEnforcement: hostBidValidation,
 	}
 }
 
@@ -4212,6 +4238,238 @@ func TestCallSignHeader(t *testing.T) {
 
 }
 
+func TestValidateBannerCreativeSize(t *testing.T) {
+	exchange := exchange{bidValidationEnforcement: config.Validations{MaxCreativeWidth: 100, MaxCreativeHeight: 100},
+		me: metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames(), nil, nil),
+	}
+	testCases := []struct {
+		description                 string
+		givenBid                    *entities.PbsOrtbBid
+		givenBidResponseExt         *openrtb_ext.ExtBidResponse
+		givenBidderName             string
+		givenPubID                  string
+		expectedBannerCreativeValid bool
+	}{
+		{
+			description:                 "The dimensions are invalid, both values bigger than the max",
+			givenBid:                    &entities.PbsOrtbBid{Bid: &openrtb2.Bid{W: 200, H: 200}},
+			givenBidResponseExt:         &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)},
+			givenBidderName:             "bidder",
+			givenPubID:                  "1",
+			expectedBannerCreativeValid: false,
+		},
+		{
+			description:                 "The width is invalid, height is valid, the dimensions as a whole are invalid",
+			givenBid:                    &entities.PbsOrtbBid{Bid: &openrtb2.Bid{W: 200, H: 50}},
+			givenBidResponseExt:         &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)},
+			givenBidderName:             "bidder",
+			givenPubID:                  "1",
+			expectedBannerCreativeValid: false,
+		},
+		{
+			description:                 "The width is valid, height is invalid, the dimensions as a whole are invalid",
+			givenBid:                    &entities.PbsOrtbBid{Bid: &openrtb2.Bid{W: 50, H: 200}},
+			givenBidResponseExt:         &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)},
+			givenBidderName:             "bidder",
+			givenPubID:                  "1",
+			expectedBannerCreativeValid: false,
+		},
+		{
+			description:                 "Both width and height are valid, the dimensions are valid",
+			givenBid:                    &entities.PbsOrtbBid{Bid: &openrtb2.Bid{W: 50, H: 50}},
+			givenBidResponseExt:         &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)},
+			givenBidderName:             "bidder",
+			givenPubID:                  "1",
+			expectedBannerCreativeValid: true,
+		},
+	}
+	for _, test := range testCases {
+		acutalBannerCreativeValid := exchange.validateBannerCreativeSize(test.givenBid, test.givenBidResponseExt, openrtb_ext.BidderName(test.givenBidderName), test.givenPubID, "enforce")
+		assert.Equal(t, test.expectedBannerCreativeValid, acutalBannerCreativeValid)
+	}
+}
+
+func TestValidateBidAdM(t *testing.T) {
+	exchange := exchange{bidValidationEnforcement: config.Validations{MaxCreativeWidth: 100, MaxCreativeHeight: 100},
+		me: metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames(), nil, nil),
+	}
+	testCases := []struct {
+		description         string
+		givenBid            *entities.PbsOrtbBid
+		givenBidResponseExt *openrtb_ext.ExtBidResponse
+		givenBidderName     string
+		givenPubID          string
+		expectedBidAdMValid bool
+	}{
+		{
+			description:         "The adm of the bid contains insecure string and no secure string, adm is invalid",
+			givenBid:            &entities.PbsOrtbBid{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid"}},
+			givenBidResponseExt: &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)},
+			givenBidderName:     "bidder",
+			givenPubID:          "1",
+			expectedBidAdMValid: false,
+		},
+		{
+			description:         "The adm has both an insecure and secure string defined and therefore the adm is valid",
+			givenBid:            &entities.PbsOrtbBid{Bid: &openrtb2.Bid{AdM: "http://www.foo.com https://www.bar.com"}},
+			givenBidResponseExt: &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)},
+			givenBidderName:     "bidder",
+			givenPubID:          "1",
+			expectedBidAdMValid: true,
+		},
+		{
+			description:         "The adm has both an insecure and secure string defined and therefore the adm is valid",
+			givenBid:            &entities.PbsOrtbBid{Bid: &openrtb2.Bid{AdM: "http%3A//www.foo.com https%3A//www.bar.com"}},
+			givenBidResponseExt: &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)},
+			givenBidderName:     "bidder",
+			givenPubID:          "1",
+			expectedBidAdMValid: true,
+		},
+		{
+			description:         "The adm of the bid are valid with a secure string",
+			givenBid:            &entities.PbsOrtbBid{Bid: &openrtb2.Bid{AdM: "https://domain.com/valid"}},
+			givenBidResponseExt: &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)},
+			givenBidderName:     "bidder",
+			givenPubID:          "1",
+			expectedBidAdMValid: true,
+		},
+	}
+	for _, test := range testCases {
+		actualBidAdMValid := exchange.validateBidAdM(test.givenBid, test.givenBidResponseExt, openrtb_ext.BidderName(test.givenBidderName), test.givenPubID, "enforce")
+		assert.Equal(t, test.expectedBidAdMValid, actualBidAdMValid)
+
+	}
+}
+
+func TestMakeBidWithValidation(t *testing.T) {
+	sampleAd := "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST ...></VAST>"
+	sampleOpenrtbBid := &openrtb2.Bid{ID: "some-bid-id", AdM: sampleAd}
+
+	// Define test cases
+	testCases := []struct {
+		description       string
+		givenValidations  config.Validations
+		givenBids         []*entities.PbsOrtbBid
+		expectedNumOfBids int
+	}{
+		{
+			description:       "Validation is enforced, and one bid out of the two is invalid based on dimensions",
+			givenValidations:  config.Validations{BannerCreativeMaxSize: config.ValidationEnforce, MaxCreativeWidth: 100, MaxCreativeHeight: 100},
+			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}},
+			expectedNumOfBids: 1,
+		},
+		{
+			description:       "Validation is warned, so no bids should be removed (Validating CreativeMaxSize) ",
+			givenValidations:  config.Validations{BannerCreativeMaxSize: config.ValidationWarn, MaxCreativeWidth: 100, MaxCreativeHeight: 100},
+			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}},
+			expectedNumOfBids: 2,
+		},
+		{
+			description:       "Validation is enforced, and one bid out of the two is invalid based on AdM",
+			givenValidations:  config.Validations{SecureMarkup: config.ValidationEnforce},
+			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}},
+			expectedNumOfBids: 1,
+		},
+		{
+			description:       "Validation is warned so no bids should be removed (Validating SecureMarkup)",
+			givenValidations:  config.Validations{SecureMarkup: config.ValidationWarn},
+			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}},
+			expectedNumOfBids: 2,
+		},
+		{
+			description:       "Adm validation is skipped, creative size validation is enforced, one Adm is invalid, but because we skip, no bids should be removed",
+			givenValidations:  config.Validations{SecureMarkup: config.ValidationSkip, BannerCreativeMaxSize: config.ValidationEnforce},
+			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}},
+			expectedNumOfBids: 2,
+		},
+		{
+			description:       "Creative Size Validation is skipped, Adm Validation is enforced, one Creative Size is invalid, but because we skip, no bids should be removed",
+			givenValidations:  config.Validations{BannerCreativeMaxSize: config.ValidationWarn, MaxCreativeWidth: 100, MaxCreativeHeight: 100},
+			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}},
+			expectedNumOfBids: 2,
+		},
+	}
+
+	// Test set up
+	sampleAuction := &auction{cacheIds: map[*openrtb2.Bid]string{sampleOpenrtbBid: "CACHE_UUID_1234"}}
+
+	noBidHandler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) }
+	server := httptest.NewServer(http.HandlerFunc(noBidHandler))
+	defer server.Close()
+
+	bidderImpl := &goodSingleBidder{
+		httpRequest: &adapters.RequestData{
+			Method:  "POST",
+			Uri:     server.URL,
+			Body:    []byte("{\"key\":\"val\"}"),
+			Headers: http.Header{},
+		},
+		bidResponse: &adapters.BidderResponse{},
+	}
+	e := new(exchange)
+	e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{
+		openrtb_ext.BidderAppnexus: AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderAppnexus, nil, ""),
+	}
+	e.cache = &wellBehavedCache{}
+	e.me = &metricsConf.NilMetricsEngine{}
+
+	e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
+
+	bidExtResponse := &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)}
+
+	ImpExtInfoMap := make(map[string]ImpExtInfo)
+	ImpExtInfoMap["1"] = ImpExtInfo{}
+	ImpExtInfoMap["2"] = ImpExtInfo{}
+
+	//Run tests
+	for _, test := range testCases {
+		e.bidValidationEnforcement = test.givenValidations
+		sampleBids := test.givenBids
+		resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, true, ImpExtInfoMap, bidExtResponse, "", "")
+
+		assert.Equal(t, 0, len(resultingErrs), "%s. Test should not return errors \n", test.description)
+		assert.Equal(t, test.expectedNumOfBids, len(resultingBids), "%s. Test returns more valid bids than expected\n", test.description)
+	}
+}
+
+func TestSetBidValidationStatus(t *testing.T) {
+	testCases := []struct {
+		description  string
+		givenHost    config.Validations
+		givenAccount config.Validations
+		expected     config.Validations
+	}{
+		{
+			description:  "Host configuration is different than account, account setting should be preferred (enforce)",
+			givenHost:    config.Validations{BannerCreativeMaxSize: config.ValidationSkip, SecureMarkup: config.ValidationSkip},
+			givenAccount: config.Validations{BannerCreativeMaxSize: config.ValidationEnforce, SecureMarkup: config.ValidationEnforce},
+			expected:     config.Validations{BannerCreativeMaxSize: config.ValidationEnforce, SecureMarkup: config.ValidationSkip},
+		},
+		{
+			description:  "Host configuration is different than account, account setting should be preferred (warn)",
+			givenHost:    config.Validations{BannerCreativeMaxSize: config.ValidationEnforce, SecureMarkup: config.ValidationEnforce},
+			givenAccount: config.Validations{BannerCreativeMaxSize: config.ValidationWarn, SecureMarkup: config.ValidationWarn},
+			expected:     config.Validations{BannerCreativeMaxSize: config.ValidationWarn, SecureMarkup: config.ValidationEnforce},
+		},
+		{
+			description:  "Host configuration is different than account, account setting should be preferred (skip)",
+			givenHost:    config.Validations{BannerCreativeMaxSize: config.ValidationWarn, SecureMarkup: config.ValidationWarn},
+			givenAccount: config.Validations{BannerCreativeMaxSize: config.ValidationSkip, SecureMarkup: config.ValidationSkip},
+			expected:     config.Validations{BannerCreativeMaxSize: config.ValidationSkip, SecureMarkup: config.ValidationWarn},
+		},
+		{
+			description:  "No account confiugration given, host confg should be preferred",
+			givenHost:    config.Validations{BannerCreativeMaxSize: config.ValidationSkip, SecureMarkup: config.ValidationSkip},
+			givenAccount: config.Validations{},
+			expected:     config.Validations{BannerCreativeMaxSize: config.ValidationSkip, SecureMarkup: config.ValidationSkip},
+		},
+	}
+	for _, test := range testCases {
+		test.givenHost.SetBannerCreativeMaxSize(test.givenAccount)
+		assert.Equal(t, test.expected, test.givenHost)
+	}
+}
+
 /*
 TestOverrideConfigAlternateBidderCodesWithRequestValues makes sure that the correct alternabiddercodes list is forwarded to the adapters and only the approved bids are returned in auction response.
 
@@ -4402,20 +4660,23 @@ func (ms *MockSigner) Sign(destinationURL string, body []byte) (string, error) {
 }
 
 type exchangeSpec struct {
-	GDPREnabled       bool                   `json:"gdpr_enabled"`
-	IncomingRequest   exchangeRequest        `json:"incomingRequest"`
-	OutgoingRequests  map[string]*bidderSpec `json:"outgoingRequests"`
-	Response          exchangeResponse       `json:"response,omitempty"`
-	EnforceCCPA       bool                   `json:"enforceCcpa"`
-	EnforceLMT        bool                   `json:"enforceLmt"`
-	AssumeGDPRApplies bool                   `json:"assume_gdpr_applies"`
-	DebugLog          *DebugLog              `json:"debuglog,omitempty"`
-	EventsEnabled     bool                   `json:"events_enabled,omitempty"`
-	StartTime         int64                  `json:"start_time_ms,omitempty"`
-	BidIDGenerator    *mockBidIDGenerator    `json:"bidIDGenerator,omitempty"`
-	RequestType       *metrics.RequestType   `json:"requestType,omitempty"`
-	PassthroughFlag   bool                   `json:"passthrough_flag,omitempty"`
-	HostSChainFlag    bool                   `json:"host_schain_flag,omitempty"`
+	GDPREnabled                bool                   `json:"gdpr_enabled"`
+	IncomingRequest            exchangeRequest        `json:"incomingRequest"`
+	OutgoingRequests           map[string]*bidderSpec `json:"outgoingRequests"`
+	Response                   exchangeResponse       `json:"response,omitempty"`
+	EnforceCCPA                bool                   `json:"enforceCcpa"`
+	EnforceLMT                 bool                   `json:"enforceLmt"`
+	AssumeGDPRApplies          bool                   `json:"assume_gdpr_applies"`
+	DebugLog                   *DebugLog              `json:"debuglog,omitempty"`
+	EventsEnabled              bool                   `json:"events_enabled,omitempty"`
+	StartTime                  int64                  `json:"start_time_ms,omitempty"`
+	BidIDGenerator             *mockBidIDGenerator    `json:"bidIDGenerator,omitempty"`
+	RequestType                *metrics.RequestType   `json:"requestType,omitempty"`
+	PassthroughFlag            bool                   `json:"passthrough_flag,omitempty"`
+	HostSChainFlag             bool                   `json:"host_schain_flag,omitempty"`
+	BidValidationEnforcement   string                 `json:"bid_validation_flag,omitempty"`
+	HostConfigBidValidation    config.Validations     `json:"host_bid_validations"`
+	AccountConfigBidValidation config.Validations     `json:"account_bid_validations"`
 }
 
 type exchangeRequest struct {
diff --git a/exchange/exchangetest/bid_response_validation_enforce_creative.json b/exchange/exchangetest/bid_response_validation_enforce_creative.json
new file mode 100644
index 00000000..0c8f1467
--- /dev/null
+++ b/exchange/exchangetest/bid_response_validation_enforce_creative.json
@@ -0,0 +1,189 @@
+{
+    "host_bid_validations": {
+        "banner_creative_max_size": "skip",
+        "secure_markup": "skip",
+        "max_creative_width": 100,
+        "max_creative_height": 100
+    },
+    "account_bid_validations": {
+        "banner_creative_max_size": "enforce"
+    },
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [
+                {
+                    "id": "some-imp-id",
+                    "banner": {
+                        "format": [
+                            {
+                                "w": 600,
+                                "h": 500
+                            },
+                            {
+                                "w": 300,
+                                "h": 600
+                            }
+                        ]
+                    },
+                    "ext": {
+                        "prebid": {
+                            "bidder": {
+                                "appnexus": {
+                                    "placementId": 1
+                                },
+                                "rubicon": {
+                                    "accountId": 1,
+                                    "siteId": 2,
+                                    "zoneId": 3
+                                }
+                            }
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "some-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 600,
+                                        "h": 500
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": 1
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "apn-bid",
+                                    "impid": "some-imp-id",
+                                    "price": 0.3,
+                                    "w": 200,
+                                    "h": 500,
+                                    "crid": "creative-1"
+                                },
+                                "bidType": "banner"
+                            }
+                        ],
+                        "seat": "appnexus"
+                    }
+                ]
+            }
+        },
+        "rubicon": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "some-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 600,
+                                        "h": 500
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "accountId": 1,
+                                    "siteId": 2,
+                                    "zoneId": 3
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "rubi-bid",
+                                    "impid": "some-imp-id",
+                                    "price": 0.4,
+                                    "w": 200,
+                                    "h": 500,
+                                    "crid": "creative-2"
+                                },
+                                "bidType": "banner"
+                            }
+                        ],
+                        "seat": "rubicon"
+                    }
+                ]
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [
+                {
+                    "seat": "appnexus",
+                    "bid": []
+                },
+                {
+                    "seat": "rubicon",
+                    "bid": []
+                }
+            ]
+        },
+        "ext": {
+            "errors": {
+                "appnexus": [
+                    {
+                        "code": 4,
+                        "message": "bidResponse rejected: size WxH"
+                    }
+                ],
+                "rubicon": [
+                    {
+                        "code": 4,
+                        "message": "bidResponse rejected: size WxH"
+                    }
+                ]
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/bid_response_validation_enforce_one_bid_rejected.json b/exchange/exchangetest/bid_response_validation_enforce_one_bid_rejected.json
new file mode 100644
index 00000000..ea48f3a9
--- /dev/null
+++ b/exchange/exchangetest/bid_response_validation_enforce_one_bid_rejected.json
@@ -0,0 +1,195 @@
+{
+    "host_bid_validations": {
+        "banner_creative_max_size": "enforce",
+        "secure_markup": "skip",
+        "max_creative_width": 100,
+        "max_creative_height": 100
+    },
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [
+                {
+                    "id": "some-imp-id",
+                    "banner": {
+                        "format": [
+                            {
+                                "w": 600,
+                                "h": 500
+                            },
+                            {
+                                "w": 300,
+                                "h": 600
+                            }
+                        ]
+                    },
+                    "ext": {
+                        "prebid": {
+                            "bidder": {
+                                "appnexus": {
+                                    "placementId": 1
+                                },
+                                "rubicon": {
+                                    "accountId": 1,
+                                    "siteId": 2,
+                                    "zoneId": 3
+                                }
+                            }
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "some-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 600,
+                                        "h": 500
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": 1
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "apn-bid",
+                                    "impid": "some-imp-id",
+                                    "price": 0.3,
+                                    "w": 200,
+                                    "h": 500,
+                                    "crid": "creative-1"
+                                },
+                                "bidType": "banner"
+                            }
+                        ],
+                        "seat": "appnexus"
+                    }
+                ]
+            }
+        },
+        "rubicon": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "some-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 600,
+                                        "h": 500
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "accountId": 1,
+                                    "siteId": 2,
+                                    "zoneId": 3
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "rubi-bid",
+                                    "impid": "some-imp-id",
+                                    "price": 0.4,
+                                    "w": 20,
+                                    "h": 50,
+                                    "crid": "creative-2"
+                                },
+                                "bidType": "banner"
+                            }
+                        ],
+                        "seat": "rubicon"
+                    }
+                ]
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [
+                {
+                    "seat": "appnexus",
+                    "bid": []
+                },
+                {
+                    "seat": "rubicon",
+                    "bid": [
+                        {
+                            "id": "rubi-bid",
+                            "impid": "some-imp-id",
+                            "price": 0.4,
+                            "w": 20,
+                            "h": 50,
+                            "crid": "creative-2",
+                            "ext": {
+                                "origbidcpm": 0.4,
+                                "prebid": {
+                                    "type": "banner"
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        },
+        "ext": {
+            "errors": {
+                "appnexus": [
+                    {
+                        "code": 4,
+                        "message": "bidResponse rejected: size WxH"
+                    }
+                ]
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/bid_response_validation_enforce_secure.json b/exchange/exchangetest/bid_response_validation_enforce_secure.json
new file mode 100644
index 00000000..e036f82a
--- /dev/null
+++ b/exchange/exchangetest/bid_response_validation_enforce_secure.json
@@ -0,0 +1,188 @@
+{
+    "host_bid_validations": {
+        "banner_creative_max_size": "skip",
+        "secure_markup": "enforce",
+        "max_creative_width": 100,
+        "max_creative_height": 100
+    },
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [
+                {
+                    "id": "some-imp-id",
+                    "banner": {
+                        "format": [
+                            {
+                                "w": 600,
+                                "h": 500
+                            },
+                            {
+                                "w": 300,
+                                "h": 600
+                            }
+                        ]
+                    },
+                    "ext": {
+                        "prebid": {
+                            "bidder": {
+                                "appnexus": {
+                                    "placementId": 1
+                                },
+                                "rubicon": {
+                                    "accountId": 1,
+                                    "siteId": 2,
+                                    "zoneId": 3
+                                }
+                            }
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "some-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 600,
+                                        "h": 500
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": 1
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "apn-bid",
+                                    "impid": "some-imp-id",
+                                    "price": 0.3,
+                                    "w": 20,
+                                    "h": 50,
+                                    "adm": "http:",
+                                    "crid": "creative-1"
+                                },
+                                "bidType": "banner"
+                            }
+                        ],
+                        "seat": "appnexus"
+                    }
+                ]
+            }
+        },
+        "rubicon": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "some-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 600,
+                                        "h": 500
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "accountId": 1,
+                                    "siteId": 2,
+                                    "zoneId": 3
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "rubi-bid",
+                                    "impid": "some-imp-id",
+                                    "price": 0.4,
+                                    "w": 20,
+                                    "h": 50,
+                                    "adm": "http:",
+                                    "crid": "creative-2"
+                                },
+                                "bidType": "banner"
+                            }
+                        ],
+                        "seat": "rubicon"
+                    }
+                ]
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [
+                {
+                    "seat": "appnexus",
+                    "bid": []
+                },
+                {
+                    "seat": "rubicon",
+                    "bid": []
+                }
+            ]
+        },
+        "ext": {
+            "errors": {
+                "appnexus": [
+                    {
+                        "code": 4,
+                        "message": "bidResponse rejected: insecure creative in secure context"
+                    }
+                ],
+                "rubicon": [
+                    {
+                        "code": 4,
+                        "message": "bidResponse rejected: insecure creative in secure context"
+                    }
+                ]
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/bid_response_validation_warn_creative.json b/exchange/exchangetest/bid_response_validation_warn_creative.json
new file mode 100644
index 00000000..a170eac7
--- /dev/null
+++ b/exchange/exchangetest/bid_response_validation_warn_creative.json
@@ -0,0 +1,216 @@
+{
+    "host_bid_validations": {
+        "banner_creative_max_size": "warn",
+        "secure_markup": "skip",
+        "max_creative_width": 100,
+        "max_creative_height": 100
+    },
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [
+                {
+                    "id": "some-imp-id",
+                    "banner": {
+                        "format": [
+                            {
+                                "w": 600,
+                                "h": 500
+                            },
+                            {
+                                "w": 300,
+                                "h": 600
+                            }
+                        ]
+                    },
+                    "ext": {
+                        "prebid": {
+                            "bidder": {
+                                "appnexus": {
+                                    "placementId": 1
+                                },
+                                "rubicon": {
+                                    "accountId": 1,
+                                    "siteId": 2,
+                                    "zoneId": 3
+                                }
+                            }
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "some-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 600,
+                                        "h": 500
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": 1
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "apn-bid",
+                                    "impid": "some-imp-id",
+                                    "price": 0.3,
+                                    "w": 200,
+                                    "h": 500,
+                                    "crid": "creative-1"
+                                },
+                                "bidType": "banner"
+                            }
+                        ],
+                        "seat": "appnexus"
+                    }
+                ]
+            }
+        },
+        "rubicon": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "some-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 600,
+                                        "h": 500
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "accountId": 1,
+                                    "siteId": 2,
+                                    "zoneId": 3
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "rubi-bid",
+                                    "impid": "some-imp-id",
+                                    "price": 0.4,
+                                    "w": 200,
+                                    "h": 500,
+                                    "crid": "creative-2"
+                                },
+                                "bidType": "banner"
+                            }
+                        ],
+                        "seat": "rubicon"
+                    }
+                ]
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [
+                {
+                    "seat": "appnexus",
+                    "bid": [
+                        {
+                            "id": "apn-bid",
+                            "impid": "some-imp-id",
+                            "price": 0.3,
+                            "w": 200,
+                            "h": 500,
+                            "crid": "creative-1",
+                            "ext": {
+                                "origbidcpm": 0.3,
+                                "prebid": {
+                                    "type": "banner"
+                                }
+                            }
+                        }
+                    ]
+                },
+                {
+                    "seat": "rubicon",
+                    "bid": [
+                        {
+                            "id": "rubi-bid",
+                            "impid": "some-imp-id",
+                            "price": 0.4,
+                            "w": 200,
+                            "h": 500,
+                            "crid": "creative-2",
+                            "ext": {
+                                "origbidcpm": 0.4,
+                                "prebid": {
+                                    "type": "banner"
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        },
+        "ext": {
+            "errors": {
+                "appnexus": [
+                    {
+                        "code": 4,
+                        "message": "bidResponse creative size warning: size WxH larger than AdUnit sizes"
+                    }
+                ],
+                "rubicon": [
+                    {
+                        "code": 4,
+                        "message": "bidResponse creative size warning: size WxH larger than AdUnit sizes"
+                    }
+                ]
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/bid_response_validation_warn_secure.json b/exchange/exchangetest/bid_response_validation_warn_secure.json
new file mode 100644
index 00000000..a6b208d2
--- /dev/null
+++ b/exchange/exchangetest/bid_response_validation_warn_secure.json
@@ -0,0 +1,220 @@
+{
+    "host_bid_validations": {
+        "banner_creative_max_size": "skip",
+        "secure_markup": "warn",
+        "max_creative_width": 100,
+        "max_creative_height": 100
+    },
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [
+                {
+                    "id": "some-imp-id",
+                    "banner": {
+                        "format": [
+                            {
+                                "w": 600,
+                                "h": 500
+                            },
+                            {
+                                "w": 300,
+                                "h": 600
+                            }
+                        ]
+                    },
+                    "ext": {
+                        "prebid": {
+                            "bidder": {
+                                "appnexus": {
+                                    "placementId": 1
+                                },
+                                "rubicon": {
+                                    "accountId": 1,
+                                    "siteId": 2,
+                                    "zoneId": 3
+                                }
+                            }
+                        }
+                    }
+                }
+            ]
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "some-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 600,
+                                        "h": 500
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": 1
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "apn-bid",
+                                    "impid": "some-imp-id",
+                                    "price": 0.3,
+                                    "w": 20,
+                                    "h": 50,
+                                    "adm": "http:",
+                                    "crid": "creative-1"
+                                },
+                                "bidType": "banner"
+                            }
+                        ],
+                        "seat": "appnexus"
+                    }
+                ]
+            }
+        },
+        "rubicon": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [
+                        {
+                            "id": "some-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 600,
+                                        "h": 500
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "accountId": 1,
+                                    "siteId": 2,
+                                    "zoneId": 3
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [
+                    {
+                        "pbsBids": [
+                            {
+                                "ortbBid": {
+                                    "id": "rubi-bid",
+                                    "impid": "some-imp-id",
+                                    "price": 0.4,
+                                    "w": 20,
+                                    "h": 50,
+                                    "adm": "http:",
+                                    "crid": "creative-2"
+                                },
+                                "bidType": "banner"
+                            }
+                        ],
+                        "seat": "rubicon"
+                    }
+                ]
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [
+                {
+                    "seat": "appnexus",
+                    "bid": [
+                        {
+                            "id": "apn-bid",
+                            "impid": "some-imp-id",
+                            "price": 0.3,
+                            "w": 20,
+                            "h": 50,
+                            "adm": "http:",
+                            "crid": "creative-1",
+                            "ext": {
+                                "origbidcpm": 0.3,
+                                "prebid": {
+                                    "type": "banner"
+                                }
+                            }
+                        }
+                    ]
+                },
+                {
+                    "seat": "rubicon",
+                    "bid": [
+                        {
+                            "id": "rubi-bid",
+                            "impid": "some-imp-id",
+                            "price": 0.4,
+                            "w": 20,
+                            "h": 50,
+                            "adm": "http:",
+                            "crid": "creative-2",
+                            "ext": {
+                                "origbidcpm": 0.4,
+                                "prebid": {
+                                    "type": "banner"
+                                }
+                            }
+                        }
+                    ]
+                }
+            ]
+        },
+        "ext": {
+            "errors": {
+                "appnexus": [
+                    {
+                        "code": 4,
+                        "message": "bidResponse warning: insecure creative in secure context"
+                    }
+                ],
+                "rubicon": [
+                    {
+                        "code": 4,
+                        "message": "bidResponse warning: insecure creative in secure context"
+                    }
+                ]
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/hooks/hookexecution/enricher_test.go b/hooks/hookexecution/enricher_test.go
new file mode 100644
index 00000000..ebe86c2c
--- /dev/null
+++ b/hooks/hookexecution/enricher_test.go
@@ -0,0 +1,302 @@
+package hookexecution
+
+import (
+	"encoding/json"
+	"errors"
+	"os"
+	"testing"
+
+	"github.com/prebid/openrtb/v17/openrtb2"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/hooks/hookanalytics"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// StageOutcomeTest is used for test purpose instead of the original structure,
+// so we can unmarshal hidden fields such as StageOutcomeTest.Stage and HookOutcomeTest.Errors/Warnings
+type StageOutcomeTest struct {
+	ExecutionTime
+	Entity entity             `json:"entity"`
+	Groups []GroupOutcomeTest `json:"groups"`
+	Stage  string             `json:"stage"`
+}
+
+type GroupOutcomeTest struct {
+	ExecutionTime
+	InvocationResults []HookOutcomeTest `json:"invocation_results"`
+}
+
+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"`
+}
+
+func TestEnrichBidResponse(t *testing.T) {
+	testCases := []struct {
+		description             string
+		expectedWarnings        []error
+		expectedBidResponseFile string
+		stageOutcomesFile       string
+		bidResponse             *openrtb2.BidResponse
+		bidRequest              *openrtb2.BidRequest
+		account                 *config.Account
+	}{
+		{
+			description:             "BidResponse enriched with verbose trace and debug info when bidRequest.test=1 and trace=verbose",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/complete-stage-outcomes/expected-verbose-debug-response.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidResponse:             &openrtb2.BidResponse{Ext: []byte(`{"prebid": {"foo": "bar"}}`)},
+			bidRequest:              &openrtb2.BidRequest{Test: 1, Ext: []byte(`{"prebid": {"trace": "verbose"}}`)},
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description:             "BidResponse enriched with basic trace and debug info when bidRequest.ext.prebid.debug=true and trace=basic",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/complete-stage-outcomes/expected-basic-debug-response.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidResponse:             &openrtb2.BidResponse{Ext: []byte(`{"prebid": {"foo": "bar"}}`)},
+			bidRequest:              &openrtb2.BidRequest{Ext: []byte(`{"prebid": {"debug": true, "trace": "basic"}}`)},
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description:             "BidResponse enriched with debug info when bidRequest.ext.prebid.debug=true",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/complete-stage-outcomes/expected-debug-response.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidResponse:             &openrtb2.BidResponse{Ext: []byte(`{"prebid": {"foo": "bar"}}`)},
+			bidRequest:              &openrtb2.BidRequest{Ext: []byte(`{"prebid": {"debug": true, "trace": ""}}`)},
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description:             "BidResponse not enriched when bidRequest.ext.prebid.debug=false",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/complete-stage-outcomes/expected-empty-response.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidResponse:             &openrtb2.BidResponse{Ext: []byte(`{"prebid": {"foo": "bar"}}`)},
+			bidRequest:              &openrtb2.BidRequest{},
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description:             "BidResponse enriched only with verbose trace when bidRequest.ext.prebid.trace=verbose and account.DebugAllow=false",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/complete-stage-outcomes/expected-verbose-response.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidResponse:             &openrtb2.BidResponse{Ext: []byte(`{"prebid": {"foo": "bar"}}`)},
+			bidRequest:              &openrtb2.BidRequest{Test: 1, Ext: []byte(`{"prebid": {"debug": true, "trace": "verbose"}}`)},
+			account:                 &config.Account{DebugAllow: false},
+		},
+		{
+			description:             "BidResponse enriched with debug info if bidResponse.Ext is nil",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/complete-stage-outcomes/expected-pure-debug-response.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidResponse:             &openrtb2.BidResponse{},
+			bidRequest:              &openrtb2.BidRequest{Test: 1},
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description:             "BidResponse not enriched with modules if stage outcome groups empty",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/empty-stage-outcomes/empty.json",
+			stageOutcomesFile:       "test/empty-stage-outcomes/empty-stage-outcomes-v1.json",
+			bidResponse:             &openrtb2.BidResponse{},
+			bidRequest:              &openrtb2.BidRequest{Test: 1},
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description:             "BidResponse not enriched with modules if stage outcomes empty",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/empty-stage-outcomes/empty.json",
+			stageOutcomesFile:       "test/empty-stage-outcomes/empty-stage-outcomes-v2.json",
+			bidResponse:             &openrtb2.BidResponse{},
+			bidRequest:              &openrtb2.BidRequest{Test: 1},
+			account:                 &config.Account{DebugAllow: true},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.description, func(t *testing.T) {
+			expectedResponse := readFile(t, test.expectedBidResponseFile)
+			stageOutcomes := getStageOutcomes(t, test.stageOutcomesFile)
+
+			ext, warns, err := EnrichExtBidResponse(test.bidResponse.Ext, stageOutcomes, test.bidRequest, test.account)
+			require.NoError(t, err, "Failed to enrich BidResponse with hook debug information: %s", err)
+			assert.Equal(t, test.expectedWarnings, warns, "Unexpected warnings")
+
+			test.bidResponse.Ext = ext
+			if test.bidResponse.Ext == nil {
+				assert.Empty(t, expectedResponse)
+			} else {
+				assert.JSONEq(t, string(expectedResponse), string(test.bidResponse.Ext))
+			}
+		})
+	}
+}
+
+func TestGetModulesJSON(t *testing.T) {
+	testCases := []struct {
+		description             string
+		expectedWarnings        []error
+		expectedBidResponseFile string
+		stageOutcomesFile       string
+		bidRequest              *openrtb2.BidRequest
+		account                 *config.Account
+	}{
+		{
+			description:             "Modules Outcome contains verbose trace and debug info when bidRequest.test=1 and trace=verbose",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/complete-stage-outcomes/expected-verbose-debug-response.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidRequest:              &openrtb2.BidRequest{Test: 1, Ext: []byte(`{"prebid": {"trace": "verbose"}}`)},
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description:             "Modules Outcome contains verbose trace and debug info when bidRequest.test=1 and trace=verbose and account is not defined",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/complete-stage-outcomes/expected-verbose-debug-response.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidRequest:              &openrtb2.BidRequest{Test: 1, Ext: []byte(`{"prebid": {"trace": "verbose"}}`)},
+			account:                 nil,
+		},
+		{
+			description:             "Modules Outcome contains basic trace and debug info when bidRequest.ext.prebid.debug=true and trace=basic",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/complete-stage-outcomes/expected-basic-debug-response.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidRequest:              &openrtb2.BidRequest{Ext: []byte(`{"prebid": {"debug": true, "trace": "basic"}}`)},
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description:             "Modules Outcome contains debug info when bidRequest.ext.prebid.debug=true",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/complete-stage-outcomes/expected-debug-response.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidRequest:              &openrtb2.BidRequest{Ext: []byte(`{"prebid": {"debug": true, "trace": ""}}`)},
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description:             "Modules Outcome empty when bidRequest.ext.prebid.debug=false",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/empty-stage-outcomes/empty.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidRequest:              &openrtb2.BidRequest{},
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description:             "Modules Outcome empty when bidRequest is nil",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/empty-stage-outcomes/empty.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidRequest:              nil,
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description:             "Modules Outcome contains only verbose trace when bidRequest.ext.prebid.trace=verbose and account.DebugAllow=false",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/complete-stage-outcomes/expected-verbose-response.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidRequest:              &openrtb2.BidRequest{Test: 1, Ext: []byte(`{"prebid": {"debug": true, "trace": "verbose"}}`)},
+			account:                 &config.Account{DebugAllow: false},
+		},
+		{
+			description:             "Modules Outcome contains debug info if bidResponse.Ext is nil",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/complete-stage-outcomes/expected-pure-debug-response.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidRequest:              &openrtb2.BidRequest{Test: 1},
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description:             "Modules Outcome empty if stage outcome groups empty",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/empty-stage-outcomes/empty.json",
+			stageOutcomesFile:       "test/empty-stage-outcomes/empty-stage-outcomes-v1.json",
+			bidRequest:              &openrtb2.BidRequest{Test: 1},
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description:             "Modules Outcome empty if stage outcomes empty",
+			expectedWarnings:        nil,
+			expectedBidResponseFile: "test/empty-stage-outcomes/empty.json",
+			stageOutcomesFile:       "test/empty-stage-outcomes/empty-stage-outcomes-v2.json",
+			bidRequest:              &openrtb2.BidRequest{Test: 1},
+			account:                 &config.Account{DebugAllow: true},
+		},
+		{
+			description: "Warnings returned if debug info invalid",
+			expectedWarnings: []error{
+				errors.New("Value is not a string: 1"),
+				errors.New("Value is not a boolean: active"),
+			},
+			expectedBidResponseFile: "test/complete-stage-outcomes/expected-pure-debug-response.json",
+			stageOutcomesFile:       "test/complete-stage-outcomes/stage-outcomes.json",
+			bidRequest:              &openrtb2.BidRequest{Test: 1, Ext: json.RawMessage(`{"prebid": {"debug": "active", "trace": 1}}`)},
+			account:                 &config.Account{DebugAllow: true},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.description, func(t *testing.T) {
+			expectedResponse := readFile(t, test.expectedBidResponseFile)
+			stageOutcomes := getStageOutcomes(t, test.stageOutcomesFile)
+
+			modules, warns, err := GetModulesJSON(stageOutcomes, test.bidRequest, test.account)
+			require.NoError(t, err, "Failed to get modules outcome as json: %s", err)
+			assert.Equal(t, test.expectedWarnings, warns, "Unexpected warnings")
+
+			if modules == nil {
+				assert.Empty(t, expectedResponse)
+			} else {
+				var expectedExtBidResponse openrtb_ext.ExtBidResponse
+				err := json.Unmarshal(expectedResponse, &expectedExtBidResponse)
+				assert.NoError(t, err, "Failed to unmarshal prebid response extension")
+				assert.JSONEq(t, string(expectedExtBidResponse.Prebid.Modules), string(modules))
+			}
+		})
+	}
+}
+
+func getStageOutcomes(t *testing.T, file string) []StageOutcome {
+	var stageOutcomes []StageOutcome
+	var stageOutcomesTest []StageOutcomeTest
+
+	data := readFile(t, file)
+	err := json.Unmarshal(data, &stageOutcomesTest)
+	require.NoError(t, err, "Failed to unmarshal stage outcomes: %s", err)
+
+	for _, stageT := range stageOutcomesTest {
+		stage := StageOutcome{
+			ExecutionTime: stageT.ExecutionTime,
+			Entity:        stageT.Entity,
+			Stage:         stageT.Stage,
+		}
+
+		for _, groupT := range stageT.Groups {
+			group := GroupOutcome{ExecutionTime: groupT.ExecutionTime}
+			for _, hookT := range groupT.InvocationResults {
+				group.InvocationResults = append(group.InvocationResults, HookOutcome(hookT))
+			}
+
+			stage.Groups = append(stage.Groups, group)
+		}
+		stageOutcomes = append(stageOutcomes, stage)
+	}
+
+	return stageOutcomes
+}
+
+func readFile(t *testing.T, filename string) []byte {
+	data, err := os.ReadFile(filename)
+	require.NoError(t, err, "Failed to read file %s: %v", filename, err)
+	return data
+}
diff --git a/hooks/hookexecution/outcome_test.go b/hooks/hookexecution/outcome_test.go
deleted file mode 100644
index de81d7b0..00000000
--- a/hooks/hookexecution/outcome_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package hookexecution
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func assertEqualStageOutcomes(t *testing.T, expected StageOutcome, actual StageOutcome) {
-	t.Helper()
-
-	assert.Equal(t, len(actual.Groups), len(expected.Groups), "Stage outcomes contain different number of groups")
-
-	// calculate expected timings from actual outcome
-	for i, group := range actual.Groups {
-		expected.ExecutionTimeMillis += group.ExecutionTimeMillis
-		for _, hook := range group.InvocationResults {
-			if hook.ExecutionTimeMillis > expected.Groups[i].ExecutionTimeMillis {
-				expected.Groups[i].ExecutionTimeMillis = hook.ExecutionTimeMillis
-			}
-		}
-	}
-
-	assert.Equal(t, expected.ExecutionTimeMillis, actual.ExecutionTimeMillis, "Incorrect stage execution time")
-	assert.Equal(t, expected.Stage, actual.Stage, "Incorrect stage name")
-	assert.Equal(t, expected.Entity, actual.Entity, "Incorrect stage entity name")
-
-	for i, expGroup := range expected.Groups {
-		gotGroup := actual.Groups[i]
-		assert.Equal(t, len(expGroup.InvocationResults), len(gotGroup.InvocationResults), "Group outcomes #%d contain different number of invocation results", i)
-		assert.Equal(t, expGroup.ExecutionTimeMillis, gotGroup.ExecutionTimeMillis, "Incorrect group #%d execution time", i)
-
-		for _, expHook := range expGroup.InvocationResults {
-			gotHook := findCorrespondingHookResult(expHook.HookID, gotGroup)
-			assert.NotNil(t, gotHook, "Expected to get hook, got nil: group #%d, hookID %v", i, expHook.HookID)
-
-			gotHook.ExecutionTimeMillis = 0 // reset hook execution time, we cannot predict it
-			assert.Equal(t, expHook, *gotHook, "Incorrect hook outcome: group #%d, hookID %v", i, expHook.HookID)
-		}
-	}
-}
-
-func findCorrespondingHookResult(hookID HookID, group GroupOutcome) *HookOutcome {
-	for _, hook := range group.InvocationResults {
-		if hook.HookID.ModuleCode == hookID.ModuleCode &&
-			hook.HookID.HookImplCode == hookID.HookImplCode {
-			return &hook
-		}
-	}
-	return nil
-}
diff --git a/hooks/hookexecution/test/complete-stage-outcomes/expected-basic-debug-response.json b/hooks/hookexecution/test/complete-stage-outcomes/expected-basic-debug-response.json
new file mode 100644
index 00000000..1f52d187
--- /dev/null
+++ b/hooks/hookexecution/test/complete-stage-outcomes/expected-basic-debug-response.json
@@ -0,0 +1,151 @@
+{
+  "prebid": {
+    "foo": "bar",
+    "modules": {
+      "errors": {
+        "foobar": {
+          "foo": [
+            "error 1"
+          ],
+          "bar": [
+            "error 2",
+            "error 4"
+          ]
+        },
+        "foobaz": {
+          "baz": [
+            "error 3"
+          ]
+        }
+      },
+      "warnings": {
+        "foobaz": {
+          "baz": [
+            "warning 1"
+          ]
+        }
+      },
+      "trace": {
+        "execution_time_millis": 600,
+        "stages": [
+          {
+            "stage": "entrypoint",
+            "execution_time_millis": 300,
+            "outcomes": [
+              {
+                "entity": "http-request",
+                "execution_time_millis": 300,
+                "groups": [
+                  {
+                    "execution_time_millis": 200,
+                    "invocation_results": [
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "foo"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 200,
+                        "analytics_tags": {},
+                        "message": ""
+                      },
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "bar"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 50,
+                        "analytics_tags": {},
+                        "message": ""
+                      }
+                    ]
+                  },
+                  {
+                    "execution_time_millis": 100,
+                    "invocation_results": [
+                      {
+                        "hook_id": {
+                          "module_code": "foobaz",
+                          "hook_impl_code": "baz"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 100,
+                        "analytics_tags": {},
+                        "message": ""
+                      },
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "bar"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 10,
+                        "analytics_tags": {},
+                        "message": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "stage": "bidder_request",
+            "execution_time_millis": 300,
+            "outcomes": [
+              {
+                "entity": "adf",
+                "execution_time_millis": 200,
+                "groups": [
+                  {
+                    "execution_time_millis": 200,
+                    "invocation_results": [
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "foo"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 200,
+                        "analytics_tags": {},
+                        "message": ""
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "entity": "adot",
+                "execution_time_millis": 300,
+                "groups": [
+                  {
+                    "execution_time_millis": 200,
+                    "invocation_results": [
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "bar"
+                        },
+                        "status": "success",
+                        "action": "no_action",
+                        "execution_time_millis": 200,
+                        "analytics_tags": {},
+                        "message": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/hooks/hookexecution/test/complete-stage-outcomes/expected-debug-response.json b/hooks/hookexecution/test/complete-stage-outcomes/expected-debug-response.json
new file mode 100644
index 00000000..2b119fc3
--- /dev/null
+++ b/hooks/hookexecution/test/complete-stage-outcomes/expected-debug-response.json
@@ -0,0 +1,30 @@
+{
+  "prebid": {
+    "foo": "bar",
+    "modules": {
+      "errors": {
+        "foobar": {
+          "foo": [
+            "error 1"
+          ],
+          "bar": [
+            "error 2",
+            "error 4"
+          ]
+        },
+        "foobaz": {
+          "baz": [
+            "error 3"
+          ]
+        }
+      },
+      "warnings": {
+        "foobaz": {
+          "baz": [
+            "warning 1"
+          ]
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/hooks/hookexecution/test/complete-stage-outcomes/expected-empty-response.json b/hooks/hookexecution/test/complete-stage-outcomes/expected-empty-response.json
new file mode 100644
index 00000000..a23b7652
--- /dev/null
+++ b/hooks/hookexecution/test/complete-stage-outcomes/expected-empty-response.json
@@ -0,0 +1,5 @@
+{
+  "prebid": {
+    "foo": "bar"
+  }
+}
\ No newline at end of file
diff --git a/hooks/hookexecution/test/complete-stage-outcomes/expected-pure-debug-response.json b/hooks/hookexecution/test/complete-stage-outcomes/expected-pure-debug-response.json
new file mode 100644
index 00000000..7336aff0
--- /dev/null
+++ b/hooks/hookexecution/test/complete-stage-outcomes/expected-pure-debug-response.json
@@ -0,0 +1,29 @@
+{
+  "prebid": {
+    "modules": {
+      "errors": {
+        "foobar": {
+          "foo": [
+            "error 1"
+          ],
+          "bar": [
+            "error 2",
+            "error 4"
+          ]
+        },
+        "foobaz": {
+          "baz": [
+            "error 3"
+          ]
+        }
+      },
+      "warnings": {
+        "foobaz": {
+          "baz": [
+            "warning 1"
+          ]
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/hooks/hookexecution/test/complete-stage-outcomes/expected-verbose-debug-response.json b/hooks/hookexecution/test/complete-stage-outcomes/expected-verbose-debug-response.json
new file mode 100644
index 00000000..9825d1fd
--- /dev/null
+++ b/hooks/hookexecution/test/complete-stage-outcomes/expected-verbose-debug-response.json
@@ -0,0 +1,186 @@
+{
+  "prebid": {
+    "foo": "bar",
+    "modules": {
+      "errors": {
+        "foobar": {
+          "foo": [
+            "error 1"
+          ],
+          "bar": [
+            "error 2",
+            "error 4"
+          ]
+        },
+        "foobaz": {
+          "baz": [
+            "error 3"
+          ]
+        }
+      },
+      "warnings": {
+        "foobaz": {
+          "baz": [
+            "warning 1"
+          ]
+        }
+      },
+      "trace": {
+        "execution_time_millis": 600,
+        "stages": [
+          {
+            "stage": "entrypoint",
+            "execution_time_millis": 300,
+            "outcomes": [
+              {
+                "entity": "http-request",
+                "execution_time_millis": 300,
+                "groups": [
+                  {
+                    "execution_time_millis": 200,
+                    "invocation_results": [
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "foo"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 200,
+                        "analytics_tags": {
+                          "activities": [
+                            {
+                              "name": "device-id",
+                              "status": "success",
+                              "results": [
+                                {
+                                  "status": "success-allow",
+                                  "values": {
+                                    "foo": "bar"
+                                  },
+                                  "appliedto": {
+                                    "impids": [
+                                      "impId1"
+                                    ],
+                                    "request": true
+                                  }
+                                }
+                              ]
+                            },
+                            {
+                              "name": "define-blocks",
+                              "status": "error"
+                            }
+                          ]
+                        },
+                        "debug_messages": [
+                          "debug message 1"
+                        ],
+                        "message": ""
+                      },
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "bar"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 50,
+                        "analytics_tags": {},
+                        "message": ""
+                      }
+                    ]
+                  },
+                  {
+                    "execution_time_millis": 100,
+                    "invocation_results": [
+                      {
+                        "hook_id": {
+                          "module_code": "foobaz",
+                          "hook_impl_code": "baz"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 100,
+                        "analytics_tags": {},
+                        "debug_messages": [
+                          "debug message 1",
+                          "debug message 2"
+                        ],
+                        "message": ""
+                      },
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "bar"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 10,
+                        "analytics_tags": {},
+                        "message": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "stage": "bidder_request",
+            "execution_time_millis": 300,
+            "outcomes": [
+              {
+                "entity": "adf",
+                "execution_time_millis": 200,
+                "groups": [
+                  {
+                    "execution_time_millis": 200,
+                    "invocation_results": [
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "foo"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 200,
+                        "analytics_tags": {},
+                        "debug_messages": [
+                          "debug message 1"
+                        ],
+                        "message": ""
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "entity": "adot",
+                "execution_time_millis": 300,
+                "groups": [
+                  {
+                    "execution_time_millis": 200,
+                    "invocation_results": [
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "bar"
+                        },
+                        "status": "success",
+                        "action": "no_action",
+                        "execution_time_millis": 200,
+                        "analytics_tags": {},
+                        "message": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/hooks/hookexecution/test/complete-stage-outcomes/expected-verbose-response.json b/hooks/hookexecution/test/complete-stage-outcomes/expected-verbose-response.json
new file mode 100644
index 00000000..8561d352
--- /dev/null
+++ b/hooks/hookexecution/test/complete-stage-outcomes/expected-verbose-response.json
@@ -0,0 +1,163 @@
+{
+  "prebid": {
+    "foo": "bar",
+    "modules": {
+      "trace": {
+        "execution_time_millis": 600,
+        "stages": [
+          {
+            "stage": "entrypoint",
+            "execution_time_millis": 300,
+            "outcomes": [
+              {
+                "entity": "http-request",
+                "execution_time_millis": 300,
+                "groups": [
+                  {
+                    "execution_time_millis": 200,
+                    "invocation_results": [
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "foo"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 200,
+                        "analytics_tags": {
+                          "activities": [
+                            {
+                              "name": "device-id",
+                              "status": "success",
+                              "results": [
+                                {
+                                  "status": "success-allow",
+                                  "values": {
+                                    "foo": "bar"
+                                  },
+                                  "appliedto": {
+                                    "impids": [
+                                      "impId1"
+                                    ],
+                                    "request": true
+                                  }
+                                }
+                              ]
+                            },
+                            {
+                              "name": "define-blocks",
+                              "status": "error"
+                            }
+                          ]
+                        },
+                        "debug_messages": [
+                          "debug message 1"
+                        ],
+                        "message": ""
+                      },
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "bar"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 50,
+                        "analytics_tags": {},
+                        "message": ""
+                      }
+                    ]
+                  },
+                  {
+                    "execution_time_millis": 100,
+                    "invocation_results": [
+                      {
+                        "hook_id": {
+                          "module_code": "foobaz",
+                          "hook_impl_code": "baz"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 100,
+                        "analytics_tags": {},
+                        "debug_messages": [
+                          "debug message 1",
+                          "debug message 2"
+                        ],
+                        "message": ""
+                      },
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "bar"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 10,
+                        "analytics_tags": {},
+                        "message": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            "stage": "bidder_request",
+            "execution_time_millis": 300,
+            "outcomes": [
+              {
+                "entity": "adf",
+                "execution_time_millis": 200,
+                "groups": [
+                  {
+                    "execution_time_millis": 200,
+                    "invocation_results": [
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "foo"
+                        },
+                        "status": "success",
+                        "action": "update",
+                        "execution_time_millis": 200,
+                        "analytics_tags": {},
+                        "debug_messages": [
+                          "debug message 1"
+                        ],
+                        "message": ""
+                      }
+                    ]
+                  }
+                ]
+              },
+              {
+                "entity": "adot",
+                "execution_time_millis": 300,
+                "groups": [
+                  {
+                    "execution_time_millis": 200,
+                    "invocation_results": [
+                      {
+                        "hook_id": {
+                          "module_code": "foobar",
+                          "hook_impl_code": "bar"
+                        },
+                        "status": "success",
+                        "action": "no_action",
+                        "execution_time_millis": 200,
+                        "analytics_tags": {},
+                        "message": ""
+                      }
+                    ]
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/hooks/hookexecution/test/complete-stage-outcomes/stage-outcomes.json b/hooks/hookexecution/test/complete-stage-outcomes/stage-outcomes.json
new file mode 100644
index 00000000..62a6f822
--- /dev/null
+++ b/hooks/hookexecution/test/complete-stage-outcomes/stage-outcomes.json
@@ -0,0 +1,159 @@
+[
+  {
+    "entity": "http-request",
+    "stage": "entrypoint",
+    "execution_time_millis": 300,
+    "groups": [
+      {
+        "execution_time_millis": 200,
+        "invocation_results": [
+          {
+            "hook_id": {
+              "module_code": "foobar",
+              "hook_impl_code": "foo"
+            },
+            "status": "success",
+            "action": "update",
+            "execution_time_millis": 200,
+            "analytics_tags": {
+              "activities": [
+                {
+                  "name": "device-id",
+                  "status": "success",
+                  "results": [
+                    {
+                      "status": "success-allow",
+                      "values": {
+                        "foo": "bar"
+                      },
+                      "appliedto": {
+                        "impids": [
+                          "impId1"
+                        ],
+                        "request": true
+                      }
+                    }
+                  ]
+                },
+                {
+                  "name": "define-blocks",
+                  "status": "error"
+                }
+              ]
+            },
+            "debug_messages": [
+              "debug message 1"
+            ],
+            "errors": [
+              "error 1"
+            ],
+            "message": ""
+          },
+          {
+            "hook_id": {
+              "module_code": "foobar",
+              "hook_impl_code": "bar"
+            },
+            "status": "success",
+            "action": "update",
+            "execution_time_millis": 50,
+            "analytics_tags": {},
+            "errors": [
+              "error 2"
+            ],
+            "message": ""
+          }
+        ]
+      },
+      {
+        "execution_time_millis": 100,
+        "invocation_results": [
+          {
+            "hook_id": {
+              "module_code": "foobaz",
+              "hook_impl_code": "baz"
+            },
+            "status": "success",
+            "action": "update",
+            "execution_time_millis": 100,
+            "analytics_tags": {},
+            "debug_messages": [
+              "debug message 1",
+              "debug message 2"
+            ],
+            "errors": [
+              "error 3"
+            ],
+            "warnings": [
+              "warning 1"
+            ],
+            "message": ""
+          },
+          {
+            "hook_id": {
+              "module_code": "foobar",
+              "hook_impl_code": "bar"
+            },
+            "status": "success",
+            "action": "update",
+            "execution_time_millis": 10,
+            "analytics_tags": {},
+            "errors": [
+              "error 4"
+            ],
+            "message": ""
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "entity": "adf",
+    "stage": "bidder_request",
+    "execution_time_millis": 200,
+    "groups": [
+      {
+        "execution_time_millis": 200,
+        "invocation_results": [
+          {
+            "hook_id": {
+              "module_code": "foobar",
+              "hook_impl_code": "foo"
+            },
+            "status": "success",
+            "action": "update",
+            "execution_time_millis": 200,
+            "analytics_tags": {},
+            "debug_messages": [
+              "debug message 1"
+            ],
+            "message": ""
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "entity": "adot",
+    "stage": "bidder_request",
+    "execution_time_millis": 300,
+    "groups": [
+      {
+        "execution_time_millis": 200,
+        "invocation_results": [
+          {
+            "hook_id": {
+              "module_code": "foobar",
+              "hook_impl_code": "bar"
+            },
+            "status": "success",
+            "action": "no_action",
+            "execution_time_millis": 200,
+            "analytics_tags": {},
+            "message": ""
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/hooks/hookexecution/test/empty-stage-outcomes/empty-stage-outcomes-v1.json b/hooks/hookexecution/test/empty-stage-outcomes/empty-stage-outcomes-v1.json
new file mode 100644
index 00000000..165c4d9f
--- /dev/null
+++ b/hooks/hookexecution/test/empty-stage-outcomes/empty-stage-outcomes-v1.json
@@ -0,0 +1,8 @@
+[
+  {
+    "entity": "http-request",
+    "stage": "entrypoint",
+    "execution_time_millis": 0,
+    "groups": []
+  }
+]
\ No newline at end of file
diff --git a/hooks/hookexecution/test/empty-stage-outcomes/empty-stage-outcomes-v2.json b/hooks/hookexecution/test/empty-stage-outcomes/empty-stage-outcomes-v2.json
new file mode 100644
index 00000000..0637a088
--- /dev/null
+++ b/hooks/hookexecution/test/empty-stage-outcomes/empty-stage-outcomes-v2.json
@@ -0,0 +1,1 @@
+[]
\ No newline at end of file
diff --git a/hooks/hookexecution/test/empty-stage-outcomes/empty.json b/hooks/hookexecution/test/empty-stage-outcomes/empty.json
new file mode 100644
index 00000000..e69de29b
diff --git a/hooks/hookexecution/test_utils.go b/hooks/hookexecution/test_utils.go
new file mode 100644
index 00000000..bd94d577
--- /dev/null
+++ b/hooks/hookexecution/test_utils.go
@@ -0,0 +1,105 @@
+package hookexecution
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// AssertEqualModulesData is the test helper function which asserts
+// that expected modules data fully corresponds the actual modules data.
+// Dynamic data for execution time is calculated from actual modules data.
+func AssertEqualModulesData(t *testing.T, expectedData, actualData json.RawMessage) {
+	t.Helper()
+
+	var expectedModulesOutcome ModulesOutcome
+	var actualModulesOutcome ModulesOutcome
+
+	assert.NoError(t, json.Unmarshal(expectedData, &expectedModulesOutcome), "Failed to unmarshal expected modules data.")
+	assert.NoError(t, json.Unmarshal(actualData, &actualModulesOutcome), "Failed to unmarshal actual modules data.")
+	assert.Equal(t, expectedModulesOutcome.Errors, actualModulesOutcome.Errors, "Invalid error messages.")
+	assert.Equal(t, expectedModulesOutcome.Warnings, actualModulesOutcome.Warnings, "Invalid warning messages.")
+
+	assertEqualTraces(t, expectedModulesOutcome.Trace, actualModulesOutcome.Trace)
+}
+
+func assertEqualTraces(t *testing.T, expectedTrace *TraceOutcome, actualTrace *TraceOutcome) {
+	if expectedTrace == nil {
+		assert.Nil(t, actualTrace, "Nil trace not expected.")
+	}
+
+	// calculate expected timings from actual modules outcome
+	for i, actualStage := range actualTrace.Stages {
+		expectedStage := expectedTrace.Stages[i]
+		expectedTrace.ExecutionTimeMillis += actualStage.ExecutionTimeMillis
+
+		for _, actualOutcome := range actualStage.Outcomes {
+			if expectedStage.ExecutionTimeMillis < actualOutcome.ExecutionTimeMillis {
+				expectedStage.ExecutionTimeMillis = actualOutcome.ExecutionTimeMillis
+			}
+
+			expectedOutcome := findCorrespondingStageOutcome(expectedStage, actualOutcome)
+			assert.NotNil(t, expectedOutcome, "Not found corresponding stage outcome, actual:`", actualOutcome)
+			assertEqualStageOutcomes(t, *expectedOutcome, actualOutcome)
+		}
+
+		assert.Equal(t, expectedStage.Stage, actualStage.Stage, "Invalid stage name.")
+		assert.Equal(t, expectedStage.ExecutionTimeMillis, actualStage.ExecutionTimeMillis, "Invalid stage execution time.")
+	}
+
+	assert.Equal(t, expectedTrace.ExecutionTimeMillis, actualTrace.ExecutionTimeMillis, "Invalid trace execution time.")
+}
+
+func assertEqualStageOutcomes(t *testing.T, expected StageOutcome, actual StageOutcome) {
+	t.Helper()
+
+	assert.Equal(t, len(actual.Groups), len(expected.Groups), "Stage outcomes contain different number of groups")
+
+	// calculate expected timings from actual outcome
+	for i, group := range actual.Groups {
+		expected.ExecutionTimeMillis += group.ExecutionTimeMillis
+		for _, hook := range group.InvocationResults {
+			if hook.ExecutionTimeMillis > expected.Groups[i].ExecutionTimeMillis {
+				expected.Groups[i].ExecutionTimeMillis = hook.ExecutionTimeMillis
+			}
+		}
+	}
+
+	assert.Equal(t, expected.ExecutionTimeMillis, actual.ExecutionTimeMillis, "Incorrect stage execution time")
+	assert.Equal(t, expected.Stage, actual.Stage, "Incorrect stage name")
+	assert.Equal(t, expected.Entity, actual.Entity, "Incorrect stage entity name")
+
+	for i, expGroup := range expected.Groups {
+		gotGroup := actual.Groups[i]
+		assert.Equal(t, len(expGroup.InvocationResults), len(gotGroup.InvocationResults), "Group outcomes #%d contain different number of invocation results", i)
+		assert.Equal(t, expGroup.ExecutionTimeMillis, gotGroup.ExecutionTimeMillis, "Incorrect group #%d execution time", i)
+
+		for _, expHook := range expGroup.InvocationResults {
+			gotHook := findCorrespondingHookResult(expHook.HookID, gotGroup)
+			assert.NotNil(t, gotHook, "Expected to get hook, got nil: group #%d, hookID %v", i, expHook.HookID)
+
+			gotHook.ExecutionTimeMillis = 0 // reset hook execution time, we cannot predict it
+			assert.Equal(t, expHook, *gotHook, "Incorrect hook outcome: group #%d, hookID %v", i, expHook.HookID)
+		}
+	}
+}
+
+func findCorrespondingStageOutcome(stage Stage, outcome StageOutcome) *StageOutcome {
+	for _, out := range stage.Outcomes {
+		if out.Entity == outcome.Entity {
+			return &out
+		}
+	}
+	return nil
+}
+
+func findCorrespondingHookResult(hookID HookID, group GroupOutcome) *HookOutcome {
+	for _, hook := range group.InvocationResults {
+		if hook.HookID.ModuleCode == hookID.ModuleCode &&
+			hook.HookID.HookImplCode == hookID.HookImplCode {
+			return &hook
+		}
+	}
+	return nil
+}
diff --git a/metrics/go_metrics_test.go b/metrics/go_metrics_test.go
index d3bd2d66..7be83638 100644
--- a/metrics/go_metrics_test.go
+++ b/metrics/go_metrics_test.go
@@ -130,6 +130,12 @@ func ensureContainsAdapterMetrics(t *testing.T, registry metrics.Registry, name
 	ensureContains(t, registry, name+".connections_created", adapterMetrics.ConnCreated)
 	ensureContains(t, registry, name+".connections_reused", adapterMetrics.ConnReused)
 	ensureContains(t, registry, name+".connection_wait_time", adapterMetrics.ConnWaitTime)
+
+	ensureContains(t, registry, name+".response.validation.size.err", adapterMetrics.BidValidationCreativeSizeErrorMeter)
+	ensureContains(t, registry, name+".response.validation.size.warn", adapterMetrics.BidValidationCreativeSizeWarnMeter)
+	ensureContains(t, registry, name+".response.validation.secure.err", adapterMetrics.BidValidationSecureMarkupErrorMeter)
+	ensureContains(t, registry, name+".response.validation.secure.warn", adapterMetrics.BidValidationSecureMarkupWarnMeter)
+
 }
 
 func ensureContainsModuleMetrics(t *testing.T, registry metrics.Registry, name string, moduleMetrics *ModuleMetrics) {
@@ -272,6 +278,90 @@ func TestRecordDebugRequest(t *testing.T) {
 	}
 }
 
+func TestRecordBidValidationCreativeSize(t *testing.T) {
+	testCases := []struct {
+		description          string
+		givenDisabledMetrics config.DisabledMetrics
+		givenPubID           string
+		expectedAccountCount int64
+		expectedAdapterCount int64
+	}{
+		{
+			description: "Account Metric isn't disabled, so both metrics should be incremented",
+			givenDisabledMetrics: config.DisabledMetrics{
+				AccountAdapterDetails: false,
+			},
+			givenPubID:           "acct-id",
+			expectedAdapterCount: 1,
+			expectedAccountCount: 1,
+		},
+		{
+			description: "Account Metric is disabled, so only the adapter metric should increment",
+			givenDisabledMetrics: config.DisabledMetrics{
+				AccountAdapterDetails: true,
+			},
+			givenPubID:           "acct-id",
+			expectedAdapterCount: 1,
+			expectedAccountCount: 0,
+		},
+	}
+	for _, test := range testCases {
+		registry := metrics.NewRegistry()
+		m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, test.givenDisabledMetrics, nil, nil)
+
+		m.RecordBidValidationCreativeSizeError(openrtb_ext.BidderAppnexus, test.givenPubID)
+		m.RecordBidValidationCreativeSizeWarn(openrtb_ext.BidderAppnexus, test.givenPubID)
+		am := m.getAccountMetrics(test.givenPubID)
+
+		assert.Equal(t, test.expectedAdapterCount, m.AdapterMetrics[openrtb_ext.BidderAppnexus].BidValidationCreativeSizeErrorMeter.Count())
+		assert.Equal(t, test.expectedAdapterCount, m.AdapterMetrics[openrtb_ext.BidderAppnexus].BidValidationCreativeSizeWarnMeter.Count())
+		assert.Equal(t, test.expectedAccountCount, am.bidValidationCreativeSizeMeter.Count())
+		assert.Equal(t, test.expectedAccountCount, am.bidValidationCreativeSizeWarnMeter.Count())
+	}
+}
+
+func TestRecordBidValidationSecureMarkup(t *testing.T) {
+	testCases := []struct {
+		description          string
+		givenDisabledMetrics config.DisabledMetrics
+		givenPubID           string
+		expectedAccountCount int64
+		expectedAdapterCount int64
+	}{
+		{
+			description: "Account Metric isn't disabled, so both metrics should be incremented",
+			givenDisabledMetrics: config.DisabledMetrics{
+				AccountAdapterDetails: false,
+			},
+			givenPubID:           "acct-id",
+			expectedAdapterCount: 1,
+			expectedAccountCount: 1,
+		},
+		{
+			description: "Account Metric is disabled, so only the adapter metric should increment",
+			givenDisabledMetrics: config.DisabledMetrics{
+				AccountAdapterDetails: true,
+			},
+			givenPubID:           "acct-id",
+			expectedAdapterCount: 1,
+			expectedAccountCount: 0,
+		},
+	}
+	for _, test := range testCases {
+		registry := metrics.NewRegistry()
+		m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, test.givenDisabledMetrics, nil, nil)
+
+		m.RecordBidValidationSecureMarkupError(openrtb_ext.BidderAppnexus, test.givenPubID)
+		m.RecordBidValidationSecureMarkupWarn(openrtb_ext.BidderAppnexus, test.givenPubID)
+		am := m.getAccountMetrics(test.givenPubID)
+
+		assert.Equal(t, test.expectedAdapterCount, m.AdapterMetrics[openrtb_ext.BidderAppnexus].BidValidationSecureMarkupErrorMeter.Count())
+		assert.Equal(t, test.expectedAdapterCount, m.AdapterMetrics[openrtb_ext.BidderAppnexus].BidValidationSecureMarkupWarnMeter.Count())
+		assert.Equal(t, test.expectedAccountCount, am.bidValidationSecureMarkupMeter.Count())
+		assert.Equal(t, test.expectedAccountCount, am.bidValidationSecureMarkupWarnMeter.Count())
+	}
+}
+
 func TestRecordDNSTime(t *testing.T) {
 	testCases := []struct {
 		description         string
diff --git a/metrics/prometheus/prometheus_test.go b/metrics/prometheus/prometheus_test.go
index ce1fa085..17b9be83 100644
--- a/metrics/prometheus/prometheus_test.go
+++ b/metrics/prometheus/prometheus_test.go
@@ -65,7 +65,7 @@ func TestMetricCountGatekeeping(t *testing.T) {
 	// Verify Per-Adapter Cardinality
 	// - This assertion provides a warning for newly added adapter metrics. Threre are 40+ adapters which makes the
 	//   cost of new per-adapter metrics rather expensive. Thought should be given when adding new per-adapter metrics.
-	assert.True(t, perAdapterCardinalityCount <= 27, "Per-Adapter Cardinality count equals %d \n", perAdapterCardinalityCount)
+	assert.True(t, perAdapterCardinalityCount <= 29, "Per-Adapter Cardinality count equals %d \n", perAdapterCardinalityCount)
 }
 
 func TestConnectionMetrics(t *testing.T) {
@@ -206,6 +206,78 @@ func TestDebugRequestMetric(t *testing.T) {
 	}
 }
 
+func TestBidValidationCreativeSizeMetric(t *testing.T) {
+	testCases := []struct {
+		description                        string
+		givenDebugEnabledFlag              bool
+		givenAccountAdapterMetricsDisabled bool
+		expectedAdapterCount               float64
+		expectedAccountCount               float64
+	}{
+		{
+			description:                        "Account Metric isn't disabled, so both metrics should be incremented",
+			givenAccountAdapterMetricsDisabled: false,
+			expectedAdapterCount:               1,
+			expectedAccountCount:               1,
+		},
+		{
+			description:                        "Account Metric is disabled, so only adapter metric should be incremented",
+			givenAccountAdapterMetricsDisabled: true,
+			expectedAdapterCount:               1,
+			expectedAccountCount:               0,
+		},
+	}
+
+	for _, test := range testCases {
+		m := createMetricsForTesting()
+		m.metricsDisabled.AccountAdapterDetails = test.givenAccountAdapterMetricsDisabled
+		m.RecordBidValidationCreativeSizeError(adapterLabel, "acct-id")
+		m.RecordBidValidationCreativeSizeWarn(adapterLabel, "acct-id")
+
+		assertCounterVecValue(t, "", "account bid validation", m.accountBidResponseValidationSizeError, test.expectedAccountCount, prometheus.Labels{accountLabel: "acct-id", successLabel: successLabel})
+		assertCounterVecValue(t, "", "adapter bid validation", m.adapterBidResponseValidationSizeError, test.expectedAdapterCount, prometheus.Labels{adapterLabel: adapterLabel, successLabel: successLabel})
+
+		assertCounterVecValue(t, "", "account bid validation", m.accountBidResponseValidationSizeWarn, test.expectedAccountCount, prometheus.Labels{accountLabel: "acct-id", successLabel: successLabel})
+		assertCounterVecValue(t, "", "adapter bid validation", m.adapterBidResponseValidationSizeWarn, test.expectedAdapterCount, prometheus.Labels{adapterLabel: adapterLabel, successLabel: successLabel})
+	}
+}
+
+func TestBidValidationSecureMarkupMetric(t *testing.T) {
+	testCases := []struct {
+		description                        string
+		givenDebugEnabledFlag              bool
+		givenAccountAdapterMetricsDisabled bool
+		expectedAdapterCount               float64
+		expectedAccountCount               float64
+	}{
+		{
+			description:                        "Account Metric isn't disabled, so both metrics should be incremented",
+			givenAccountAdapterMetricsDisabled: false,
+			expectedAdapterCount:               1,
+			expectedAccountCount:               1,
+		},
+		{
+			description:                        "Account Metric is disabled, so only adapter metric should be incremented",
+			givenAccountAdapterMetricsDisabled: true,
+			expectedAdapterCount:               1,
+			expectedAccountCount:               0,
+		},
+	}
+
+	for _, test := range testCases {
+		m := createMetricsForTesting()
+		m.metricsDisabled.AccountAdapterDetails = test.givenAccountAdapterMetricsDisabled
+		m.RecordBidValidationSecureMarkupError(adapterLabel, "acct-id")
+		m.RecordBidValidationSecureMarkupWarn(adapterLabel, "acct-id")
+
+		assertCounterVecValue(t, "", "Account Secure Markup Error", m.accountBidResponseSecureMarkupError, test.expectedAccountCount, prometheus.Labels{accountLabel: "acct-id", successLabel: successLabel})
+		assertCounterVecValue(t, "", "Adapter Secure Markup Error", m.adapterBidResponseSecureMarkupError, test.expectedAdapterCount, prometheus.Labels{adapterLabel: adapterLabel, successLabel: successLabel})
+
+		assertCounterVecValue(t, "", "Account Secure Markup Warn", m.accountBidResponseSecureMarkupWarn, test.expectedAccountCount, prometheus.Labels{accountLabel: "acct-id", successLabel: successLabel})
+		assertCounterVecValue(t, "", "Adapter Secure Markup Warn", m.adapterBidResponseSecureMarkupWarn, test.expectedAdapterCount, prometheus.Labels{adapterLabel: adapterLabel, successLabel: successLabel})
+	}
+}
+
 func TestRequestMetricWithoutCookie(t *testing.T) {
 	requestType := metrics.ReqTypeORTB2Web
 	performTest := func(m *Metrics, cookieFlag metrics.CookieFlag) {