Repository: prebid/prebid-server
Pull request #1515 (2020-09-30 14:05:44)

Add metrics to capture stored data fetch all/delta durations with fetch status


Patch:
diff --git a/endpoints/auction.go b/endpoints/auction.go
index c6fd5712..0be77297 100644
--- a/endpoints/auction.go
+++ b/endpoints/auction.go
@@ -310,10 +310,7 @@ func sortBidsAddKeywordsMobile(bids pbs.PBSBidSlice, pbs_req *pbs.PBSRequest, pr
 		// after sorting we need to add the ad targeting keywords
 		for i, bid := range bar {
 			// We should eventually check for the error and do something.
-			roundedCpm, err := exchange.GetCpmStringValue(bid.Price, openrtb_ext.PriceGranularityFromString(priceGranularitySetting))
-			if err != nil {
-				glog.Error(err.Error())
-			}
+			roundedCpm := exchange.GetPriceBucket(bid.Price, openrtb_ext.PriceGranularityFromString(priceGranularitySetting))
 
 			hbSize := ""
 			if bid.Width != 0 && bid.Height != 0 {
diff --git a/exchange/auction.go b/exchange/auction.go
index aa446ddb..785eae29 100644
--- a/exchange/auction.go
+++ b/exchange/auction.go
@@ -11,7 +11,6 @@ import (
 	"time"
 
 	uuid "github.com/gofrs/uuid"
-	"github.com/golang/glog"
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
@@ -129,11 +128,7 @@ func (a *auction) setRoundedPrices(priceGranularity openrtb_ext.PriceGranularity
 	roundedPrices := make(map[*pbsOrtbBid]string, 5*len(a.winningBids))
 	for _, topBidsPerImp := range a.winningBidsByBidder {
 		for _, topBidPerBidder := range topBidsPerImp {
-			roundedPrice, err := GetCpmStringValue(topBidPerBidder.bid.Price, priceGranularity)
-			if err != nil {
-				glog.Errorf(`Error rounding price according to granularity. This shouldn't happen unless /openrtb2 input validation is buggy. Granularity was "%v".`, priceGranularity)
-			}
-			roundedPrices[topBidPerBidder] = roundedPrice
+			roundedPrices[topBidPerBidder] = GetPriceBucket(topBidPerBidder.bid.Price, priceGranularity)
 		}
 	}
 	a.roundedPrices = roundedPrices
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 64b0043c..c2683b95 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -96,6 +96,7 @@ func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *con
 
 func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, usersyncs IdFetcher, labels pbsmetrics.Labels, account *config.Account, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *DebugLog) (*openrtb.BidResponse, error) {
 
+	var err error
 	requestExt, err := extractBidRequestExt(bidRequest)
 	if err != nil {
 		return nil, err
@@ -114,34 +115,11 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 
 	bidAdjustmentFactors := getExtBidAdjustmentFactors(requestExt)
 
-	for _, impInRequest := range bidRequest.Imp {
-		var impLabels pbsmetrics.ImpLabels = pbsmetrics.ImpLabels{
-			BannerImps: impInRequest.Banner != nil,
-			VideoImps:  impInRequest.Video != nil,
-			AudioImps:  impInRequest.Audio != nil,
-			NativeImps: impInRequest.Native != nil,
-		}
-		e.me.RecordImps(impLabels)
-	}
+	recordImpMetrics(bidRequest, e.me)
 
 	// Make our best guess if GDPR applies
-	usersyncIfAmbiguous := e.UsersyncIfAmbiguous
-	var geo *openrtb.Geo = nil
-	if bidRequest.User != nil && bidRequest.User.Geo != nil {
-		geo = bidRequest.User.Geo
-	} else if bidRequest.Device != nil && bidRequest.Device.Geo != nil {
-		geo = bidRequest.Device.Geo
-	}
-	if geo != nil {
-		// If we have a country set, and it is on the list, we assume GDPR applies if not set on the request.
-		// Otherwise we assume it does not apply as long as it appears "valid" (is 3 characters long).
-		if _, found := e.privacyConfig.GDPR.EEACountriesMap[strings.ToUpper(geo.Country)]; found {
-			usersyncIfAmbiguous = false
-		} else if len(geo.Country) == 3 {
-			// The country field is formatted properly as a three character country code
-			usersyncIfAmbiguous = true
-		}
-	}
+	usersyncIfAmbiguous := e.parseUsersyncIfAmbiguous(bidRequest)
+
 	// Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder
 	blabels := make(map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels)
 	cleanRequests, aliases, privacyLabels, errs := cleanOpenRTBRequests(ctx, bidRequest, requestExt, usersyncs, blabels, labels, e.gDPR, usersyncIfAmbiguous, e.privacyConfig)
@@ -161,14 +139,13 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 
 	adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, cleanRequests, aliases, bidAdjustmentFactors, blabels, conversions)
 
-	var auc *auction = nil
-	var bidResponseExt *openrtb_ext.ExtBidResponse = nil
+	var auc *auction
+	var cacheErrs []error
 	if anyBidsReturned {
 
 		var bidCategory map[string]string
 		//If includebrandcategory is present in ext then CE feature is on.
 		if requestExt.Prebid.Targeting != nil && requestExt.Prebid.Targeting.IncludeBrandCategory != nil {
-			var err error
 			var rejections []string
 			bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, requestExt, adapterBids, *categoriesFetcher, targData)
 			if err != nil {
@@ -189,42 +166,33 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 				errs = append(errs, dealErrs...)
 			}
 
-			if debugLog != nil && debugLog.Enabled {
-				bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, debugInfo, errs)
-				if bidRespExtBytes, err := json.Marshal(bidResponseExt); err == nil {
-					debugLog.Data.Response = string(bidRespExtBytes)
-				} else {
-					debugLog.Data.Response = "Unable to marshal response ext for debugging"
-					errs = append(errs, errors.New(debugLog.Data.Response))
-				}
-			}
-
 			cacheErrs := auc.doCache(ctx, e.cache, targData, bidRequest, 60, &account.CacheTTL, bidCategory, debugLog)
 			if len(cacheErrs) > 0 {
 				errs = append(errs, cacheErrs...)
 			}
 			targData.setTargeting(auc, bidRequest.App != nil, bidCategory)
 
-			// Ensure caching errors are added if the bid response ext has already been created
-			if bidResponseExt != nil && len(cacheErrs) > 0 {
-				bidderCacheErrs := errsToBidderErrors(cacheErrs)
-				bidResponseExt.Errors[openrtb_ext.PrebidExtKey] = append(bidResponseExt.Errors[openrtb_ext.PrebidExtKey], bidderCacheErrs...)
-			}
 		}
+	}
 
+	bidResponseExt := e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, debugInfo, errs)
+
+	// Ensure caching errors are added in case auc.doCache was called and errors were returned
+	if len(cacheErrs) > 0 {
+		bidderCacheErrs := errsToBidderErrors(cacheErrs)
+		bidResponseExt.Errors[openrtb_ext.PrebidExtKey] = append(bidResponseExt.Errors[openrtb_ext.PrebidExtKey], bidderCacheErrs...)
 	}
 
-	if !anyBidsReturned {
-		if debugLog != nil && debugLog.Enabled {
+	if debugLog != nil && debugLog.Enabled {
+		if bidRespExtBytes, err := json.Marshal(bidResponseExt); err == nil {
+			debugLog.Data.Response = string(bidRespExtBytes)
+		} else {
+			debugLog.Data.Response = "Unable to marshal response ext for debugging"
+			errs = append(errs, err)
+		}
+		if !anyBidsReturned {
 			if rawUUID, err := uuid.NewV4(); err == nil {
 				debugLog.CacheKey = rawUUID.String()
-
-				bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, debugInfo, errs)
-				if bidRespExtBytes, err := json.Marshal(bidResponseExt); err == nil {
-					debugLog.Data.Response = string(bidRespExtBytes)
-				} else {
-					debugLog.Data.Response = "Unable to marshal response ext for debugging"
-				}
 			} else {
 				errs = append(errs, err)
 			}
@@ -235,6 +203,41 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 	return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, errs)
 }
 
+func (e *exchange) parseUsersyncIfAmbiguous(bidRequest *openrtb.BidRequest) bool {
+	usersyncIfAmbiguous := e.UsersyncIfAmbiguous
+	var geo *openrtb.Geo = nil
+
+	if bidRequest.User != nil && bidRequest.User.Geo != nil {
+		geo = bidRequest.User.Geo
+	} else if bidRequest.Device != nil && bidRequest.Device.Geo != nil {
+		geo = bidRequest.Device.Geo
+	}
+	if geo != nil {
+		// If we have a country set, and it is on the list, we assume GDPR applies if not set on the request.
+		// Otherwise we assume it does not apply as long as it appears "valid" (is 3 characters long).
+		if _, found := e.privacyConfig.GDPR.EEACountriesMap[strings.ToUpper(geo.Country)]; found {
+			usersyncIfAmbiguous = false
+		} else if len(geo.Country) == 3 {
+			// The country field is formatted properly as a three character country code
+			usersyncIfAmbiguous = true
+		}
+	}
+
+	return usersyncIfAmbiguous
+}
+
+func recordImpMetrics(bidRequest *openrtb.BidRequest, metricsEngine pbsmetrics.MetricsEngine) {
+	for _, impInRequest := range bidRequest.Imp {
+		var impLabels pbsmetrics.ImpLabels = pbsmetrics.ImpLabels{
+			BannerImps: impInRequest.Banner != nil,
+			VideoImps:  impInRequest.Video != nil,
+			AudioImps:  impInRequest.Audio != nil,
+			NativeImps: impInRequest.Native != nil,
+		}
+		metricsEngine.RecordImps(impLabels)
+	}
+}
+
 type DealTierInfo struct {
 	Prefix      string `json:"prefix"`
 	MinDealTier int    `json:"minDealTier"`
@@ -479,6 +482,7 @@ func errsToBidderErrors(errs []error) []openrtb_ext.ExtBidderError {
 // 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, adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, bidRequest *openrtb.BidRequest, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, errList []error) (*openrtb.BidResponse, error) {
 	bidResponse := new(openrtb.BidResponse)
+	var err error
 
 	bidResponse.ID = bidRequest.ID
 	if len(liveAdapters) == 0 {
@@ -500,21 +504,19 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_
 
 	bidResponse.SeatBid = seatBids
 
-	if bidResponseExt == nil {
-		contextDebugValue := ctx.Value(DebugContextKey)
-		var debugInfo bool
-		if contextDebugValue != nil {
-			debugInfo = contextDebugValue.(bool)
-		}
-		bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, debugInfo, errList)
-	}
+	bidResponse.Ext, err = encodeBidResponseExt(bidResponseExt)
+
+	return bidResponse, err
+}
+
+func encodeBidResponseExt(bidResponseExt *openrtb_ext.ExtBidResponse) ([]byte, error) {
 	buffer := &bytes.Buffer{}
 	enc := json.NewEncoder(buffer)
+
 	enc.SetEscapeHTML(false)
 	err := enc.Encode(bidResponseExt)
-	bidResponse.Ext = buffer.Bytes()
 
-	return bidResponse, err
+	return buffer.Bytes(), err
 }
 
 func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData) (map[string]string, map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []string, error) {
@@ -601,7 +603,7 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques
 
 			// TODO: consider should we remove bids with zero duration here?
 
-			pb, _ = GetCpmStringValue(bid.bid.Price, targData.priceGranularity)
+			pb = GetPriceBucket(bid.bid.Price, targData.priceGranularity)
 
 			newDur := duration
 			if len(requestExt.Prebid.Targeting.DurationRangeSec) > 0 {
diff --git a/exchange/price_granularity.go b/exchange/price_granularity.go
index 27b0302d..242d420f 100644
--- a/exchange/price_granularity.go
+++ b/exchange/price_granularity.go
@@ -7,33 +7,32 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
-// DEFAULT_PRECISION should be taken care of in openrtb_ext/request.go, but throwing an additional safety check here.
-
-// GetCpmStringValue is the externally facing function for computing CPM buckets
-func GetCpmStringValue(cpm float64, config openrtb_ext.PriceGranularity) (string, error) {
+// GetPriceBucket is the externally facing function for computing CPM buckets
+func GetPriceBucket(cpm float64, config openrtb_ext.PriceGranularity) string {
 	cpmStr := ""
 	bucketMax := 0.0
 	increment := 0.0
 	precision := config.Precision
-	// calculate max of highest bucket
+
 	for i := 0; i < len(config.Ranges); i++ {
 		if config.Ranges[i].Max > bucketMax {
 			bucketMax = config.Ranges[i].Max
 		}
-	} // calculate which bucket cpm is in
-	if cpm > bucketMax {
-		// If we are over max, just return that
-		return strconv.FormatFloat(bucketMax, 'f', precision, 64), nil
-	}
-	for i := 0; i < len(config.Ranges); i++ {
+		// find what range cpm is in
 		if cpm >= config.Ranges[i].Min && cpm <= config.Ranges[i].Max {
 			increment = config.Ranges[i].Increment
 		}
 	}
-	if increment > 0 {
+
+	if cpm > bucketMax {
+		// We are over max, just return that
+		cpmStr = strconv.FormatFloat(bucketMax, 'f', precision, 64)
+	} else if increment > 0 {
+		// If increment exists, get cpm string value
 		cpmStr = getCpmTarget(cpm, increment, precision)
 	}
-	return cpmStr, nil
+
+	return cpmStr
 }
 
 func getCpmTarget(cpm float64, increment float64, precision int) string {
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index 08df7c59..79a76c6f 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -8,6 +8,7 @@ import (
 // FirstPartyDataContextExtKey defines the field name within bidrequest.ext reserved
 // for first party data support.
 const FirstPartyDataContextExtKey string = "context"
+const MaxDecimalFigures int = 15
 
 // ExtRequest defines the contract for bidrequest.ext
 type ExtRequest struct {
@@ -179,6 +180,9 @@ func (pg *PriceGranularity) UnmarshalJSON(b []byte) error {
 	if pgraw.Precision < 0 {
 		return errors.New("Price granularity error: precision must be non-negative")
 	}
+	if pgraw.Precision > MaxDecimalFigures {
+		return errors.New("Price granularity error: precision of more than 15 significant figures is not supported")
+	}
 	if len(pgraw.Ranges) > 0 {
 		var prevMax float64 = 0
 		for i, gr := range pgraw.Ranges {
@@ -190,9 +194,6 @@ func (pg *PriceGranularity) UnmarshalJSON(b []byte) error {
 			}
 			// Enforce that we don't read "min" from the request
 			pgraw.Ranges[i].Min = prevMax
-			if pgraw.Ranges[i].Min < prevMax {
-				return errors.New("Price granularity error: overlapping granularity ranges")
-			}
 			prevMax = gr.Max
 		}
 		*pg = PriceGranularity(pgraw)
diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go
index 6a36f9e7..903b17bf 100644
--- a/pbsmetrics/config/metrics.go
+++ b/pbsmetrics/config/metrics.go
@@ -104,6 +104,20 @@ func (me *MultiMetricsEngine) RecordRequestTime(labels pbsmetrics.Labels, length
 	}
 }
 
+// RecordStoredDataFetchTime across all engines
+func (me *MultiMetricsEngine) RecordStoredDataFetchTime(labels pbsmetrics.StoredDataLabels, length time.Duration) {
+	for _, thisME := range *me {
+		thisME.RecordStoredDataFetchTime(labels, length)
+	}
+}
+
+// RecordStoredDataError across all engines
+func (me *MultiMetricsEngine) RecordStoredDataError(labels pbsmetrics.StoredDataLabels) {
+	for _, thisME := range *me {
+		thisME.RecordStoredDataError(labels)
+	}
+}
+
 // RecordAdapterPanic across all engines
 func (me *MultiMetricsEngine) RecordAdapterPanic(labels pbsmetrics.AdapterLabels) {
 	for _, thisME := range *me {
@@ -244,6 +258,14 @@ func (me *DummyMetricsEngine) RecordLegacyImps(labels pbsmetrics.Labels, numImps
 func (me *DummyMetricsEngine) RecordRequestTime(labels pbsmetrics.Labels, length time.Duration) {
 }
 
+// RecordStoredDataFetchTime as a noop
+func (me *DummyMetricsEngine) RecordStoredDataFetchTime(labels pbsmetrics.StoredDataLabels, length time.Duration) {
+}
+
+// RecordStoredDataError as a noop
+func (me *DummyMetricsEngine) RecordStoredDataError(labels pbsmetrics.StoredDataLabels) {
+}
+
 // RecordAdapterPanic as a noop
 func (me *DummyMetricsEngine) RecordAdapterPanic(labels pbsmetrics.AdapterLabels) {
 }
diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go
index 26f6ce07..3842bfd2 100644
--- a/pbsmetrics/go_metrics.go
+++ b/pbsmetrics/go_metrics.go
@@ -27,6 +27,8 @@ type Metrics struct {
 	RequestsQueueTimer             map[RequestType]map[bool]metrics.Timer
 	PrebidCacheRequestTimerSuccess metrics.Timer
 	PrebidCacheRequestTimerError   metrics.Timer
+	StoredDataFetchTimer           map[StoredDataType]map[StoredDataFetchType]metrics.Timer
+	StoredDataErrorMeter           map[StoredDataType]map[StoredDataError]metrics.Meter
 	StoredReqCacheMeter            map[CacheResult]metrics.Meter
 	StoredImpCacheMeter            map[CacheResult]metrics.Meter
 	DNSLookupTimer                 metrics.Timer
@@ -131,6 +133,8 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 		RequestsQueueTimer:             make(map[RequestType]map[bool]metrics.Timer),
 		PrebidCacheRequestTimerSuccess: blankTimer,
 		PrebidCacheRequestTimerError:   blankTimer,
+		StoredDataFetchTimer:           make(map[StoredDataType]map[StoredDataFetchType]metrics.Timer),
+		StoredDataErrorMeter:           make(map[StoredDataType]map[StoredDataError]metrics.Meter),
 		StoredReqCacheMeter:            make(map[CacheResult]metrics.Meter),
 		StoredImpCacheMeter:            make(map[CacheResult]metrics.Meter),
 		AmpNoCookieMeter:               blankMeter,
@@ -183,6 +187,17 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 		newMetrics.PrivacyTCFRequestVersion[v] = blankMeter
 	}
 
+	for _, dt := range StoredDataTypes() {
+		newMetrics.StoredDataFetchTimer[dt] = make(map[StoredDataFetchType]metrics.Timer)
+		newMetrics.StoredDataErrorMeter[dt] = make(map[StoredDataError]metrics.Meter)
+		for _, ft := range StoredDataFetchTypes() {
+			newMetrics.StoredDataFetchTimer[dt][ft] = blankTimer
+		}
+		for _, e := range StoredDataErrors() {
+			newMetrics.StoredDataErrorMeter[dt][e] = blankMeter
+		}
+	}
+
 	//to minimize memory usage, queuedTimeout metric is now supported for video endpoint only
 	//boolean value represents 2 general request statuses: accepted and rejected
 	newMetrics.RequestsQueueTimer["video"] = make(map[bool]metrics.Timer)
@@ -218,6 +233,17 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d
 	newMetrics.PrebidCacheRequestTimerSuccess = metrics.GetOrRegisterTimer("prebid_cache_request_time.ok", registry)
 	newMetrics.PrebidCacheRequestTimerError = metrics.GetOrRegisterTimer("prebid_cache_request_time.err", registry)
 
+	for _, dt := range StoredDataTypes() {
+		for _, ft := range StoredDataFetchTypes() {
+			timerName := fmt.Sprintf("stored_%s_fetch_time.%s", string(dt), string(ft))
+			newMetrics.StoredDataFetchTimer[dt][ft] = metrics.GetOrRegisterTimer(timerName, registry)
+		}
+		for _, e := range StoredDataErrors() {
+			meterName := fmt.Sprintf("stored_%s_error.%s", string(dt), string(e))
+			newMetrics.StoredDataErrorMeter[dt][e] = metrics.GetOrRegisterMeter(meterName, registry)
+		}
+	}
+
 	newMetrics.AmpNoCookieMeter = metrics.GetOrRegisterMeter("amp_no_cookie_requests", registry)
 	newMetrics.CookieSyncMeter = metrics.GetOrRegisterMeter("cookie_sync_requests", registry)
 	newMetrics.userSyncBadRequest = metrics.GetOrRegisterMeter("usersync.bad_requests", registry)
@@ -444,6 +470,16 @@ func (me *Metrics) RecordRequestTime(labels Labels, length time.Duration) {
 	}
 }
 
+// RecordStoredDataFetchTime implements a part of the MetricsEngine interface
+func (me *Metrics) RecordStoredDataFetchTime(labels StoredDataLabels, length time.Duration) {
+	me.StoredDataFetchTimer[labels.DataType][labels.DataFetchType].Update(length)
+}
+
+// RecordStoredDataError implements a part of the MetricsEngine interface
+func (me *Metrics) RecordStoredDataError(labels StoredDataLabels) {
+	me.StoredDataErrorMeter[labels.DataType][labels.Error].Mark(1)
+}
+
 // RecordAdapterPanic implements a part of the MetricsEngine interface
 func (me *Metrics) RecordAdapterPanic(labels AdapterLabels) {
 	am, ok := me.AdapterMetrics[labels.Adapter]
diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go
index 8133bc73..7f9d66b5 100644
--- a/pbsmetrics/metrics.go
+++ b/pbsmetrics/metrics.go
@@ -51,6 +51,60 @@ type PrivacyLabels struct {
 	LMTEnforced    bool
 }
 
+type StoredDataType string
+
+const (
+	AccountDataType  StoredDataType = "account"
+	AMPDataType      StoredDataType = "amp"
+	CategoryDataType StoredDataType = "category"
+	RequestDataType  StoredDataType = "request"
+	VideoDataType    StoredDataType = "video"
+)
+
+func StoredDataTypes() []StoredDataType {
+	return []StoredDataType{
+		AccountDataType,
+		AMPDataType,
+		CategoryDataType,
+		RequestDataType,
+		VideoDataType,
+	}
+}
+
+type StoredDataFetchType string
+
+const (
+	FetchAll   StoredDataFetchType = "all"
+	FetchDelta StoredDataFetchType = "delta"
+)
+
+func StoredDataFetchTypes() []StoredDataFetchType {
+	return []StoredDataFetchType{
+		FetchAll,
+		FetchDelta,
+	}
+}
+
+type StoredDataLabels struct {
+	DataType      StoredDataType
+	DataFetchType StoredDataFetchType
+	Error         StoredDataError
+}
+
+type StoredDataError string
+
+const (
+	StoredDataErrorNetwork   StoredDataError = "network"
+	StoredDataErrorUndefined StoredDataError = "undefined"
+)
+
+func StoredDataErrors() []StoredDataError {
+	return []StoredDataError{
+		StoredDataErrorNetwork,
+		StoredDataErrorUndefined,
+	}
+}
+
 // Label typecasting. Se below the type definitions for possible values
 
 // DemandSource : Demand source enumeration
@@ -314,6 +368,8 @@ type MetricsEngine interface {
 	RecordUserIDSet(userLabels UserLabels) // Function should verify bidder values
 	RecordStoredReqCacheResult(cacheResult CacheResult, inc int)
 	RecordStoredImpCacheResult(cacheResult CacheResult, inc int)
+	RecordStoredDataFetchTime(labels StoredDataLabels, length time.Duration)
+	RecordStoredDataError(labels StoredDataLabels)
 	RecordPrebidCacheRequestTime(success bool, length time.Duration)
 	RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration)
 	RecordTimeoutNotice(sucess bool)
diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go
index 42a2d1b4..63b05196 100644
--- a/pbsmetrics/metrics_mock.go
+++ b/pbsmetrics/metrics_mock.go
@@ -42,6 +42,16 @@ func (me *MetricsEngineMock) RecordRequestTime(labels Labels, length time.Durati
 	me.Called(labels, length)
 }
 
+// RecordStoredDataFetchTime mock
+func (me *MetricsEngineMock) RecordStoredDataFetchTime(labels StoredDataLabels, length time.Duration) {
+	me.Called(labels, length)
+}
+
+// RecordStoredDataError mock
+func (me *MetricsEngineMock) RecordStoredDataError(labels StoredDataLabels) {
+	me.Called(labels)
+}
+
 // RecordAdapterPanic mock
 func (me *MetricsEngineMock) RecordAdapterPanic(labels AdapterLabels) {
 	me.Called(labels)
diff --git a/pbsmetrics/prometheus/preload.go b/pbsmetrics/prometheus/preload.go
index 4f62a18a..52620bc6 100644
--- a/pbsmetrics/prometheus/preload.go
+++ b/pbsmetrics/prometheus/preload.go
@@ -7,17 +7,19 @@ import (
 
 func preloadLabelValues(m *Metrics) {
 	var (
-		actionValues          = actionsAsString()
-		adapterErrorValues    = adapterErrorsAsString()
-		adapterValues         = adaptersAsString()
-		bidTypeValues         = []string{markupDeliveryAdm, markupDeliveryNurl}
-		boolValues            = boolValuesAsString()
-		cacheResultValues     = cacheResultsAsString()
-		connectionErrorValues = []string{connectionAcceptError, connectionCloseError}
-		cookieValues          = cookieTypesAsString()
-		requestStatusValues   = requestStatusesAsString()
-		requestTypeValues     = requestTypesAsString()
-		sourceValues          = []string{sourceRequest}
+		actionValues              = actionsAsString()
+		adapterErrorValues        = adapterErrorsAsString()
+		adapterValues             = adaptersAsString()
+		bidTypeValues             = []string{markupDeliveryAdm, markupDeliveryNurl}
+		boolValues                = boolValuesAsString()
+		cacheResultValues         = cacheResultsAsString()
+		connectionErrorValues     = []string{connectionAcceptError, connectionCloseError}
+		cookieValues              = cookieTypesAsString()
+		requestStatusValues       = requestStatusesAsString()
+		requestTypeValues         = requestTypesAsString()
+		storedDataFetchTypeValues = storedDataFetchTypesAsString()
+		storedDataErrorValues     = storedDataErrorsAsString()
+		sourceValues              = []string{sourceRequest}
 	)
 
 	preloadLabelValuesForCounter(m.connectionsError, map[string][]string{
@@ -44,6 +46,46 @@ func preloadLabelValues(m *Metrics) {
 		requestTypeLabel: requestTypeValues,
 	})
 
+	preloadLabelValuesForHistogram(m.storedAccountFetchTimer, map[string][]string{
+		storedDataFetchTypeLabel: storedDataFetchTypeValues,
+	})
+
+	preloadLabelValuesForHistogram(m.storedAMPFetchTimer, map[string][]string{
+		storedDataFetchTypeLabel: storedDataFetchTypeValues,
+	})
+
+	preloadLabelValuesForHistogram(m.storedCategoryFetchTimer, map[string][]string{
+		storedDataFetchTypeLabel: storedDataFetchTypeValues,
+	})
+
+	preloadLabelValuesForHistogram(m.storedRequestFetchTimer, map[string][]string{
+		storedDataFetchTypeLabel: storedDataFetchTypeValues,
+	})
+
+	preloadLabelValuesForHistogram(m.storedVideoFetchTimer, map[string][]string{
+		storedDataFetchTypeLabel: storedDataFetchTypeValues,
+	})
+
+	preloadLabelValuesForCounter(m.storedAccountErrors, map[string][]string{
+		storedDataErrorLabel: storedDataErrorValues,
+	})
+
+	preloadLabelValuesForCounter(m.storedAMPErrors, map[string][]string{
+		storedDataErrorLabel: storedDataErrorValues,
+	})
+
+	preloadLabelValuesForCounter(m.storedCategoryErrors, map[string][]string{
+		storedDataErrorLabel: storedDataErrorValues,
+	})
+
+	preloadLabelValuesForCounter(m.storedRequestErrors, map[string][]string{
+		storedDataErrorLabel: storedDataErrorValues,
+	})
+
+	preloadLabelValuesForCounter(m.storedVideoErrors, map[string][]string{
+		storedDataErrorLabel: storedDataErrorValues,
+	})
+
 	preloadLabelValuesForCounter(m.requestsWithoutCookie, map[string][]string{
 		requestTypeLabel: requestTypeValues,
 	})
diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go
index b42399b2..046d0769 100644
--- a/pbsmetrics/prometheus/prometheus.go
+++ b/pbsmetrics/prometheus/prometheus.go
@@ -28,6 +28,16 @@ type Metrics struct {
 	requestsWithoutCookie        *prometheus.CounterVec
 	storedImpressionsCacheResult *prometheus.CounterVec
 	storedRequestCacheResult     *prometheus.CounterVec
+	storedAccountFetchTimer      *prometheus.HistogramVec
+	storedAccountErrors          *prometheus.CounterVec
+	storedAMPFetchTimer          *prometheus.HistogramVec
+	storedAMPErrors              *prometheus.CounterVec
+	storedCategoryFetchTimer     *prometheus.HistogramVec
+	storedCategoryErrors         *prometheus.CounterVec
+	storedRequestFetchTimer      *prometheus.HistogramVec
+	storedRequestErrors          *prometheus.CounterVec
+	storedVideoFetchTimer        *prometheus.HistogramVec
+	storedVideoErrors            *prometheus.CounterVec
 	timeoutNotifications         *prometheus.CounterVec
 	dnsLookupTimer               prometheus.Histogram
 	privacyCCPA                  *prometheus.CounterVec
@@ -102,6 +112,11 @@ const (
 	sourceRequest = "request"
 )
 
+const (
+	storedDataFetchTypeLabel = "stored_data_fetch_type"
+	storedDataErrorLabel     = "stored_data_error"
+)
+
 // NewMetrics initializes a new Prometheus metrics instance with preloaded label values.
 func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMetrics) *Metrics {
 	standardTimeBuckets := []float64{0.05, 0.1, 0.15, 0.20, 0.25, 0.3, 0.4, 0.5, 0.75, 1}
@@ -171,6 +186,61 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet
 		"Count of stored request cache requests attempts by hits or miss.",
 		[]string{cacheResultLabel})
 
+	metrics.storedAccountFetchTimer = newHistogramVec(cfg, metrics.Registry,
+		"stored_account_fetch_time_seconds",
+		"Seconds to fetch stored accounts labeled by fetch type",
+		[]string{storedDataFetchTypeLabel},
+		standardTimeBuckets)
+
+	metrics.storedAccountErrors = newCounter(cfg, metrics.Registry,
+		"stored_account_errors",
+		"Count of stored account errors by error type",
+		[]string{storedDataErrorLabel})
+
+	metrics.storedAMPFetchTimer = newHistogramVec(cfg, metrics.Registry,
+		"stored_amp_fetch_time_seconds",
+		"Seconds to fetch stored AMP requests labeled by fetch type",
+		[]string{storedDataFetchTypeLabel},
+		standardTimeBuckets)
+
+	metrics.storedAMPErrors = newCounter(cfg, metrics.Registry,
+		"stored_amp_errors",
+		"Count of stored AMP errors by error type",
+		[]string{storedDataErrorLabel})
+
+	metrics.storedCategoryFetchTimer = newHistogramVec(cfg, metrics.Registry,
+		"stored_category_fetch_time_seconds",
+		"Seconds to fetch stored categories labeled by fetch type",
+		[]string{storedDataFetchTypeLabel},
+		standardTimeBuckets)
+
+	metrics.storedCategoryErrors = newCounter(cfg, metrics.Registry,
+		"stored_category_errors",
+		"Count of stored category errors by error type",
+		[]string{storedDataErrorLabel})
+
+	metrics.storedRequestFetchTimer = newHistogramVec(cfg, metrics.Registry,
+		"stored_request_fetch_time_seconds",
+		"Seconds to fetch stored requests labeled by fetch type",
+		[]string{storedDataFetchTypeLabel},
+		standardTimeBuckets)
+
+	metrics.storedRequestErrors = newCounter(cfg, metrics.Registry,
+		"stored_request_errors",
+		"Count of stored request errors by error type",
+		[]string{storedDataErrorLabel})
+
+	metrics.storedVideoFetchTimer = newHistogramVec(cfg, metrics.Registry,
+		"stored_video_fetch_time_seconds",
+		"Seconds to fetch stored video labeled by fetch type",
+		[]string{storedDataFetchTypeLabel},
+		standardTimeBuckets)
+
+	metrics.storedVideoErrors = newCounter(cfg, metrics.Registry,
+		"stored_video_errors",
+		"Count of stored video errors by error type",
+		[]string{storedDataErrorLabel})
+
 	metrics.timeoutNotifications = newCounter(cfg, metrics.Registry,
 		"timeout_notification",
 		"Count of timeout notifications triggered, and if they were successfully sent.",
@@ -387,6 +457,56 @@ func (m *Metrics) RecordRequestTime(labels pbsmetrics.Labels, length time.Durati
 	}
 }
 
+func (m *Metrics) RecordStoredDataFetchTime(labels pbsmetrics.StoredDataLabels, length time.Duration) {
+	switch labels.DataType {
+	case pbsmetrics.AccountDataType:
+		m.storedAccountFetchTimer.With(prometheus.Labels{
+			storedDataFetchTypeLabel: string(labels.DataFetchType),
+		}).Observe(length.Seconds())
+	case pbsmetrics.AMPDataType:
+		m.storedAMPFetchTimer.With(prometheus.Labels{
+			storedDataFetchTypeLabel: string(labels.DataFetchType),
+		}).Observe(length.Seconds())
+	case pbsmetrics.CategoryDataType:
+		m.storedCategoryFetchTimer.With(prometheus.Labels{
+			storedDataFetchTypeLabel: string(labels.DataFetchType),
+		}).Observe(length.Seconds())
+	case pbsmetrics.RequestDataType:
+		m.storedRequestFetchTimer.With(prometheus.Labels{
+			storedDataFetchTypeLabel: string(labels.DataFetchType),
+		}).Observe(length.Seconds())
+	case pbsmetrics.VideoDataType:
+		m.storedVideoFetchTimer.With(prometheus.Labels{
+			storedDataFetchTypeLabel: string(labels.DataFetchType),
+		}).Observe(length.Seconds())
+	}
+}
+
+func (m *Metrics) RecordStoredDataError(labels pbsmetrics.StoredDataLabels) {
+	switch labels.DataType {
+	case pbsmetrics.AccountDataType:
+		m.storedAccountErrors.With(prometheus.Labels{
+			storedDataErrorLabel: string(labels.Error),
+		}).Inc()
+	case pbsmetrics.AMPDataType:
+		m.storedAMPErrors.With(prometheus.Labels{
+			storedDataErrorLabel: string(labels.Error),
+		}).Inc()
+	case pbsmetrics.CategoryDataType:
+		m.storedCategoryErrors.With(prometheus.Labels{
+			storedDataErrorLabel: string(labels.Error),
+		}).Inc()
+	case pbsmetrics.RequestDataType:
+		m.storedRequestErrors.With(prometheus.Labels{
+			storedDataErrorLabel: string(labels.Error),
+		}).Inc()
+	case pbsmetrics.VideoDataType:
+		m.storedVideoErrors.With(prometheus.Labels{
+			storedDataErrorLabel: string(labels.Error),
+		}).Inc()
+	}
+}
+
 func (m *Metrics) RecordAdapterRequest(labels pbsmetrics.AdapterLabels) {
 	m.adapterRequests.With(prometheus.Labels{
 		adapterLabel: string(labels.Adapter),
diff --git a/pbsmetrics/prometheus/type_conversion.go b/pbsmetrics/prometheus/type_conversion.go
index 55a7092e..75efb377 100644
--- a/pbsmetrics/prometheus/type_conversion.go
+++ b/pbsmetrics/prometheus/type_conversion.go
@@ -77,6 +77,33 @@ func requestTypesAsString() []string {
 	return valuesAsString
 }
 
+func storedDataTypesAsString() []string {
+	values := pbsmetrics.StoredDataTypes()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
+
+func storedDataFetchTypesAsString() []string {
+	values := pbsmetrics.StoredDataFetchTypes()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
+
+func storedDataErrorsAsString() []string {
+	values := pbsmetrics.StoredDataErrors()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
+
 func tcfVersionsAsString() []string {
 	values := pbsmetrics.TCFVersions()
 	valuesAsString := make([]string, len(values))
diff --git a/stored_requests/config/config.go b/stored_requests/config/config.go
index d32773a7..1036cf0a 100644
--- a/stored_requests/config/config.go
+++ b/stored_requests/config/config.go
@@ -65,7 +65,7 @@ func CreateStoredRequests(cfg *config.StoredRequests, metricsEngine pbsmetrics.M
 		}
 	}
 
-	eventProducers := newEventProducers(cfg, client, dbc.db, router)
+	eventProducers := newEventProducers(cfg, client, dbc.db, metricsEngine, router)
 	fetcher = newFetcher(cfg, client, dbc.db)
 
 	var shutdown1 func()
@@ -190,7 +190,7 @@ func newCache(cfg *config.StoredRequests) stored_requests.Cache {
 	return cache
 }
 
-func newEventProducers(cfg *config.StoredRequests, client *http.Client, db *sql.DB, router *httprouter.Router) (eventProducers []events.EventProducer) {
+func newEventProducers(cfg *config.StoredRequests, client *http.Client, db *sql.DB, metricsEngine pbsmetrics.MetricsEngine, router *httprouter.Router) (eventProducers []events.EventProducer) {
 	if cfg.CacheEvents.Enabled {
 		eventProducers = append(eventProducers, newEventsAPI(router, cfg.CacheEvents.Endpoint))
 	}
@@ -205,6 +205,7 @@ func newEventProducers(cfg *config.StoredRequests, client *http.Client, db *sql.
 			CacheInitTimeout:   time.Duration(cfg.Postgres.CacheInitialization.Timeout) * time.Millisecond,
 			CacheUpdateQuery:   cfg.Postgres.PollUpdates.Query,
 			CacheUpdateTimeout: time.Duration(cfg.Postgres.PollUpdates.Timeout) * time.Millisecond,
+			MetricsEngine:      metricsEngine,
 		}
 		pgEventProducer := postgresEvents.NewPostgresEventProducer(pgEventCfg)
 		fetchInterval := time.Duration(cfg.Postgres.PollUpdates.RefreshRate) * time.Second
diff --git a/stored_requests/events/postgres/database.go b/stored_requests/events/postgres/database.go
index cec9b504..8e1269c6 100644
--- a/stored_requests/events/postgres/database.go
+++ b/stored_requests/events/postgres/database.go
@@ -5,10 +5,12 @@ import (
 	"context"
 	"database/sql"
 	"encoding/json"
+	"net"
 	"time"
 
 	"github.com/golang/glog"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/prebid/prebid-server/stored_requests/events"
 	"github.com/prebid/prebid-server/util/timeutil"
 )
@@ -17,6 +19,14 @@ func bytesNull() []byte {
 	return []byte{'n', 'u', 'l', 'l'}
 }
 
+var storedDataTypeMetricMap = map[config.DataType]pbsmetrics.StoredDataType{
+	config.RequestDataType:    pbsmetrics.RequestDataType,
+	config.CategoryDataType:   pbsmetrics.CategoryDataType,
+	config.VideoDataType:      pbsmetrics.VideoDataType,
+	config.AMPRequestDataType: pbsmetrics.AMPDataType,
+	config.AccountDataType:    pbsmetrics.AccountDataType,
+}
+
 type PostgresEventProducerConfig struct {
 	DB                 *sql.DB
 	RequestType        config.DataType
@@ -24,6 +34,7 @@ type PostgresEventProducerConfig struct {
 	CacheInitTimeout   time.Duration
 	CacheUpdateQuery   string
 	CacheUpdateTimeout time.Duration
+	MetricsEngine      pbsmetrics.MetricsEngine
 }
 
 type PostgresEventProducer struct {
@@ -51,9 +62,9 @@ func NewPostgresEventProducer(cfg PostgresEventProducerConfig) (eventProducer *P
 func (e *PostgresEventProducer) Run() error {
 	if e.lastUpdate.IsZero() {
 		return e.fetchAll()
-	} else {
-		return e.fetchDelta()
 	}
+
+	return e.fetchDelta()
 }
 
 func (e *PostgresEventProducer) Saves() <-chan events.Save {
@@ -64,60 +75,96 @@ func (e *PostgresEventProducer) Invalidations() <-chan events.Invalidation {
 	return e.invalidations
 }
 
-func (e *PostgresEventProducer) fetchAll() error {
-	thisTimeInUTC := e.time.Now().UTC()
-
+func (e *PostgresEventProducer) fetchAll() (fetchErr error) {
 	timeout := e.cfg.CacheInitTimeout * time.Millisecond
 	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 	defer cancel()
 
+	startTime := e.time.Now().UTC()
 	rows, err := e.cfg.DB.QueryContext(ctx, e.cfg.CacheInitQuery)
+	elapsedTime := time.Since(startTime)
+	e.recordFetchTime(elapsedTime, pbsmetrics.FetchAll)
 
 	if err != nil {
 		glog.Warningf("Failed to fetch all Stored %s data from the DB: %v", e.cfg.RequestType, err)
+		if _, ok := err.(net.Error); ok {
+			e.recordError(pbsmetrics.StoredDataErrorNetwork)
+		} else {
+			e.recordError(pbsmetrics.StoredDataErrorUndefined)
+		}
 		return err
 	}
+
 	defer func() {
 		if err := rows.Close(); err != nil {
 			glog.Warningf("Failed to close the Stored %s DB connection: %v", e.cfg.RequestType, err)
+			e.recordError(pbsmetrics.StoredDataErrorUndefined)
+			fetchErr = err
 		}
 	}()
 	if err := e.sendEvents(rows); err != nil {
 		glog.Warningf("Failed to load all Stored %s data from the DB: %v", e.cfg.RequestType, err)
+		e.recordError(pbsmetrics.StoredDataErrorUndefined)
 		return err
-	} else {
-		e.lastUpdate = thisTimeInUTC
 	}
+
+	e.lastUpdate = startTime
 	return nil
 }
 
-func (e *PostgresEventProducer) fetchDelta() error {
-	thisTimeInUTC := e.time.Now().UTC()
-
+func (e *PostgresEventProducer) fetchDelta() (fetchErr error) {
 	timeout := e.cfg.CacheUpdateTimeout * time.Millisecond
 	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 	defer cancel()
 
+	startTime := e.time.Now().UTC()
 	rows, err := e.cfg.DB.QueryContext(ctx, e.cfg.CacheUpdateQuery, e.lastUpdate)
+	elapsedTime := time.Since(startTime)
+	e.recordFetchTime(elapsedTime, pbsmetrics.FetchDelta)
 
 	if err != nil {
 		glog.Warningf("Failed to fetch updated Stored %s data from the DB: %v", e.cfg.RequestType, err)
+		if _, ok := err.(net.Error); ok {
+			e.recordError(pbsmetrics.StoredDataErrorNetwork)
+		} else {
+			e.recordError(pbsmetrics.StoredDataErrorUndefined)
+		}
 		return err
 	}
+
 	defer func() {
 		if err := rows.Close(); err != nil {
 			glog.Warningf("Failed to close the Stored %s DB connection: %v", e.cfg.RequestType, err)
+			e.recordError(pbsmetrics.StoredDataErrorUndefined)
+			fetchErr = err
 		}
 	}()
 	if err := e.sendEvents(rows); err != nil {
 		glog.Warningf("Failed to load updated Stored %s data from the DB: %v", e.cfg.RequestType, err)
+		e.recordError(pbsmetrics.StoredDataErrorUndefined)
 		return err
-	} else {
-		e.lastUpdate = thisTimeInUTC
 	}
+
+	e.lastUpdate = startTime
 	return nil
 }
 
+func (e *PostgresEventProducer) recordFetchTime(elapsedTime time.Duration, fetchType pbsmetrics.StoredDataFetchType) {
+	e.cfg.MetricsEngine.RecordStoredDataFetchTime(
+		pbsmetrics.StoredDataLabels{
+			DataType:      storedDataTypeMetricMap[e.cfg.RequestType],
+			DataFetchType: fetchType,
+		}, elapsedTime)
+}
+
+func (e *PostgresEventProducer) recordError(errorType pbsmetrics.StoredDataError) {
+	e.cfg.MetricsEngine.RecordStoredDataError(
+		pbsmetrics.StoredDataLabels{
+			DataType: storedDataTypeMetricMap[e.cfg.RequestType],
+			Error:    errorType,
+		})
+}
+
 // sendEvents reads the rows and sends notifications into the channel for any updates.
 // If it returns an error, then callers can be certain that no events were sent to the channels.
 func (e *PostgresEventProducer) sendEvents(rows *sql.Rows) (err error) {


Test patch:
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 99c351c4..ee7e27d0 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -955,9 +955,13 @@ func TestBidResponseCurrency(t *testing.T) {
 		},
 	}
 
+	bidResponseExt := &openrtb_ext.ExtBidResponse{
+		ResponseTimeMillis:   map[openrtb_ext.BidderName]int{openrtb_ext.BidderName("appnexus"): 5},
+		RequestTimeoutMillis: 500,
+	}
 	// Run tests
 	for i := range testCases {
-		actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, nil, true, errList)
+		actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, 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))
 	}
diff --git a/exchange/price_granularity_test.go b/exchange/price_granularity_test.go
index 09af1fcd..13840838 100644
--- a/exchange/price_granularity_test.go
+++ b/exchange/price_granularity_test.go
@@ -1,9 +1,11 @@
 package exchange
 
 import (
+	"math"
 	"testing"
 
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestGetPriceBucketString(t *testing.T) {
@@ -28,32 +30,105 @@ func TestGetPriceBucketString(t *testing.T) {
 		},
 	}
 
-	price := 1.87
-	getOnePriceBucket(t, "low", low, price, "1.50")
-	getOnePriceBucket(t, "medium", medium, price, "1.80")
-	getOnePriceBucket(t, "high", high, price, "1.87")
-	getOnePriceBucket(t, "auto", auto, price, "1.85")
-	getOnePriceBucket(t, "dense", dense, price, "1.87")
-	getOnePriceBucket(t, "custom1", custom1, price, "1.86")
-
-	// test a cpm above the max in low price bucket
-	price = 5.72
-	getOnePriceBucket(t, "low", low, price, "5.00")
-	getOnePriceBucket(t, "medium", medium, price, "5.70")
-	getOnePriceBucket(t, "high", high, price, "5.72")
-	getOnePriceBucket(t, "auto", auto, price, "5.70")
-	getOnePriceBucket(t, "dense", dense, price, "5.70")
-	getOnePriceBucket(t, "custom1", custom1, price, "5.70")
+	// Define test cases
+	type aTest struct {
+		granularityId       string
+		granularity         openrtb_ext.PriceGranularity
+		expectedPriceBucket string
+	}
+	testGroups := []struct {
+		groupDesc string
+		cpm       float64
+		testCases []aTest
+	}{
+		{
+			groupDesc: "cpm below the max in every price bucket",
+			cpm:       1.87,
+			testCases: []aTest{
+				{"low", low, "1.50"},
+				{"medium", medium, "1.80"},
+				{"high", high, "1.87"},
+				{"auto", auto, "1.85"},
+				{"dense", dense, "1.87"},
+				{"custom1", custom1, "1.86"},
+			},
+		},
+		{
+			groupDesc: "cpm above the max in low price bucket",
+			cpm:       5.72,
+			testCases: []aTest{
+				{"low", low, "5.00"},
+				{"medium", medium, "5.70"},
+				{"high", high, "5.72"},
+				{"auto", auto, "5.70"},
+				{"dense", dense, "5.70"},
+				{"custom1", custom1, "5.70"},
+			},
+		},
+		{
+			groupDesc: "Precision value corner cases",
+			cpm:       1.876,
+			testCases: []aTest{
+				{
+					"Negative precision defaults to number of digits already in CPM float",
+					openrtb_ext.PriceGranularity{Precision: -1, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}},
+					"1.85",
+				},
+				{
+					"Precision value equals zero, we expect to round up to the nearest integer",
+					openrtb_ext.PriceGranularity{Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}},
+					"2",
+				},
+				{
+					"Largest precision value PBS supports 15",
+					openrtb_ext.PriceGranularity{Precision: 15, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}},
+					"1.850000000000000",
+				},
+			},
+		},
+		{
+			groupDesc: "Increment value corner cases",
+			cpm:       1.876,
+			testCases: []aTest{
+				{
+					"Negative increment, return empty string",
+					openrtb_ext.PriceGranularity{Precision: 2, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: -0.05}}},
+					"",
+				},
+				{
+					"Zero increment, return empty string",
+					openrtb_ext.PriceGranularity{Precision: 2, Ranges: []openrtb_ext.GranularityRange{{Max: 5}}},
+					"",
+				},
+				{
+					"Increment value is greater than CPM itself, return zero float value",
+					openrtb_ext.PriceGranularity{Precision: 2, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 1.877}}},
+					"0.00",
+				},
+			},
+		},
+		{
+			groupDesc: "Negative Cpm, return empty string since it does not belong into any range",
+			cpm:       -1.876,
+			testCases: []aTest{{"low", low, ""}},
+		},
+		{
+			groupDesc: "Zero value Cpm, return the same, only in string format",
+			cpm:       0,
+			testCases: []aTest{{"low", low, "0.00"}},
+		},
+		{
+			groupDesc: "Large Cpm, return bucket Max",
+			cpm:       math.MaxFloat64,
+			testCases: []aTest{{"low", low, "5.00"}},
+		},
+	}
 
-}
+	for _, testGroup := range testGroups {
+		for _, test := range testGroup.testCases {
+			priceBucket := GetPriceBucket(testGroup.cpm, test.granularity)
 
-func getOnePriceBucket(t *testing.T, name string, granularity openrtb_ext.PriceGranularity, price float64, expected string) {
-	t.Helper()
-	priceBucket, err := GetCpmStringValue(price, granularity)
-	if err != nil {
-		t.Errorf("Granularity: %s :: GetPriceBucketString: %s", name, err.Error())
-	}
-	if priceBucket != expected {
-		t.Errorf("Granularity: %s :: Expected %s, got %s from %f", name, expected, priceBucket, price)
+			assert.Equal(t, test.expectedPriceBucket, priceBucket, "Group: %s Granularity: %s :: Expected %s, got %s from %f", testGroup.groupDesc, test.granularityId, test.expectedPriceBucket, priceBucket, testGroup.cpm)
+		}
 	}
 }
diff --git a/openrtb_ext/request_test.go b/openrtb_ext/request_test.go
index e4046a62..98a2e164 100644
--- a/openrtb_ext/request_test.go
+++ b/openrtb_ext/request_test.go
@@ -190,21 +190,43 @@ var validGranularityTests []granularityTestData = []granularityTestData{
 }
 
 func TestGranularityUnmarshalBad(t *testing.T) {
-	tests := [][]byte{
-		[]byte(`[]`),
-		[]byte(`{"precision": -1, "ranges": [{"max":20, "increment":0.5}]}`),
-		[]byte(`{"ranges":[{"max":20, "increment": -1}]}`),
-		[]byte(`{"ranges":[{"max":"20", "increment": "0.1"}]}`),
-		[]byte(`{"ranges":[{"max":20, "increment":0.1}. {"max":10, "increment":0.02}]}`),
-		[]byte(`{"ranges":[{"max":20, "min":10, "increment": 0.1}, {"max":10, "min":0, "increment":0.05}]}`),
-		[]byte(`{"ranges":[{"max":1.0, "increment": 0.07}, {"max" 1.0, "increment": 0.03}]}`),
+	testCases := []struct {
+		description          string
+		jsonPriceGranularity []byte
+	}{
+		{
+			"Malformed",
+			[]byte(`[]`),
+		},
+		{
+			"Negative precision",
+			[]byte(`{"precision": -1, "ranges": [{"max":20, "increment":0.5}]}`),
+		},
+		{
+			"Precision greater than MaxDecimalFigures supported",
+			[]byte(`{"precision": 16, "ranges": [{"max":20, "increment":0.5}]}`),
+		},
+		{
+			"Negative increment",
+			[]byte(`{"ranges":[{"max":20, "increment": -1}]}`),
+		},
+		{
+			"Range with non float64 max value",
+			[]byte(`{"ranges":[{"max":"20", "increment": "0.1"}]}`),
+		},
+		{
+			"Ranges in decreasing order",
+			[]byte(`{"ranges":[{"max":20, "increment":0.1}. {"max":10, "increment":0.02}]}`),
+		},
+		{
+			"Max equal to previous max",
+			[]byte(`{"ranges":[{"max":1.0, "increment": 0.07}, {"max" 1.0, "increment": 0.03}]}`),
+		},
 	}
-	var resolved PriceGranularity
-	for _, b := range tests {
-		resolved = PriceGranularity{}
-		err := json.Unmarshal(b, &resolved)
-		if err == nil {
-			t.Errorf("Invalid granularity unmarshalled without error.\nJSON was: %s\n Resolved to: %v", string(b), resolved)
-		}
+
+	for _, test := range testCases {
+		resolved := PriceGranularity{}
+		err := json.Unmarshal(test.jsonPriceGranularity, &resolved)
+		assert.Errorf(t, err, "Invalid granularity unmarshalled without error.\nJSON was: %s\n Resolved to: %v. Test: %s", string(test.jsonPriceGranularity), resolved, test.description)
 	}
 }
diff --git a/pbsmetrics/go_metrics_test.go b/pbsmetrics/go_metrics_test.go
index f6769916..0a763750 100644
--- a/pbsmetrics/go_metrics_test.go
+++ b/pbsmetrics/go_metrics_test.go
@@ -345,6 +345,151 @@ func TestRecordPrebidCacheRequestTimeWithNotSuccess(t *testing.T) {
 	assert.Equal(t, m.PrebidCacheRequestTimerError.Count(), int64(1))
 }
 
+func TestRecordStoredDataFetchTime(t *testing.T) {
+	tests := []struct {
+		description string
+		dataType    StoredDataType
+		fetchType   StoredDataFetchType
+	}{
+		{
+			description: "Update stored_account_fetch_time.all timer",
+			dataType:    AccountDataType,
+			fetchType:   FetchAll,
+		},
+		{
+			description: "Update stored_amp_fetch_time.all timer",
+			dataType:    AMPDataType,
+			fetchType:   FetchAll,
+		},
+		{
+			description: "Update stored_category_fetch_time.all timer",
+			dataType:    CategoryDataType,
+			fetchType:   FetchAll,
+		},
+		{
+			description: "Update stored_request_fetch_time.all timer",
+			dataType:    RequestDataType,
+			fetchType:   FetchAll,
+		},
+		{
+			description: "Update stored_video_fetch_time.all timer",
+			dataType:    VideoDataType,
+			fetchType:   FetchAll,
+		},
+		{
+			description: "Update stored_account_fetch_time.delta timer",
+			dataType:    AccountDataType,
+			fetchType:   FetchDelta,
+		},
+		{
+			description: "Update stored_amp_fetch_time.delta timer",
+			dataType:    AMPDataType,
+			fetchType:   FetchDelta,
+		},
+		{
+			description: "Update stored_category_fetch_time.delta timer",
+			dataType:    CategoryDataType,
+			fetchType:   FetchDelta,
+		},
+		{
+			description: "Update stored_request_fetch_time.delta timer",
+			dataType:    RequestDataType,
+			fetchType:   FetchDelta,
+		},
+		{
+			description: "Update stored_video_fetch_time.delta timer",
+			dataType:    VideoDataType,
+			fetchType:   FetchDelta,
+		},
+	}
+
+	for _, tt := range tests {
+		registry := metrics.NewRegistry()
+		m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true})
+		m.RecordStoredDataFetchTime(StoredDataLabels{
+			DataType:      tt.dataType,
+			DataFetchType: tt.fetchType,
+		}, time.Duration(500))
+
+		actualCount := m.StoredDataFetchTimer[tt.dataType][tt.fetchType].Count()
+		assert.Equal(t, int64(1), actualCount, tt.description)
+
+		actualDuration := m.StoredDataFetchTimer[tt.dataType][tt.fetchType].Sum()
+		assert.Equal(t, int64(500), actualDuration, tt.description)
+	}
+}
+
+func TestRecordStoredDataError(t *testing.T) {
+	tests := []struct {
+		description string
+		dataType    StoredDataType
+		errorType   StoredDataError
+	}{
+		{
+			description: "Increment stored_account_error.network meter",
+			dataType:    AccountDataType,
+			errorType:   StoredDataErrorNetwork,
+		},
+		{
+			description: "Increment stored_amp_error.network meter",
+			dataType:    AMPDataType,
+			errorType:   StoredDataErrorNetwork,
+		},
+		{
+			description: "Increment stored_category_error.network meter",
+			dataType:    CategoryDataType,
+			errorType:   StoredDataErrorNetwork,
+		},
+		{
+			description: "Increment stored_request_error.network meter",
+			dataType:    RequestDataType,
+			errorType:   StoredDataErrorNetwork,
+		},
+		{
+			description: "Increment stored_video_error.network meter",
+			dataType:    VideoDataType,
+			errorType:   StoredDataErrorNetwork,
+		},
+		{
+			description: "Increment stored_account_error.undefined meter",
+			dataType:    AccountDataType,
+			errorType:   StoredDataErrorUndefined,
+		},
+		{
+			description: "Increment stored_amp_error.undefined meter",
+			dataType:    AMPDataType,
+			errorType:   StoredDataErrorUndefined,
+		},
+		{
+			description: "Increment stored_category_error.undefined meter",
+			dataType:    CategoryDataType,
+			errorType:   StoredDataErrorUndefined,
+		},
+		{
+			description: "Increment stored_request_error.undefined meter",
+			dataType:    RequestDataType,
+			errorType:   StoredDataErrorUndefined,
+		},
+		{
+			description: "Increment stored_video_error.undefined meter",
+			dataType:    VideoDataType,
+			errorType:   StoredDataErrorUndefined,
+		},
+	}
+
+	for _, tt := range tests {
+		registry := metrics.NewRegistry()
+		m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true})
+		m.RecordStoredDataError(StoredDataLabels{
+			DataType: tt.dataType,
+			Error:    tt.errorType,
+		})
+
+		actualCount := m.StoredDataErrorMeter[tt.dataType][tt.errorType].Count()
+		assert.Equal(t, int64(1), actualCount, tt.description)
+	}
+}
+
 func TestRecordRequestPrivacy(t *testing.T) {
 	registry := metrics.NewRegistry()
 	m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true})
diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go
index b6153b16..46be7005 100644
--- a/pbsmetrics/prometheus/prometheus_test.go
+++ b/pbsmetrics/prometheus/prometheus_test.go
@@ -407,6 +407,193 @@ func TestRequestTimeMetric(t *testing.T) {
 	}
 }
 
+func TestRecordStoredDataFetchTime(t *testing.T) {
+	tests := []struct {
+		description string
+		dataType    pbsmetrics.StoredDataType
+		fetchType   pbsmetrics.StoredDataFetchType
+	}{
+		{
+			description: "Update stored account histogram with all label",
+			dataType:    pbsmetrics.AccountDataType,
+			fetchType:   pbsmetrics.FetchAll,
+		},
+		{
+			description: "Update stored AMP histogram with all label",
+			dataType:    pbsmetrics.AMPDataType,
+			fetchType:   pbsmetrics.FetchAll,
+		},
+		{
+			description: "Update stored category histogram with all label",
+			dataType:    pbsmetrics.CategoryDataType,
+			fetchType:   pbsmetrics.FetchAll,
+		},
+		{
+			description: "Update stored request histogram with all label",
+			dataType:    pbsmetrics.RequestDataType,
+			fetchType:   pbsmetrics.FetchAll,
+		},
+		{
+			description: "Update stored video histogram with all label",
+			dataType:    pbsmetrics.VideoDataType,
+			fetchType:   pbsmetrics.FetchAll,
+		},
+		{
+			description: "Update stored account histogram with delta label",
+			dataType:    pbsmetrics.AccountDataType,
+			fetchType:   pbsmetrics.FetchDelta,
+		},
+		{
+			description: "Update stored AMP histogram with delta label",
+			dataType:    pbsmetrics.AMPDataType,
+			fetchType:   pbsmetrics.FetchDelta,
+		},
+		{
+			description: "Update stored category histogram with delta label",
+			dataType:    pbsmetrics.CategoryDataType,
+			fetchType:   pbsmetrics.FetchDelta,
+		},
+		{
+			description: "Update stored request histogram with delta label",
+			dataType:    pbsmetrics.RequestDataType,
+			fetchType:   pbsmetrics.FetchDelta,
+		},
+		{
+			description: "Update stored video histogram with delta label",
+			dataType:    pbsmetrics.VideoDataType,
+			fetchType:   pbsmetrics.FetchDelta,
+		},
+	}
+
+	for _, tt := range tests {
+		m := createMetricsForTesting()
+
+		fetchTime := time.Duration(0.5 * float64(time.Second))
+		m.RecordStoredDataFetchTime(pbsmetrics.StoredDataLabels{
+			DataType:      tt.dataType,
+			DataFetchType: tt.fetchType,
+		}, fetchTime)
+
+		var metricsTimer *prometheus.HistogramVec
+		switch tt.dataType {
+		case pbsmetrics.AccountDataType:
+			metricsTimer = m.storedAccountFetchTimer
+		case pbsmetrics.AMPDataType:
+			metricsTimer = m.storedAMPFetchTimer
+		case pbsmetrics.CategoryDataType:
+			metricsTimer = m.storedCategoryFetchTimer
+		case pbsmetrics.RequestDataType:
+			metricsTimer = m.storedRequestFetchTimer
+		case pbsmetrics.VideoDataType:
+			metricsTimer = m.storedVideoFetchTimer
+		}
+
+		result := getHistogramFromHistogramVec(
+			metricsTimer,
+			storedDataFetchTypeLabel,
+			string(tt.fetchType))
+		assertHistogram(t, tt.description, result, 1, 0.5)
+	}
+}
+
+func TestRecordStoredDataError(t *testing.T) {
+	tests := []struct {
+		description string
+		dataType    pbsmetrics.StoredDataType
+		errorType   pbsmetrics.StoredDataError
+		metricName  string
+	}{
+		{
+			description: "Update stored_account_errors counter with network label",
+			dataType:    pbsmetrics.AccountDataType,
+			errorType:   pbsmetrics.StoredDataErrorNetwork,
+			metricName:  "stored_account_errors",
+		},
+		{
+			description: "Update stored_amp_errors counter with network label",
+			dataType:    pbsmetrics.AMPDataType,
+			errorType:   pbsmetrics.StoredDataErrorNetwork,
+			metricName:  "stored_amp_errors",
+		},
+		{
+			description: "Update stored_category_errors counter with network label",
+			dataType:    pbsmetrics.CategoryDataType,
+			errorType:   pbsmetrics.StoredDataErrorNetwork,
+			metricName:  "stored_category_errors",
+		},
+		{
+			description: "Update stored_request_errors counter with network label",
+			dataType:    pbsmetrics.RequestDataType,
+			errorType:   pbsmetrics.StoredDataErrorNetwork,
+			metricName:  "stored_request_errors",
+		},
+		{
+			description: "Update stored_video_errors counter with network label",
+			dataType:    pbsmetrics.VideoDataType,
+			errorType:   pbsmetrics.StoredDataErrorNetwork,
+			metricName:  "stored_video_errors",
+		},
+		{
+			description: "Update stored_account_errors counter with undefined label",
+			dataType:    pbsmetrics.AccountDataType,
+			errorType:   pbsmetrics.StoredDataErrorUndefined,
+			metricName:  "stored_account_errors",
+		},
+		{
+			description: "Update stored_amp_errors counter with undefined label",
+			dataType:    pbsmetrics.AMPDataType,
+			errorType:   pbsmetrics.StoredDataErrorUndefined,
+			metricName:  "stored_amp_errors",
+		},
+		{
+			description: "Update stored_category_errors counter with undefined label",
+			dataType:    pbsmetrics.CategoryDataType,
+			errorType:   pbsmetrics.StoredDataErrorUndefined,
+			metricName:  "stored_category_errors",
+		},
+		{
+			description: "Update stored_request_errors counter with undefined label",
+			dataType:    pbsmetrics.RequestDataType,
+			errorType:   pbsmetrics.StoredDataErrorUndefined,
+			metricName:  "stored_request_errors",
+		},
+		{
+			description: "Update stored_video_errors counter with undefined label",
+			dataType:    pbsmetrics.VideoDataType,
+			errorType:   pbsmetrics.StoredDataErrorUndefined,
+			metricName:  "stored_video_errors",
+		},
+	}
+
+	for _, tt := range tests {
+		m := createMetricsForTesting()
+		m.RecordStoredDataError(pbsmetrics.StoredDataLabels{
+			DataType: tt.dataType,
+			Error:    tt.errorType,
+		})
+
+		var metricsCounter *prometheus.CounterVec
+		switch tt.dataType {
+		case pbsmetrics.AccountDataType:
+			metricsCounter = m.storedAccountErrors
+		case pbsmetrics.AMPDataType:
+			metricsCounter = m.storedAMPErrors
+		case pbsmetrics.CategoryDataType:
+			metricsCounter = m.storedCategoryErrors
+		case pbsmetrics.RequestDataType:
+			metricsCounter = m.storedRequestErrors
+		case pbsmetrics.VideoDataType:
+			metricsCounter = m.storedVideoErrors
+		}
+
+		assertCounterVecValue(t, tt.description, tt.metricName, metricsCounter,
+			1,
+			prometheus.Labels{
+				storedDataErrorLabel: string(tt.errorType),
+			})
+	}
+}
+
 func TestAdapterBidReceivedMetric(t *testing.T) {
 	adapterName := "anyName"
 	performTest := func(m *Metrics, hasAdm bool) {
@@ -1240,6 +1427,8 @@ func getHistogramFromHistogramVecByTwoKeys(histogram *prometheus.HistogramVec, l
 				valInd := ind
 				if ind == 1 {
 					valInd = 0
+				} else {
+					valInd = 1
 				}
 				if m.Label[valInd].GetName() == label2Key && m.Label[valInd].GetValue() == label2Value {
 					result = *m.GetHistogram()
diff --git a/stored_requests/config/config_test.go b/stored_requests/config/config_test.go
index f225f74b..712fef32 100644
--- a/stored_requests/config/config_test.go
+++ b/stored_requests/config/config_test.go
@@ -14,11 +14,13 @@ import (
 	sqlmock "github.com/DATA-DOG/go-sqlmock"
 	"github.com/julienschmidt/httprouter"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
 	"github.com/prebid/prebid-server/stored_requests/backends/http_fetcher"
 	"github.com/prebid/prebid-server/stored_requests/events"
 	httpEvents "github.com/prebid/prebid-server/stored_requests/events/http"
+	"github.com/stretchr/testify/mock"
 )
 
 func typedConfig(dataType config.DataType, sr *config.StoredRequests) *config.StoredRequests {
@@ -76,7 +78,10 @@ func TestNewHTTPEvents(t *testing.T) {
 			Timeout:     1000,
 		},
 	}
-	evProducers := newEventProducers(cfg, server1.Client(), nil, nil)
+
+	metricsMock := &pbsmetrics.MetricsEngineMock{}
+
+	evProducers := newEventProducers(cfg, server1.Client(), nil, metricsMock, nil)
 	assertSliceLength(t, evProducers, 1)
 	assertHttpWithURL(t, evProducers[0], server1.URL)
 }
@@ -114,6 +119,10 @@ func TestNewInMemoryAccountCache(t *testing.T) {
 }
 
 func TestNewPostgresEventProducers(t *testing.T) {
+	metricsMock := &pbsmetrics.MetricsEngineMock{}
+	metricsMock.Mock.On("RecordStoredDataFetchTime", mock.Anything, mock.Anything).Return()
+	metricsMock.Mock.On("RecordStoredDataError", mock.Anything).Return()
+
 	cfg := &config.StoredRequests{
 		Postgres: config.PostgresConfig{
 			CacheInitialization: config.PostgresCacheInitializer{
@@ -134,10 +143,11 @@ func TestNewPostgresEventProducers(t *testing.T) {
 	}
 	mock.ExpectQuery("^" + regexp.QuoteMeta(cfg.Postgres.CacheInitialization.Query) + "$").WillReturnError(errors.New("Query failed"))
 
-	evProducers := newEventProducers(cfg, client, db, nil)
+	evProducers := newEventProducers(cfg, client, db, metricsMock, nil)
 	assertProducerLength(t, evProducers, 1)
 
 	assertExpectationsMet(t, mock)
+	metricsMock.AssertExpectations(t)
 }
 
 func TestNewEventsAPI(t *testing.T) {
diff --git a/stored_requests/events/postgres/database_test.go b/stored_requests/events/postgres/database_test.go
index da2e0868..4471ad41 100644
--- a/stored_requests/events/postgres/database_test.go
+++ b/stored_requests/events/postgres/database_test.go
@@ -7,8 +7,11 @@ import (
 	"testing"
 	"time"
 
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/prebid/prebid-server/stored_requests/events"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
 	sqlmock "github.com/DATA-DOG/go-sqlmock"
 )
@@ -88,13 +91,21 @@ func TestFetchAllSuccess(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		db, mock, _ := sqlmock.New()
-		mock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
+		db, dbMock, _ := sqlmock.New()
+		dbMock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
+
+		metricsMock := &pbsmetrics.MetricsEngineMock{}
+		metricsMock.Mock.On("RecordStoredDataFetchTime", pbsmetrics.StoredDataLabels{
+			DataType:      pbsmetrics.RequestDataType,
+			DataFetchType: pbsmetrics.FetchAll,
+		}, mock.Anything).Return()
 
 		eventProducer := NewPostgresEventProducer(PostgresEventProducerConfig{
 			DB:               db,
+			RequestType:      config.RequestDataType,
 			CacheInitTimeout: 100 * time.Millisecond,
 			CacheInitQuery:   fakeQuery,
+			MetricsEngine:    metricsMock,
 		})
 		eventProducer.time = &FakeTime{time: tt.giveFakeTime}
 		err := eventProducer.Run()
@@ -119,74 +130,76 @@ func TestFetchAllSuccess(t *testing.T) {
 		assert.Equal(t, tt.wantSavedImps, saves.Imps, tt.description)
 		assert.Equal(t, tt.wantInvalidatedReqs, invalidations.Requests, tt.description)
 		assert.Equal(t, tt.wantInvalidatedImps, invalidations.Imps, tt.description)
+
+		metricsMock.AssertExpectations(t)
 	}
 }
 
 func TestFetchAllErrors(t *testing.T) {
 	tests := []struct {
-		description         string
-		giveFakeTime        time.Time
-		giveMockRows        *sqlmock.Rows
-		wantReturnedError   bool
-		wantLastUpdate      time.Time
-		wantSavedReqs       map[string]json.RawMessage
-		wantSavedImps       map[string]json.RawMessage
-		wantInvalidatedReqs []string
-		wantInvalidatedImps []string
+		description       string
+		giveFakeTime      time.Time
+		giveTimeoutMS     int
+		giveMockRows      *sqlmock.Rows
+		wantRecordedError pbsmetrics.StoredDataError
+		wantLastUpdate    time.Time
 	}{
+		{
+			description:       "fetch all timeout",
+			giveFakeTime:      time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			giveMockRows:      nil,
+			wantRecordedError: pbsmetrics.StoredDataErrorNetwork,
+			wantLastUpdate:    time.Time{},
+		},
 		{
 			description:       "fetch all query error",
 			giveFakeTime:      time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			giveTimeoutMS:     100,
 			giveMockRows:      nil,
-			wantReturnedError: true,
+			wantRecordedError: pbsmetrics.StoredDataErrorUndefined,
 			wantLastUpdate:    time.Time{},
 		},
 		{
-			description:  "fetch all row error",
-			giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			description:   "fetch all row error",
+			giveFakeTime:  time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			giveTimeoutMS: 100,
 			giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).
 				AddRow("stored-req-id", "true", "request").
 				RowError(0, errors.New("Some row error.")),
-			wantReturnedError: true,
+			wantRecordedError: pbsmetrics.StoredDataErrorUndefined,
 			wantLastUpdate:    time.Time{},
 		},
-		{
-			description:  "fetch all close error",
-			giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
-			giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).
-				AddRow("req-1", "true", "request").
-				AddRow("imp-1", "true", "imp").
-				AddRow("req-2", "", "request").
-				AddRow("imp-2", "", "imp").
-				CloseError(errors.New("Some close error.")),
-			wantReturnedError: false,
-			wantLastUpdate:    time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
-			wantSavedReqs:     map[string]json.RawMessage{"req-1": json.RawMessage(`true`)},
-			wantSavedImps:     map[string]json.RawMessage{"imp-1": json.RawMessage(`true`)},
-		},
 	}
 
 	for _, tt := range tests {
-		db, mock, _ := sqlmock.New()
+		db, dbMock, _ := sqlmock.New()
 		if tt.giveMockRows == nil {
-			mock.ExpectQuery(fakeQueryRegex()).WillReturnError(errors.New("Query failed."))
+			dbMock.ExpectQuery(fakeQueryRegex()).WillReturnError(errors.New("Query failed."))
 		} else {
-			mock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
+			dbMock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
 		}
 
+		metricsMock := &pbsmetrics.MetricsEngineMock{}
+		metricsMock.Mock.On("RecordStoredDataFetchTime", pbsmetrics.StoredDataLabels{
+			DataType:      pbsmetrics.RequestDataType,
+			DataFetchType: pbsmetrics.FetchAll,
+		}, mock.Anything).Return()
+		metricsMock.Mock.On("RecordStoredDataError", pbsmetrics.StoredDataLabels{
+			DataType: pbsmetrics.RequestDataType,
+			Error:    tt.wantRecordedError,
+		}).Return()
+
 		eventProducer := NewPostgresEventProducer(PostgresEventProducerConfig{
 			DB:               db,
-			CacheInitTimeout: 100 * time.Millisecond,
+			RequestType:      config.RequestDataType,
+			CacheInitTimeout: time.Duration(tt.giveTimeoutMS) * time.Millisecond,
 			CacheInitQuery:   fakeQuery,
+			MetricsEngine:    metricsMock,
 		})
 		eventProducer.time = &FakeTime{time: tt.giveFakeTime}
 		err := eventProducer.Run()
 
-		if tt.wantReturnedError {
-			assert.NotNil(t, err, tt.description)
-		} else {
-			assert.Nil(t, err, tt.description)
-		}
+		assert.NotNil(t, err, tt.description)
 		assert.Equal(t, tt.wantLastUpdate, eventProducer.lastUpdate, tt.description)
 
 		var saves events.Save
@@ -202,10 +215,12 @@ func TestFetchAllErrors(t *testing.T) {
 		case <-time.After(10 * time.Millisecond):
 		}
 
-		assert.Equal(t, tt.wantSavedReqs, saves.Requests, tt.description)
-		assert.Equal(t, tt.wantSavedImps, saves.Imps, tt.description)
-		assert.Equal(t, tt.wantInvalidatedReqs, invalidations.Requests, tt.description)
-		assert.Equal(t, tt.wantInvalidatedImps, invalidations.Imps, tt.description)
+		assert.Nil(t, saves.Requests, tt.description)
+		assert.Nil(t, saves.Imps, tt.description)
+		assert.Nil(t, invalidations.Requests, tt.description)
+		assert.Nil(t, invalidations.Requests, tt.description)
+
+		metricsMock.AssertExpectations(t)
 	}
 }
 
@@ -289,13 +304,21 @@ func TestFetchDeltaSuccess(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		db, mock, _ := sqlmock.New()
-		mock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
+		db, dbMock, _ := sqlmock.New()
+		dbMock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
+
+		metricsMock := &pbsmetrics.MetricsEngineMock{}
+		metricsMock.Mock.On("RecordStoredDataFetchTime", pbsmetrics.StoredDataLabels{
+			DataType:      pbsmetrics.RequestDataType,
+			DataFetchType: pbsmetrics.FetchDelta,
+		}, mock.Anything).Return()
 
 		eventProducer := NewPostgresEventProducer(PostgresEventProducerConfig{
 			DB:                 db,
+			RequestType:        config.RequestDataType,
 			CacheUpdateTimeout: 100 * time.Millisecond,
 			CacheUpdateQuery:   fakeQuery,
+			MetricsEngine:      metricsMock,
 		})
 		eventProducer.lastUpdate = time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC)
 		eventProducer.time = &FakeTime{time: tt.giveFakeTime}
@@ -321,81 +344,81 @@ func TestFetchDeltaSuccess(t *testing.T) {
 		assert.Equal(t, tt.wantSavedImps, saves.Imps, tt.description)
 		assert.Equal(t, tt.wantInvalidatedReqs, invalidations.Requests, tt.description)
 		assert.Equal(t, tt.wantInvalidatedImps, invalidations.Imps, tt.description)
+
+		metricsMock.AssertExpectations(t)
 	}
 }
 
 func TestFetchDeltaErrors(t *testing.T) {
 	tests := []struct {
-		description         string
-		giveFakeTime        time.Time
-		giveLastUpdate      time.Time
-		giveMockRows        *sqlmock.Rows
-		wantReturnedError   bool
-		wantLastUpdate      time.Time
-		wantSavedReqs       map[string]json.RawMessage
-		wantSavedImps       map[string]json.RawMessage
-		wantInvalidatedReqs []string
-		wantInvalidatedImps []string
+		description       string
+		giveFakeTime      time.Time
+		giveTimeoutMS     int
+		giveLastUpdate    time.Time
+		giveMockRows      *sqlmock.Rows
+		wantRecordedError pbsmetrics.StoredDataError
+		wantLastUpdate    time.Time
 	}{
+		{
+			description:       "fetch delta timeout",
+			giveFakeTime:      time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			giveLastUpdate:    time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
+			giveMockRows:      nil,
+			wantRecordedError: pbsmetrics.StoredDataErrorNetwork,
+			wantLastUpdate:    time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
+		},
 		{
 			description:       "fetch delta query error",
 			giveFakeTime:      time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			giveTimeoutMS:     100,
 			giveLastUpdate:    time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
 			giveMockRows:      nil,
-			wantReturnedError: true,
+			wantRecordedError: pbsmetrics.StoredDataErrorUndefined,
 			wantLastUpdate:    time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
 		},
 		{
-			description:    "fetch all row error",
+			description:    "fetch delta row error",
 			giveFakeTime:   time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			giveTimeoutMS:  100,
 			giveLastUpdate: time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
 			giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).
 				AddRow("stored-req-id", "true", "request").
 				RowError(0, errors.New("Some row error.")),
-			wantReturnedError: true,
+			wantRecordedError: pbsmetrics.StoredDataErrorUndefined,
 			wantLastUpdate:    time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
 		},
-		{
-			description:    "fetch all close error",
-			giveFakeTime:   time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
-			giveLastUpdate: time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
-			giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).
-				AddRow("req-1", "true", "request").
-				AddRow("imp-1", "true", "imp").
-				AddRow("req-2", "", "request").
-				AddRow("imp-2", "", "imp").
-				CloseError(errors.New("Some close error.")),
-			wantReturnedError:   false,
-			wantLastUpdate:      time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
-			wantSavedReqs:       map[string]json.RawMessage{"req-1": json.RawMessage(`true`)},
-			wantSavedImps:       map[string]json.RawMessage{"imp-1": json.RawMessage(`true`)},
-			wantInvalidatedReqs: []string{"req-2"},
-			wantInvalidatedImps: []string{"imp-2"},
-		},
 	}
 
 	for _, tt := range tests {
-		db, mock, _ := sqlmock.New()
+		db, dbMock, _ := sqlmock.New()
 		if tt.giveMockRows == nil {
-			mock.ExpectQuery(fakeQueryRegex()).WillReturnError(errors.New("Query failed."))
+			dbMock.ExpectQuery(fakeQueryRegex()).WillReturnError(errors.New("Query failed."))
 		} else {
-			mock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
+			dbMock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
 		}
 
+		metricsMock := &pbsmetrics.MetricsEngineMock{}
+		metricsMock.Mock.On("RecordStoredDataFetchTime", pbsmetrics.StoredDataLabels{
+			DataType:      pbsmetrics.RequestDataType,
+			DataFetchType: pbsmetrics.FetchDelta,
+		}, mock.Anything).Return()
+		metricsMock.Mock.On("RecordStoredDataError", pbsmetrics.StoredDataLabels{
+			DataType: pbsmetrics.RequestDataType,
+			Error:    tt.wantRecordedError,
+		}).Return()
+
 		eventProducer := NewPostgresEventProducer(PostgresEventProducerConfig{
 			DB:                 db,
-			CacheUpdateTimeout: 100 * time.Millisecond,
+			RequestType:        config.RequestDataType,
+			CacheUpdateTimeout: time.Duration(tt.giveTimeoutMS) * time.Millisecond,
 			CacheUpdateQuery:   fakeQuery,
+			MetricsEngine:      metricsMock,
 		})
 		eventProducer.lastUpdate = tt.giveLastUpdate
 		eventProducer.time = &FakeTime{time: tt.giveFakeTime}
 		err := eventProducer.Run()
 
-		if tt.wantReturnedError {
-			assert.NotNil(t, err, tt.description)
-		} else {
-			assert.Nil(t, err, tt.description)
-		}
+		assert.NotNil(t, err, tt.description)
 		assert.Equal(t, tt.wantLastUpdate, eventProducer.lastUpdate, tt.description)
 
 		var saves events.Save
@@ -411,9 +434,11 @@ func TestFetchDeltaErrors(t *testing.T) {
 		case <-time.After(10 * time.Millisecond):
 		}
 
-		assert.Equal(t, tt.wantSavedReqs, saves.Requests, tt.description)
-		assert.Equal(t, tt.wantSavedImps, saves.Imps, tt.description)
-		assert.Equal(t, tt.wantInvalidatedReqs, invalidations.Requests, tt.description)
-		assert.Equal(t, tt.wantInvalidatedImps, invalidations.Imps, tt.description)
+		assert.Nil(t, saves.Requests, tt.description)
+		assert.Nil(t, saves.Imps, tt.description)
+		assert.Nil(t, invalidations.Requests, tt.description)
+		assert.Nil(t, invalidations.Requests, tt.description)
+
+		metricsMock.AssertExpectations(t)
 	}
 }