Repository: prebid/prebid-server
Pull request #1125 (2019-11-27 15:33:54)

CCPA Phase 1: AMP Endpoint
Implements: prebid/prebid-server#1107

Parses the CCPA 'us_privacy' signal from the AMP request URL and inserts it into the OpenRTB request.


Patch:
diff --git a/config/config.go b/config/config.go
index 0c3d36df..c5e736d5 100644
--- a/config/config.go
+++ b/config/config.go
@@ -485,12 +485,12 @@ func (cfg *Configuration) setDerivedDefaults() {
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderBeachfront, "https://sync.bfmio.com/sync_s2s?gdpr={{.GDPR}}&url="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dbeachfront%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bio_cid%5D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConsumable, "https://e.serverbid.com/udb/9969/match?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dconsumable%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConversant, "https://prebid-match.dotomi.com/prebid/match?rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dconversant%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
-	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderDatablocks, "https://search.v5prebid.datablocks.net/s2ssync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Ddatablocks%26gdpr%3D%24%7Bgdpr%7D%26gdpr_consent%3D%24%7Bgdpr_consent%7D%26uid%3D%24%7Buid%7D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderDatablocks, "https://sync.v5prebid.datablocks.net/s2ssync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Ddatablocks%26gdpr%3D%24%7Bgdpr%7D%26gdpr_consent%3D%24%7Bgdpr_consent%7D%26uid%3D%24%7Buid%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderEmxDigital, "https://cs.emxdgt.com/um?ssp=pbs&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Demx_digital%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderEPlanning, "https://ads.us.e-planning.net/uspd/1/?du=https%3A%2F%2Fads.us.e-planning.net%2Fgetuid%2F1%2F5a1ad71d2d53a0f5%3F"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Deplanning%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	// openrtb_ext.BidderFacebook doesn't have a good default.
 	// openrtb_ext.BidderGamma doesn't have a good default.
-	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGrid, "https://grid.bidswitch.net/sp_sync?sp_id=prebid&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgrid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGrid, "https://x.bidswitch.net/check_uuid/"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgrid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BBSW_UUID%7D?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGumGum, "https://rtb.gumgum.com/usync/prbds2s?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgumgum%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderImprovedigital, "https://ad.360yield.com/server_match?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dimprovedigital%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7BPUB_USER_ID%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderIx, "https://ssum.casalemedia.com/usermatchredir?s=184932&cb="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dix%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
diff --git a/currencies/rates.go b/currencies/rates.go
index fd51570e..6a205213 100644
--- a/currencies/rates.go
+++ b/currencies/rates.go
@@ -62,9 +62,12 @@ func (r *Rates) GetRate(from string, to string) (float64, error) {
 	}
 	if r.Conversions != nil {
 		if conversion, present := r.Conversions[fromUnit.String()][toUnit.String()]; present {
+			// In case we have an entry FROM -> TO
 			return conversion, err
+		} else if conversion, present := r.Conversions[toUnit.String()][fromUnit.String()]; present {
+			// In case we have an entry TO -> FROM
+			return 1 / conversion, err
 		}
-
 		return 0, fmt.Errorf("Currency conversion rate not found: '%s' => '%s'", fromUnit.String(), toUnit.String())
 	}
 	return 0, errors.New("rates are nil")
diff --git a/endpoints/auction.go b/endpoints/auction.go
index d736dcf2..9a1d670d 100644
--- a/endpoints/auction.go
+++ b/endpoints/auction.go
@@ -78,33 +78,11 @@ func Auction(cfg *config.Configuration, syncers map[openrtb_ext.BidderName]users
 
 func (a *auction) auction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 	w.Header().Add("Content-Type", "application/json")
-	labels := pbsmetrics.Labels{
-		Source:        pbsmetrics.DemandUnknown,
-		RType:         pbsmetrics.ReqTypeLegacy,
-		PubID:         "",
-		Browser:       pbsmetrics.BrowserOther,
-		CookieFlag:    pbsmetrics.CookieFlagUnknown,
-		RequestStatus: pbsmetrics.RequestStatusOK,
-	}
-	if ua := user_agent.New(r.Header.Get("User-Agent")); ua != nil {
-		name, _ := ua.Browser()
-		if name == "Safari" {
-			labels.Browser = pbsmetrics.BrowserSafari
-		}
-	}
+	var labels = getDefaultLabels(r)
 	req, err := pbs.ParsePBSRequest(r, &a.cfg.AuctionTimeouts, a.dataCache, &(a.cfg.HostCookie))
-	// Defer here because we need req defined.
-	defer func() {
-		if req == nil {
-			a.metricsEngine.RecordRequest(labels)
-			a.metricsEngine.RecordLegacyImps(labels, 0)
-		} else {
-			// handles the case that ParsePBSRequest returns an error, so req.Start is not defined
-			a.metricsEngine.RecordRequest(labels)
-			a.metricsEngine.RecordLegacyImps(labels, len(req.AdUnits))
-			a.metricsEngine.RecordRequestTime(labels, time.Since(req.Start))
-		}
-	}()
+
+	defer a.recordMetrics(req, labels)
+
 	if err != nil {
 		if glog.V(2) {
 			glog.Infof("Failed to parse /auction request: %v", err)
@@ -114,17 +92,7 @@ func (a *auction) auction(w http.ResponseWriter, r *http.Request, _ httprouter.P
 		return
 	}
 	status := "OK"
-	if req.App != nil {
-		labels.Source = pbsmetrics.DemandApp
-	} else {
-		labels.Source = pbsmetrics.DemandWeb
-		if req.Cookie.LiveSyncCount() == 0 {
-			labels.CookieFlag = pbsmetrics.CookieFlagNo
-			status = "no_cookie"
-		} else {
-			labels.CookieFlag = pbsmetrics.CookieFlagYes
-		}
-	}
+	setLabelSource(&labels, req, &status)
 	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(req.TimeoutMillis))
 	defer cancel()
 	account, err := a.dataCache.Accounts().Get(req.AccountID)
@@ -150,46 +118,14 @@ func (a *auction) auction(w http.ResponseWriter, r *http.Request, _ httprouter.P
 			blabels := pbsmetrics.AdapterLabels{
 				Source:      labels.Source,
 				RType:       labels.RType,
-				Adapter:     openrtb_ext.BidderMap[bidder.BidderCode],
+				Adapter:     getAdapterValue(bidder),
 				PubID:       labels.PubID,
 				Browser:     labels.Browser,
 				CookieFlag:  labels.CookieFlag,
 				AdapterBids: pbsmetrics.AdapterBidPresent,
 			}
-			if blabels.Adapter == "" {
-				// "districtm" is legal, but not in BidderMap. Other values will log errors in the go_metrics code
-				blabels.Adapter = openrtb_ext.BidderName(bidder.BidderCode)
-			}
-			if req.App == nil {
-				// If exchanges[bidderCode] exists, then a.syncers[bidderCode] exists *except for districtm*.
-				// OpenRTB handles aliases differently, so this hack will keep legacy code working. For all other
-				// bidderCodes, a.syncers[bidderCode] will exist if exchanges[bidderCode] also does.
-				// This is guaranteed by the following unit tests, which compare these maps to the (source of truth) openrtb_ext.BidderMap:
-				//   1. TestSyncers inside usersync/usersync_test.go
-				//   2. TestExchangeMap inside pbs_light_test.go
-				syncerCode := bidder.BidderCode
-				if syncerCode == "districtm" {
-					syncerCode = "appnexus"
-				}
-				syncer := a.syncers[openrtb_ext.BidderName(syncerCode)]
-				uid, _, _ := req.Cookie.GetUID(syncer.FamilyName())
-				if uid == "" {
-					bidder.NoCookie = true
-					gdprApplies := req.ParseGDPR()
-					consent := req.ParseConsent()
-					if a.shouldUsersync(ctx, openrtb_ext.BidderName(syncerCode), gdprApplies, consent) {
-						syncInfo, err := syncer.GetUsersyncInfo(gdprApplies, consent)
-						if err == nil {
-							bidder.UsersyncInfo = syncInfo
-						} else {
-							glog.Errorf("Failed to get usersync info for %s: %v", syncerCode, err)
-						}
-					}
-					blabels.CookieFlag = pbsmetrics.CookieFlagNo
-					if ex.SkipNoCookies() {
-						continue
-					}
-				}
+			if skip := a.processUserSync(req, bidder, blabels, ex, &ctx); skip == true {
+				continue
 			}
 			sentBids++
 			bidderRunner := a.recoverSafely(func(bidder *pbs.PBSBidder, aLabels pbsmetrics.AdapterLabels) {
@@ -198,44 +134,7 @@ func (a *auction) auction(w http.ResponseWriter, r *http.Request, _ httprouter.P
 				bidList, err := ex.Call(ctx, req, bidder)
 				a.metricsEngine.RecordAdapterTime(aLabels, time.Since(start))
 				bidder.ResponseTime = int(time.Since(start) / time.Millisecond)
-				if err != nil {
-					var s struct{}
-					switch err {
-					case context.DeadlineExceeded:
-						aLabels.AdapterErrors = map[pbsmetrics.AdapterError]struct{}{pbsmetrics.AdapterErrorTimeout: s}
-						bidder.Error = "Timed out"
-					case context.Canceled:
-						fallthrough
-					default:
-						bidder.Error = err.Error()
-						switch err.(type) {
-						case *errortypes.BadInput:
-							aLabels.AdapterErrors = map[pbsmetrics.AdapterError]struct{}{pbsmetrics.AdapterErrorBadInput: s}
-						case *errortypes.BadServerResponse:
-							aLabels.AdapterErrors = map[pbsmetrics.AdapterError]struct{}{pbsmetrics.AdapterErrorBadServerResponse: s}
-						default:
-							glog.Warningf("Error from bidder %v. Ignoring all bids: %v", bidder.BidderCode, err)
-							aLabels.AdapterErrors = map[pbsmetrics.AdapterError]struct{}{pbsmetrics.AdapterErrorUnknown: s}
-						}
-					}
-				} else if bidList != nil {
-					bidList = checkForValidBidSize(bidList, bidder)
-					bidder.NumBids = len(bidList)
-					for _, bid := range bidList {
-						var cpm = float64(bid.Price * 1000)
-						a.metricsEngine.RecordAdapterPrice(aLabels, cpm)
-						switch bid.CreativeMediaType {
-						case "banner":
-							a.metricsEngine.RecordAdapterBidReceived(aLabels, openrtb_ext.BidTypeBanner, bid.Adm != "")
-						case "video":
-							a.metricsEngine.RecordAdapterBidReceived(aLabels, openrtb_ext.BidTypeVideo, bid.Adm != "")
-						}
-						bid.ResponseTime = bidder.ResponseTime
-					}
-				} else {
-					bidder.NoBid = true
-					aLabels.AdapterBids = pbsmetrics.AdapterBidNone
-				}
+				processBidResult(bidList, bidder, &aLabels, a.metricsEngine, err)
 
 				ch <- bidResult{
 					bidder:  bidder,
@@ -257,40 +156,10 @@ func (a *auction) auction(w http.ResponseWriter, r *http.Request, _ httprouter.P
 			resp.Bids = append(resp.Bids, bid)
 		}
 	}
-	if req.CacheMarkup == 1 {
-		cobjs := make([]*pbc.CacheObject, len(resp.Bids))
-		for i, bid := range resp.Bids {
-			if bid.CreativeMediaType == "video" {
-				cobjs[i] = &pbc.CacheObject{
-					Value:   bid.Adm,
-					IsVideo: true,
-				}
-			} else {
-				cobjs[i] = &pbc.CacheObject{
-					Value: &pbc.BidCache{
-						Adm:    bid.Adm,
-						NURL:   bid.NURL,
-						Width:  bid.Width,
-						Height: bid.Height,
-					},
-					IsVideo: false,
-				}
-			}
-		}
-		if err := pbc.Put(ctx, cobjs); err != nil {
-			writeAuctionError(w, "Prebid cache failed", err)
-			labels.RequestStatus = pbsmetrics.RequestStatusErr
-			return
-		}
-		for i, bid := range resp.Bids {
-			bid.CacheID = cobjs[i].UUID
-			bid.CacheURL = a.cfg.GetCachedAssetURL(bid.CacheID)
-			bid.NURL = ""
-			bid.Adm = ""
-		}
-	}
-	if req.CacheMarkup == 2 {
-		cacheVideoOnly(resp.Bids, ctx, w, a, &labels)
+	if err := cacheAccordingToMarkup(req, &resp, ctx, a, &labels); err != nil {
+		writeAuctionError(w, "Prebid cache failed", err)
+		labels.RequestStatus = pbsmetrics.RequestStatusErr
+		return
 	}
 	if req.SortBids == 1 {
 		sortBidsAddKeywordsMobile(resp.Bids, req, account.PriceGranularity)
@@ -338,7 +207,7 @@ func (a *auction) shouldUsersync(ctx context.Context, bidder openrtb_ext.BidderN
 }
 
 // cache video bids only for Web
-func cacheVideoOnly(bids pbs.PBSBidSlice, ctx context.Context, w http.ResponseWriter, deps *auction, labels *pbsmetrics.Labels) {
+func cacheVideoOnly(bids pbs.PBSBidSlice, ctx context.Context, deps *auction, labels *pbsmetrics.Labels) error {
 	var cobjs []*pbc.CacheObject
 	for _, bid := range bids {
 		if bid.CreativeMediaType == "video" {
@@ -350,9 +219,7 @@ func cacheVideoOnly(bids pbs.PBSBidSlice, ctx context.Context, w http.ResponseWr
 	}
 	err := pbc.Put(ctx, cobjs)
 	if err != nil {
-		writeAuctionError(w, "Prebid cache failed", err)
-		labels.RequestStatus = pbsmetrics.RequestStatusErr
-		return
+		return err
 	}
 	videoIndex := 0
 	for _, bid := range bids {
@@ -364,6 +231,7 @@ func cacheVideoOnly(bids pbs.PBSBidSlice, ctx context.Context, w http.ResponseWr
 			videoIndex++
 		}
 	}
+	return nil
 }
 
 // checkForValidBidSize goes through list of bids & find those which are banner mediaType and with height or width not defined
@@ -376,16 +244,9 @@ func checkForValidBidSize(bids pbs.PBSBidSlice, bidder *pbs.PBSBidder) pbs.PBSBi
 	finalBidCounter := 0
 bidLoop:
 	for _, bid := range bids {
-		if bid.CreativeMediaType == "banner" && (bid.Height == 0 || bid.Width == 0) {
+		if isUndimensionedBanner(bid) {
 			for _, adunit := range bidder.AdUnits {
-				if adunit.BidID == bid.BidID && adunit.Code == bid.AdUnitCode {
-					if len(adunit.Sizes) == 1 {
-						bid.Width, bid.Height = adunit.Sizes[0].W, adunit.Sizes[0].H
-						finalValidBids[finalBidCounter] = bid
-						finalBidCounter = finalBidCounter + 1
-					} else if len(adunit.Sizes) > 1 {
-						glog.Warningf("Bid was rejected for bidder %s because no size was defined", bid.BidderCode)
-					}
+				if copyBannerDimensions(&adunit, bid, finalValidBids, &finalBidCounter) {
 					continue bidLoop
 				}
 			}
@@ -397,6 +258,27 @@ bidLoop:
 	return finalValidBids[:finalBidCounter]
 }
 
+func isUndimensionedBanner(bid *pbs.PBSBid) bool {
+	return bid.CreativeMediaType == "banner" && (bid.Height == 0 || bid.Width == 0)
+}
+
+func copyBannerDimensions(adunit *pbs.PBSAdUnit, bid *pbs.PBSBid, finalValidBids []*pbs.PBSBid, finalBidCounter *int) bool {
+	var bidIDEqualsCode bool = false
+
+	if adunit.BidID == bid.BidID && adunit.Code == bid.AdUnitCode && adunit.Sizes != nil {
+		if len(adunit.Sizes) == 1 {
+			bid.Width, bid.Height = adunit.Sizes[0].W, adunit.Sizes[0].H
+			finalValidBids[*finalBidCounter] = bid
+			*finalBidCounter += 1
+		} else if len(adunit.Sizes) > 1 {
+			glog.Warningf("Bid was rejected for bidder %s because no size was defined", bid.BidderCode)
+		}
+		bidIDEqualsCode = true
+	}
+
+	return bidIDEqualsCode
+}
+
 // sortBidsAddKeywordsMobile sorts the bids and adds ad server targeting keywords to each bid.
 // The bids are sorted by cpm to find the highest bid.
 // The ad server targeting keywords are added to all bids, with specific keywords for the highest bid.
@@ -482,3 +364,164 @@ func sortBidsAddKeywordsMobile(bids pbs.PBSBidSlice, pbs_req *pbs.PBSRequest, pr
 		}
 	}
 }
+
+func getDefaultLabels(r *http.Request) pbsmetrics.Labels {
+	rlabels := pbsmetrics.Labels{
+		Source:        pbsmetrics.DemandUnknown,
+		RType:         pbsmetrics.ReqTypeLegacy,
+		PubID:         "",
+		Browser:       pbsmetrics.BrowserOther,
+		CookieFlag:    pbsmetrics.CookieFlagUnknown,
+		RequestStatus: pbsmetrics.RequestStatusOK,
+	}
+
+	if ua := user_agent.New(r.Header.Get("User-Agent")); ua != nil {
+		name, _ := ua.Browser()
+		if name == "Safari" {
+			rlabels.Browser = pbsmetrics.BrowserSafari
+		}
+	}
+	return rlabels
+}
+
+func setLabelSource(labels *pbsmetrics.Labels, req *pbs.PBSRequest, status *string) {
+	if req.App != nil {
+		labels.Source = pbsmetrics.DemandApp
+	} else {
+		labels.Source = pbsmetrics.DemandWeb
+		if req.Cookie.LiveSyncCount() == 0 {
+			labels.CookieFlag = pbsmetrics.CookieFlagNo
+			*status = "no_cookie"
+		} else {
+			labels.CookieFlag = pbsmetrics.CookieFlagYes
+		}
+	}
+}
+
+func getAdapterValue(bidder *pbs.PBSBidder) openrtb_ext.BidderName {
+	adapterLabelName, ok := openrtb_ext.BidderMap[bidder.BidderCode]
+	if ok && adapterLabelName != "" {
+		return adapterLabelName
+	} else {
+		return openrtb_ext.BidderName(bidder.BidderCode)
+	}
+}
+
+func cacheAccordingToMarkup(req *pbs.PBSRequest, resp *pbs.PBSResponse, ctx context.Context, a *auction, labels *pbsmetrics.Labels) error {
+	if req.CacheMarkup == 1 {
+		cobjs := make([]*pbc.CacheObject, len(resp.Bids))
+		for i, bid := range resp.Bids {
+			if bid.CreativeMediaType == "video" {
+				cobjs[i] = &pbc.CacheObject{
+					Value:   bid.Adm,
+					IsVideo: true,
+				}
+			} else {
+				cobjs[i] = &pbc.CacheObject{
+					Value: &pbc.BidCache{
+						Adm:    bid.Adm,
+						NURL:   bid.NURL,
+						Width:  bid.Width,
+						Height: bid.Height,
+					},
+					IsVideo: false,
+				}
+			}
+		}
+		if err := pbc.Put(ctx, cobjs); err != nil {
+			return err
+		}
+		for i, bid := range resp.Bids {
+			bid.CacheID = cobjs[i].UUID
+			bid.CacheURL = a.cfg.GetCachedAssetURL(bid.CacheID)
+			bid.NURL = ""
+			bid.Adm = ""
+		}
+	} else if req.CacheMarkup == 2 {
+		return cacheVideoOnly(resp.Bids, ctx, a, labels)
+	}
+	return nil
+}
+
+func processBidResult(bidList pbs.PBSBidSlice, bidder *pbs.PBSBidder, aLabels *pbsmetrics.AdapterLabels, metrics pbsmetrics.MetricsEngine, err error) {
+	if err != nil {
+		var s struct{}
+		if err == context.DeadlineExceeded {
+			aLabels.AdapterErrors = map[pbsmetrics.AdapterError]struct{}{pbsmetrics.AdapterErrorTimeout: s}
+			bidder.Error = "Timed out"
+		} else if err != context.Canceled {
+			bidder.Error = err.Error()
+			switch err.(type) {
+			case *errortypes.BadInput:
+				aLabels.AdapterErrors = map[pbsmetrics.AdapterError]struct{}{pbsmetrics.AdapterErrorBadInput: s}
+			case *errortypes.BadServerResponse:
+				aLabels.AdapterErrors = map[pbsmetrics.AdapterError]struct{}{pbsmetrics.AdapterErrorBadServerResponse: s}
+			default:
+				glog.Warningf("Error from bidder %v. Ignoring all bids: %v", bidder.BidderCode, err)
+				aLabels.AdapterErrors = map[pbsmetrics.AdapterError]struct{}{pbsmetrics.AdapterErrorUnknown: s}
+			}
+		}
+	} else if bidList != nil {
+		bidList = checkForValidBidSize(bidList, bidder)
+		bidder.NumBids = len(bidList)
+		for _, bid := range bidList {
+			var cpm = float64(bid.Price * 1000)
+			metrics.RecordAdapterPrice(*aLabels, cpm)
+			switch bid.CreativeMediaType {
+			case "banner":
+				metrics.RecordAdapterBidReceived(*aLabels, openrtb_ext.BidTypeBanner, bid.Adm != "")
+			case "video":
+				metrics.RecordAdapterBidReceived(*aLabels, openrtb_ext.BidTypeVideo, bid.Adm != "")
+			}
+			bid.ResponseTime = bidder.ResponseTime
+		}
+	} else {
+		bidder.NoBid = true
+		aLabels.AdapterBids = pbsmetrics.AdapterBidNone
+	}
+}
+
+func (a *auction) recordMetrics(req *pbs.PBSRequest, labels pbsmetrics.Labels) {
+	a.metricsEngine.RecordRequest(labels)
+	if req == nil {
+		a.metricsEngine.RecordLegacyImps(labels, 0)
+		return
+	}
+	a.metricsEngine.RecordLegacyImps(labels, len(req.AdUnits))
+	a.metricsEngine.RecordRequestTime(labels, time.Since(req.Start))
+}
+
+func (a *auction) processUserSync(req *pbs.PBSRequest, bidder *pbs.PBSBidder, blabels pbsmetrics.AdapterLabels, ex adapters.Adapter, ctx *context.Context) bool {
+	var skip bool = false
+	if req.App != nil {
+		return skip
+	}
+	// If exchanges[bidderCode] exists, then a.syncers[bidderCode] exists *except for districtm*.
+	// OpenRTB handles aliases differently, so this hack will keep legacy code working. For all other
+	// bidderCodes, a.syncers[bidderCode] will exist if exchanges[bidderCode] also does.
+	// This is guaranteed by the TestSyncers unit test inside usersync/usersync_test.go, which compares these maps to the (source of truth) openrtb_ext.BidderMap:
+	syncerCode := bidder.BidderCode
+	if syncerCode == "districtm" {
+		syncerCode = "appnexus"
+	}
+	syncer := a.syncers[openrtb_ext.BidderName(syncerCode)]
+	uid, _, _ := req.Cookie.GetUID(syncer.FamilyName())
+	if uid == "" {
+		bidder.NoCookie = true
+		gdprApplies := req.ParseGDPR()
+		consent := req.ParseConsent()
+		if a.shouldUsersync(*ctx, openrtb_ext.BidderName(syncerCode), gdprApplies, consent) {
+			syncInfo, err := syncer.GetUsersyncInfo(gdprApplies, consent)
+			if err == nil {
+				bidder.UsersyncInfo = syncInfo
+			} else {
+				glog.Errorf("Failed to get usersync info for %s: %v", syncerCode, err)
+			}
+		}
+		blabels.CookieFlag = pbsmetrics.CookieFlagNo
+		if ex.SkipNoCookies() {
+			skip = true
+		}
+	}
+	return skip
+}
diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index 2c8a12bf..48437008 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -52,7 +52,7 @@ func (deps *cookieSyncDeps) Endpoint(w http.ResponseWriter, r *http.Request, _ h
 
 	defer deps.pbsAnalytics.LogCookieSyncObject(&co)
 
-	deps.metrics.RecordCookieSync(pbsmetrics.Labels{})
+	deps.metrics.RecordCookieSync()
 	userSyncCookie := usersync.ParsePBSCookieFromRequest(r, deps.hostCookie)
 	if !userSyncCookie.AllowSyncs() {
 		http.Error(w, "User has opted out", http.StatusUnauthorized)
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 8fa21eca..7328d3c1 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -22,6 +22,9 @@ import (
 	"github.com/prebid/prebid-server/exchange"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbsmetrics"
+	"github.com/prebid/prebid-server/privacy"
+	"github.com/prebid/prebid-server/privacy/ccpa"
+	"github.com/prebid/prebid-server/privacy/gdpr"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
 	"github.com/prebid/prebid-server/usersync"
@@ -379,22 +382,19 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope
 		req.Imp[0].TagID = slot
 	}
 
-	//In the AMP endpoint the consent string found in the http.Request query overrides that of the prebid query
-	queryConsentString := httpRequest.FormValue("gdpr_consent")
-	if queryConsentString != "" {
-		jsonMsg := json.RawMessage(`{"consent":"` + queryConsentString + `"}`)
-		// If nil, initialize
-		if req.User == nil {
-			req.User = &openrtb.User{Ext: jsonMsg}
-		} else if req.User.Ext == nil {
-			req.User.Ext = jsonMsg
-		} else { // req.User.Ext != nil, keep whatever is in there and only substitute the consent string
-			var parserErr error
-			req.User.Ext, parserErr = jsonparser.Set(req.User.Ext, []byte(`"`+queryConsentString+`"`), "consent")
-			if parserErr != nil {
-				return parserErr
-			}
-		}
+	gdprConsent := getQueryParam(httpRequest, "gdpr_consent")
+	ccpaValue := getQueryParam(httpRequest, "us_privacy")
+	privacyPolicies := privacy.Policies{
+		GDPR: gdpr.Policy{
+			Consent: gdprConsent,
+		},
+		CCPA: ccpa.Policy{
+			Value: ccpaValue,
+		},
+	}
+
+	if err := privacyPolicies.Write(req); err != nil {
+		return err
 	}
 
 	if timeout, err := strconv.ParseInt(httpRequest.FormValue("timeout"), 10, 64); err == nil {
@@ -404,6 +404,17 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope
 	return nil
 }
 
+func getQueryParam(httpRequest *http.Request, name string) string {
+	values, ok := httpRequest.URL.Query()[name]
+
+	if !ok || len(values) == 0 {
+		return ""
+	}
+
+	// return first value of the query param, matching the behavior of httpRequest.FormValue
+	return values[0]
+}
+
 func makeFormatReplacement(overrideWidth uint64, overrideHeight uint64, width uint64, height uint64, multisize string) []openrtb.Format {
 	if overrideWidth != 0 && overrideHeight != 0 {
 		return []openrtb.Format{{
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index fd180359..61ea929e 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -444,6 +444,10 @@ func fillAndValidateNative(n *openrtb.Native, impIndex int) error {
 }
 
 func validateNativeContextTypes(cType native.ContextType, cSubtype native.ContextSubType, impIndex int) error {
+	if cType == 0 {
+		// Context is only recommended, so none is a valid type.
+		return nil
+	}
 	if cType < native.ContextTypeContent || cType > native.ContextTypeProduct {
 		return fmt.Errorf("request.imp[%d].native.request.context is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39", impIndex)
 	}
@@ -480,6 +484,10 @@ func validateNativeContextTypes(cType native.ContextType, cSubtype native.Contex
 }
 
 func validateNativePlacementType(pt native.PlacementType, impIndex int) error {
+	if pt == 0 {
+		// Placement Type is only reccomended, not required.
+		return nil
+	}
 	if pt < native.PlacementTypeFeed || pt > native.PlacementTypeRecommendationWidget {
 		return fmt.Errorf("request.imp[%d].native.request.plcmttype is invalid. See https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40", impIndex)
 	}
@@ -491,23 +499,37 @@ func fillAndValidateNativeAssets(assets []nativeRequests.Asset, impIndex int) er
 		return fmt.Errorf("request.imp[%d].native.request.assets must be an array containing at least one object", impIndex)
 	}
 
+	assetIDs := make(map[int64]struct{}, len(assets))
+
+	// If none of the asset IDs are defined by the caller, then prebid server should assign its own unique IDs. But
+	// if the caller did assign its own asset IDs, then prebid server will respect those IDs
+	assignAssetIDs := true
+	for i := 0; i < len(assets); i++ {
+		assignAssetIDs = assignAssetIDs && (assets[i].ID == 0)
+	}
+
 	for i := 0; i < len(assets); i++ {
-		// Per the OpenRTB spec docs, this is a "unique asset ID, assigned by exchange. Typically a counter for the array"
-		// To avoid conflict with the Request, we'll return a 400 if the Request _did_ define this ID,
-		// and then populate it as the spec suggests.
 		if err := validateNativeAsset(assets[i], impIndex, i); err != nil {
 			return err
 		}
-		assets[i].ID = int64(i)
+
+		if assignAssetIDs {
+			assets[i].ID = int64(i)
+			continue
+		}
+
+		// Each asset should have a unique ID thats assigned by the caller
+		if _, ok := assetIDs[assets[i].ID]; ok {
+			return fmt.Errorf("request.imp[%d].native.request.assets[%d].id is already being used by another asset. Each asset ID must be unique.", impIndex, i)
+		}
+
+		assetIDs[assets[i].ID] = struct{}{}
 	}
+
 	return nil
 }
 
 func validateNativeAsset(asset nativeRequests.Asset, impIndex int, assetIndex int) error {
-	if asset.ID != 0 {
-		return fmt.Errorf(`request.imp[%d].native.request.assets[%d].id must not be defined. Prebid Server will set this automatically, using the index of the asset in the array as the ID`, impIndex, assetIndex)
-	}
-
 	assetErr := "request.imp[%d].native.request.assets[%d] must define exactly one of {title, img, video, data}"
 	foundType := false
 
@@ -523,9 +545,7 @@ func validateNativeAsset(asset nativeRequests.Asset, impIndex int, assetIndex in
 			return fmt.Errorf(assetErr, impIndex, assetIndex)
 		}
 		foundType = true
-		if err := validateNativeAssetImg(asset.Img, impIndex, assetIndex); err != nil {
-			return err
-		}
+		// It is technically valid to have neither w/h nor wmin/hmin, so no check
 	}
 
 	if asset.Video != nil {
@@ -587,19 +607,6 @@ func validateNativeEventTracker(tracker nativeRequests.EventTracker, impIndex in
 	return nil
 }
 
-func validateNativeAssetImg(image *nativeRequests.Image, impIndex int, assetIndex int) error {
-	// Note that w, wmin, h, and hmin cannot be negative because these variables use unsigned ints.
-	// Those fail during the standard json.Unmarshal() call.
-	if image.W == 0 && image.WMin == 0 {
-		return fmt.Errorf(`request.imp[%d].native.request.assets[%d].img must contain at least one of "w" or "wmin"`, impIndex, assetIndex)
-	}
-	if image.H == 0 && image.HMin == 0 {
-		return fmt.Errorf(`request.imp[%d].native.request.assets[%d].img must contain at least one of "h" or "hmin"`, impIndex, assetIndex)
-	}
-
-	return nil
-}
-
 func validateNativeAssetVideo(video *nativeRequests.Video, impIndex int, assetIndex int) error {
 	if len(video.MIMEs) < 1 {
 		return fmt.Errorf("request.imp[%d].native.request.assets[%d].video.mimes must be an array with at least one MIME type", impIndex, assetIndex)
diff --git a/endpoints/openrtb2/sample-requests/invalid-native/assets-with-dup-ids.json b/endpoints/openrtb2/sample-requests/invalid-native/assets-with-dup-ids.json
new file mode 100644
index 00000000..dfece8cf
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/invalid-native/assets-with-dup-ids.json
@@ -0,0 +1,18 @@
+{
+    "context": 1,
+    "plcmttype": 1,
+    "assets": [
+        {
+            "id": 1,
+            "img": {
+                "wmin": 30
+            }
+        },
+        {
+            "id": 1,
+            "title": {
+                "len": 20
+            }
+        }
+    ]
+}
diff --git a/endpoints/openrtb2/sample-requests/invalid-native/assets-with-partial-ids.json b/endpoints/openrtb2/sample-requests/invalid-native/assets-with-partial-ids.json
new file mode 100644
index 00000000..291ae8d7
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/invalid-native/assets-with-partial-ids.json
@@ -0,0 +1,22 @@
+{
+    "context": 1,
+    "plcmttype": 1,
+    "assets": [
+        {
+            "id": 1,
+            "img": {
+                "wmin": 30
+            }
+        },
+        {
+            "title": {
+                "len": 20
+            }
+        },
+        {
+            "img": {
+                "wmin": 50
+            }
+        }
+    ]
+}
diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-img-no-hmin.json b/endpoints/openrtb2/sample-requests/valid-native/asset-img-no-hmin.json
similarity index 100%
rename from endpoints/openrtb2/sample-requests/invalid-native/asset-img-no-hmin.json
rename to endpoints/openrtb2/sample-requests/valid-native/asset-img-no-hmin.json
diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-img-no-wmin.json b/endpoints/openrtb2/sample-requests/valid-native/asset-img-no-wmin.json
similarity index 100%
rename from endpoints/openrtb2/sample-requests/invalid-native/asset-img-no-wmin.json
rename to endpoints/openrtb2/sample-requests/valid-native/asset-img-no-wmin.json
diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-with-id.json b/endpoints/openrtb2/sample-requests/valid-native/asset-with-id.json
similarity index 60%
rename from endpoints/openrtb2/sample-requests/invalid-native/asset-with-id.json
rename to endpoints/openrtb2/sample-requests/valid-native/asset-with-id.json
index 6f2eee53..7c55888b 100644
--- a/endpoints/openrtb2/sample-requests/invalid-native/asset-with-id.json
+++ b/endpoints/openrtb2/sample-requests/valid-native/asset-with-id.json
@@ -3,11 +3,10 @@
     "plcmttype": 1,
     "assets": [
         {
-            "id": 2,
+            "id": 1,
             "img": {
-                "hmin": 30,
-                "wmin": 20
+                "wmin": 30
             }
         }
     ]
-}
\ No newline at end of file
+}
diff --git a/endpoints/openrtb2/sample-requests/valid-native/asset-with-no-id.json b/endpoints/openrtb2/sample-requests/valid-native/asset-with-no-id.json
new file mode 100644
index 00000000..a9bc57ea
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-native/asset-with-no-id.json
@@ -0,0 +1,11 @@
+{
+    "context": 1,
+    "plcmttype": 1,
+    "assets": [
+        {
+            "img": {
+                "wmin": 30
+            }
+        }
+    ]
+}
diff --git a/endpoints/openrtb2/sample-requests/valid-native/assets-with-unique-ids.json b/endpoints/openrtb2/sample-requests/valid-native/assets-with-unique-ids.json
new file mode 100644
index 00000000..10692b9a
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-native/assets-with-unique-ids.json
@@ -0,0 +1,18 @@
+{
+    "context": 1,
+    "plcmttype": 1,
+    "assets": [
+        {
+            "id": 1,
+            "img": {
+                "wmin": 30
+            }
+        },
+        {
+            "id": 2,
+            "title": {
+                "len": 20
+            }
+        }
+    ]
+}
diff --git a/endpoints/openrtb2/sample-requests/invalid-native/request-no-context.json b/endpoints/openrtb2/sample-requests/valid-native/request-no-context.json
similarity index 100%
rename from endpoints/openrtb2/sample-requests/invalid-native/request-no-context.json
rename to endpoints/openrtb2/sample-requests/valid-native/request-no-context.json
diff --git a/endpoints/openrtb2/sample-requests/invalid-native/request-no-plcmttype.json b/endpoints/openrtb2/sample-requests/valid-native/request-plcmttype-empty.json
similarity index 100%
rename from endpoints/openrtb2/sample-requests/invalid-native/request-no-plcmttype.json
rename to endpoints/openrtb2/sample-requests/valid-native/request-plcmttype-empty.json
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index 61f4f3e7..1f234b85 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -91,7 +91,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	}
 	requestJson, err := ioutil.ReadAll(lr)
 	if err != nil {
-		handleError(labels, w, []error{err}, ao)
+		handleError(&labels, w, []error{err}, &ao)
 		return
 	}
 
@@ -102,27 +102,27 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 
 	if err != nil {
 		if deps.cfg.VideoStoredRequestRequired {
-			handleError(labels, w, []error{err}, ao)
+			handleError(&labels, w, []error{err}, &ao)
 			return
 		}
 	} else {
 		storedRequest, errs := deps.loadStoredVideoRequest(context.Background(), storedRequestId)
 		if len(errs) > 0 {
-			handleError(labels, w, errs, ao)
+			handleError(&labels, w, errs, &ao)
 			return
 		}
 
 		//merge incoming req with stored video req
 		resolvedRequest, err = jsonpatch.MergePatch(storedRequest, requestJson)
 		if err != nil {
-			handleError(labels, w, []error{err}, ao)
+			handleError(&labels, w, []error{err}, &ao)
 			return
 		}
 	}
 	//unmarshal and validate combined result
 	videoBidReq, errL, podErrors := deps.parseVideoRequest(resolvedRequest)
 	if len(errL) > 0 {
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 
@@ -130,7 +130,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	if deps.defaultRequest {
 		if err := json.Unmarshal(deps.defReqJSON, bidReq); err != nil {
 			err = fmt.Errorf("Invalid JSON in Default Request Settings: %s", err)
-			handleError(labels, w, []error{err}, ao)
+			handleError(&labels, w, []error{err}, &ao)
 			return
 		}
 	}
@@ -154,7 +154,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 		}
 		err := errors.New(fmt.Sprintf("all pods are incorrect: %s", strings.Join(resPodErr, "; ")))
 		errL = append(errL, err)
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 
@@ -166,7 +166,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 
 	errL = deps.validateRequest(bidReq)
 	if len(errL) > 0 {
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 
@@ -194,7 +194,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 
 	if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil {
 		errL = append(errL, acctIdErr)
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 	//execute auction logic
@@ -203,7 +203,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	ao.Response = response
 	if err != nil {
 		errL := []error{err}
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 
@@ -211,7 +211,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	bidResp, err := buildVideoResponse(response, podErrors)
 	if err != nil {
 		errL := []error{err}
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 	if bidReq.Test == 1 {
@@ -222,7 +222,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	//resp, err := json.Marshal(response)
 	if err != nil {
 		errL := []error{err}
-		handleError(labels, w, errL, ao)
+		handleError(&labels, w, errL, &ao)
 		return
 	}
 
@@ -238,7 +238,7 @@ func cleanupVideoBidRequest(videoReq *openrtb_ext.BidRequestVideo, podErrors []P
 	return videoReq
 }
 
-func handleError(labels pbsmetrics.Labels, w http.ResponseWriter, errL []error, ao analytics.AuctionObject) {
+func handleError(labels *pbsmetrics.Labels, w http.ResponseWriter, errL []error, ao *analytics.AuctionObject) {
 	labels.RequestStatus = pbsmetrics.RequestStatusErr
 	var errors string
 	var status int = http.StatusInternalServerError
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 76a4d302..e92e813f 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -615,11 +615,11 @@ func (e *exchange) getBidCacheInfo(bid *pbsOrtbBid, auc *auction) (openrtb_ext.E
 		if cacheUUID, found = auc.cacheIds[bid.bid]; found {
 			cacheInfo.CacheId = cacheUUID
 			extCacheHost, extCachePath = e.cache.GetExtCacheData()
-			cacheInfo.Url = extCacheHost + "/" + extCachePath + "?uuid=" + cacheUUID
+			cacheInfo.Url = extCacheHost + extCachePath + "?uuid=" + cacheUUID
 		} else if cacheUUID, found = auc.vastCacheIds[bid.bid]; found {
 			cacheInfo.CacheId = cacheUUID
 			extCacheHost, extCachePath = e.cache.GetExtCacheData()
-			cacheInfo.Url = extCacheHost + "/" + extCachePath + "?uuid=" + cacheUUID
+			cacheInfo.Url = extCacheHost + extCachePath + "?uuid=" + cacheUUID
 		}
 	}
 	return cacheInfo, found
diff --git a/go.sum b/go.sum
index 6d9e21f0..57fcbb76 100644
--- a/go.sum
+++ b/go.sum
@@ -74,8 +74,10 @@ github.com/prebid/go-gdpr v0.6.0 h1:/GKrygGkUbsgd96HIkjAu7/6CHtRedvcojRtfAd4Igc=
 github.com/prebid/go-gdpr v0.6.0/go.mod h1:FPY0uxSrl9/Mz237LnPo3ge4aCG0wQ9FWf2b4WhwNn0=
 github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed h1:0dloFFFNNDG7c+8qtkYw2FdADrWy9s5cI8wHp6tK3Mg=
 github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
 github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e h1:n/3MEhJQjQxrOUCzh1Y3Re6aJUUWRp2M9+Oc3eVn/54=
 github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 h1:agujYaXJSxSo18YNX3jzl+4G6Bstwt+kqv47GS12uL0=
diff --git a/openrtb_ext/regs.go b/openrtb_ext/regs.go
index 4f33846f..56a5179d 100644
--- a/openrtb_ext/regs.go
+++ b/openrtb_ext/regs.go
@@ -6,4 +6,7 @@ type ExtRegs struct {
 	// GDPR should be "1" if the caller believes the user is subject to GDPR laws, "0" if not, and undefined
 	// if it's unknown. For more info on this parameter, see: https://iabtechlab.com/wp-content/uploads/2018/02/OpenRTB_Advisory_GDPR_2018-02.pdf
 	GDPR *int8 `json:"gdpr,omitempty"`
+
+	// USPrivacy should be a four character string, see: https://iabtechlab.com/wp-content/uploads/2019/11/OpenRTB-Extension-U.S.-Privacy-IAB-Tech-Lab.pdf
+	USPrivacy string `json:"us_privacy,omitempty"`
 }
diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go
index da8e8128..81cfbfd0 100644
--- a/pbsmetrics/config/metrics.go
+++ b/pbsmetrics/config/metrics.go
@@ -140,9 +140,9 @@ func (me *MultiMetricsEngine) RecordAdapterTime(labels pbsmetrics.AdapterLabels,
 }
 
 // RecordCookieSync across all engines
-func (me *MultiMetricsEngine) RecordCookieSync(labels pbsmetrics.Labels) {
+func (me *MultiMetricsEngine) RecordCookieSync() {
 	for _, thisME := range *me {
-		thisME.RecordCookieSync(labels)
+		thisME.RecordCookieSync()
 	}
 }
 
@@ -175,9 +175,9 @@ func (me *MultiMetricsEngine) RecordUserIDSet(userLabels pbsmetrics.UserLabels)
 }
 
 // RecordPrebidCacheRequestTime across all engines
-func (me *MultiMetricsEngine) RecordPrebidCacheRequestTime(labels pbsmetrics.RequestLabels, length time.Duration) {
+func (me *MultiMetricsEngine) RecordPrebidCacheRequestTime(success bool, length time.Duration) {
 	for _, thisME := range *me {
-		thisME.RecordPrebidCacheRequestTime(labels, length)
+		thisME.RecordPrebidCacheRequestTime(success, length)
 	}
 }
 
@@ -186,85 +186,68 @@ type DummyMetricsEngine struct{}
 
 // RecordRequest as a noop
 func (me *DummyMetricsEngine) RecordRequest(labels pbsmetrics.Labels) {
-	return
 }
 
 // RecordConnectionAccept as a noop
 func (me *DummyMetricsEngine) RecordConnectionAccept(success bool) {
-	return
 }
 
 // RecordConnectionClose as a noop
 func (me *DummyMetricsEngine) RecordConnectionClose(success bool) {
-	return
 }
 
 // RecordImps as a noop
 func (me *DummyMetricsEngine) RecordImps(implabels pbsmetrics.ImpLabels) {
-	return
 }
 
 // RecordLegacyImps as a noop
 func (me *DummyMetricsEngine) RecordLegacyImps(labels pbsmetrics.Labels, numImps int) {
-	return
 }
 
 // RecordRequestTime as a noop
 func (me *DummyMetricsEngine) RecordRequestTime(labels pbsmetrics.Labels, length time.Duration) {
-	return
 }
 
 // RecordAdapterPanic as a noop
 func (me *DummyMetricsEngine) RecordAdapterPanic(labels pbsmetrics.AdapterLabels) {
-	return
 }
 
 // RecordAdapterRequest as a noop
 func (me *DummyMetricsEngine) RecordAdapterRequest(labels pbsmetrics.AdapterLabels) {
-	return
 }
 
 // RecordAdapterBidReceived as a noop
 func (me *DummyMetricsEngine) RecordAdapterBidReceived(labels pbsmetrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
-	return
 }
 
 // RecordAdapterPrice as a noop
 func (me *DummyMetricsEngine) RecordAdapterPrice(labels pbsmetrics.AdapterLabels, cpm float64) {
-	return
 }
 
 // RecordAdapterTime as a noop
 func (me *DummyMetricsEngine) RecordAdapterTime(labels pbsmetrics.AdapterLabels, length time.Duration) {
-	return
 }
 
 // RecordCookieSync as a noop
-func (me *DummyMetricsEngine) RecordCookieSync(labels pbsmetrics.Labels) {
-	return
+func (me *DummyMetricsEngine) RecordCookieSync() {
 }
 
 // RecordAdapterCookieSync as a noop
 func (me *DummyMetricsEngine) RecordAdapterCookieSync(adapter openrtb_ext.BidderName, gdprBlocked bool) {
-	return
 }
 
 // RecordUserIDSet as a noop
 func (me *DummyMetricsEngine) RecordUserIDSet(userLabels pbsmetrics.UserLabels) {
-	return
 }
 
 // RecordStoredReqCacheResult as a noop
 func (me *DummyMetricsEngine) RecordStoredReqCacheResult(cacheResult pbsmetrics.CacheResult, inc int) {
-	return
 }
 
 // RecordStoredImpCacheResult as a noop
 func (me *DummyMetricsEngine) RecordStoredImpCacheResult(cacheResult pbsmetrics.CacheResult, inc int) {
-	return
 }
 
 // RecordPrebidCacheRequestTime as a noop
-func (me *DummyMetricsEngine) RecordPrebidCacheRequestTime(labels pbsmetrics.RequestLabels, length time.Duration) {
-	return
+func (me *DummyMetricsEngine) RecordPrebidCacheRequestTime(success bool, length time.Duration) {
 }
diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go
index 699c11e1..9b3dd65f 100644
--- a/pbsmetrics/go_metrics.go
+++ b/pbsmetrics/go_metrics.go
@@ -478,7 +478,7 @@ func (me *Metrics) RecordAdapterTime(labels AdapterLabels, length time.Duration)
 }
 
 // RecordCookieSync implements a part of the MetricsEngine interface. Records a cookie sync request
-func (me *Metrics) RecordCookieSync(labels Labels) {
+func (me *Metrics) RecordCookieSync() {
 	me.CookieSyncMeter.Mark(1)
 }
 
@@ -518,13 +518,12 @@ func (me *Metrics) RecordStoredImpCacheResult(cacheResult CacheResult, inc int)
 
 // RecordPrebidCacheRequestTime implements a part of the MetricsEngine interface. Records the
 // amount of time taken to store the auction result in Prebid Cache.
-func (me *Metrics) RecordPrebidCacheRequestTime(labels RequestLabels, length time.Duration) {
-	if labels.RequestStatus == RequestStatusOK {
+func (me *Metrics) RecordPrebidCacheRequestTime(success bool, length time.Duration) {
+	if success {
 		me.PrebidCacheRequestTimerSuccess.Update(length)
-		return
+	} else {
+		me.PrebidCacheRequestTimerError.Update(length)
 	}
-
-	me.PrebidCacheRequestTimerError.Update(length)
 }
 
 func doMark(bidder openrtb_ext.BidderName, meters map[openrtb_ext.BidderName]metrics.Meter) {
diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go
index 4808365e..aea9735c 100644
--- a/pbsmetrics/metrics.go
+++ b/pbsmetrics/metrics.go
@@ -236,6 +236,16 @@ const (
 	RequestActionErr    RequestAction = "err"
 )
 
+// RequestActions returns possible setuid action labels
+func RequestActions() []RequestAction {
+	return []RequestAction{
+		RequestActionSet,
+		RequestActionOptOut,
+		RequestActionGDPR,
+		RequestActionErr,
+	}
+}
+
 // MetricsEngine is a generic interface to record PBS metrics into the desired backend
 // The first three metrics function fire off once per incoming request, so total metrics
 // will equal the total numer of incoming requests. The remaining 5 fire off per outgoing
@@ -256,10 +266,10 @@ type MetricsEngine interface {
 	RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool)
 	RecordAdapterPrice(labels AdapterLabels, cpm float64)
 	RecordAdapterTime(labels AdapterLabels, length time.Duration)
-	RecordCookieSync(labels Labels) // May ignore all labels
+	RecordCookieSync()
 	RecordAdapterCookieSync(adapter openrtb_ext.BidderName, gdprBlocked bool)
 	RecordUserIDSet(userLabels UserLabels) // Function should verify bidder values
 	RecordStoredReqCacheResult(cacheResult CacheResult, inc int)
 	RecordStoredImpCacheResult(cacheResult CacheResult, inc int)
-	RecordPrebidCacheRequestTime(labels RequestLabels, length time.Duration)
+	RecordPrebidCacheRequestTime(success bool, length time.Duration)
 }
diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go
index c5e9a196..6d57f9fc 100644
--- a/pbsmetrics/metrics_mock.go
+++ b/pbsmetrics/metrics_mock.go
@@ -68,8 +68,8 @@ func (me *MetricsEngineMock) RecordAdapterTime(labels AdapterLabels, length time
 }
 
 // RecordCookieSync mock
-func (me *MetricsEngineMock) RecordCookieSync(labels Labels) {
-	me.Called(labels)
+func (me *MetricsEngineMock) RecordCookieSync() {
+	me.Called()
 }
 
 // RecordAdapterCookieSync mock
@@ -93,6 +93,6 @@ func (me *MetricsEngineMock) RecordStoredImpCacheResult(cacheResult CacheResult,
 }
 
 // RecordPrebidCacheRequestTime mock
-func (me *MetricsEngineMock) RecordPrebidCacheRequestTime(labels RequestLabels, length time.Duration) {
-	me.Called(labels, length)
+func (me *MetricsEngineMock) RecordPrebidCacheRequestTime(success bool, length time.Duration) {
+	me.Called(success, length)
 }
diff --git a/pbsmetrics/prometheus/preload.go b/pbsmetrics/prometheus/preload.go
new file mode 100644
index 00000000..7654dd54
--- /dev/null
+++ b/pbsmetrics/prometheus/preload.go
@@ -0,0 +1,147 @@
+package prometheusmetrics
+
+import (
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+func preloadLabelValues(m *Metrics) {
+	var (
+		actionValues          = actionsAsString()
+		adapterValues         = adaptersAsString()
+		adapterErrorValues    = adapterErrorsAsString()
+		bidTypeValues         = []string{markupDeliveryAdm, markupDeliveryNurl}
+		boolValues            = boolValuesAsString()
+		cacheResultValues     = cacheResultsAsString()
+		cookieValues          = cookieTypesAsString()
+		connectionErrorValues = []string{connectionAcceptError, connectionCloseError}
+		requestStatusValues   = requestStatusesAsString()
+		requestTypeValues     = requestTypesAsString()
+	)
+
+	preloadLabelValuesForCounter(m.connectionsError, map[string][]string{
+		connectionErrorLabel: connectionErrorValues,
+	})
+
+	preloadLabelValuesForCounter(m.impressions, map[string][]string{
+		isBannerLabel: boolValues,
+		isVideoLabel:  boolValues,
+		isAudioLabel:  boolValues,
+		isNativeLabel: boolValues,
+	})
+
+	preloadLabelValuesForHistogram(m.prebidCacheWriteTimer, map[string][]string{
+		successLabel: boolValues,
+	})
+
+	preloadLabelValuesForCounter(m.requests, map[string][]string{
+		requestTypeLabel:   requestTypeValues,
+		requestStatusLabel: requestStatusValues,
+	})
+
+	preloadLabelValuesForHistogram(m.requestsTimer, map[string][]string{
+		requestTypeLabel: requestTypeValues,
+	})
+
+	preloadLabelValuesForCounter(m.requestsWithoutCookie, map[string][]string{
+		requestTypeLabel: requestTypeValues,
+	})
+
+	preloadLabelValuesForCounter(m.storedImpressionsCacheResult, map[string][]string{
+		cacheResultLabel: cacheResultValues,
+	})
+
+	preloadLabelValuesForCounter(m.storedRequestCacheResult, map[string][]string{
+		cacheResultLabel: cacheResultValues,
+	})
+
+	preloadLabelValuesForCounter(m.adapterBids, map[string][]string{
+		adapterLabel:        adapterValues,
+		markupDeliveryLabel: bidTypeValues,
+	})
+
+	preloadLabelValuesForCounter(m.adapterCookieSync, map[string][]string{
+		adapterLabel:        adapterValues,
+		privacyBlockedLabel: boolValues,
+	})
+
+	preloadLabelValuesForCounter(m.adapterErrors, map[string][]string{
+		adapterLabel:      adapterValues,
+		adapterErrorLabel: adapterErrorValues,
+	})
+
+	preloadLabelValuesForCounter(m.adapterPanics, map[string][]string{
+		adapterLabel: adapterValues,
+	})
+
+	preloadLabelValuesForHistogram(m.adapterPrices, map[string][]string{
+		adapterLabel: adapterValues,
+	})
+
+	preloadLabelValuesForCounter(m.adapterRequests, map[string][]string{
+		adapterLabel: adapterValues,
+		cookieLabel:  cookieValues,
+		hasBidsLabel: boolValues,
+	})
+
+	preloadLabelValuesForHistogram(m.adapterRequestsTimer, map[string][]string{
+		adapterLabel: adapterValues,
+	})
+
+	preloadLabelValuesForCounter(m.adapterUserSync, map[string][]string{
+		adapterLabel: adapterValues,
+		actionLabel:  actionValues,
+	})
+}
+
+func preloadLabelValuesForCounter(counter *prometheus.CounterVec, labelsWithValues map[string][]string) {
+	registerLabelPermutations(labelsWithValues, func(labels prometheus.Labels) {
+		counter.With(labels)
+	})
+}
+
+func preloadLabelValuesForHistogram(histogram *prometheus.HistogramVec, labelsWithValues map[string][]string) {
+	registerLabelPermutations(labelsWithValues, func(labels prometheus.Labels) {
+		histogram.With(labels)
+	})
+}
+
+func registerLabelPermutations(labelsWithValues map[string][]string, register func(prometheus.Labels)) {
+	if len(labelsWithValues) == 0 {
+		return
+	}
+
+	keys := make([]string, 0, len(labelsWithValues))
+	values := make([][]string, 0, len(labelsWithValues))
+	for k, v := range labelsWithValues {
+		keys = append(keys, k)
+		values = append(values, v)
+	}
+
+	labels := prometheus.Labels{}
+	registerLabelPermutationsRecursive(0, keys, values, labels, register)
+}
+
+func registerLabelPermutationsRecursive(depth int, keys []string, values [][]string, labels prometheus.Labels, register func(prometheus.Labels)) {
+	label := keys[depth]
+	isLeaf := depth == len(keys)-1
+
+	if isLeaf {
+		for _, v := range values[depth] {
+			labels[label] = v
+			register(cloneLabels(labels))
+		}
+	} else {
+		for _, v := range values[depth] {
+			labels[label] = v
+			registerLabelPermutationsRecursive(depth+1, keys, values, labels, register)
+		}
+	}
+}
+
+func cloneLabels(labels prometheus.Labels) prometheus.Labels {
+	clone := prometheus.Labels{}
+	for k, v := range labels {
+		clone[k] = v
+	}
+	return clone
+}
diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go
index b46259de..7cb80643 100644
--- a/pbsmetrics/prometheus/prometheus.go
+++ b/pbsmetrics/prometheus/prometheus.go
@@ -1,198 +1,222 @@
 package prometheusmetrics
 
 import (
+	"strconv"
 	"time"
 
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/prometheus/client_golang/prometheus"
-	_ "github.com/prometheus/client_golang/prometheus/promhttp"
 )
 
-// Defines the actual Prometheus metrics we will be using. Satisfies interface MetricsEngine
+// Metrics defines the Prometheus metrics backing the MetricsEngine implementation.
 type Metrics struct {
-	Registry             *prometheus.Registry
-	connCounter          prometheus.Gauge
-	connError            *prometheus.CounterVec
-	imps                 *prometheus.CounterVec
-	legacyImps           *prometheus.CounterVec
-	requests             *prometheus.CounterVec
-	reqTimer             *prometheus.HistogramVec
-	adaptRequests        *prometheus.CounterVec
-	adaptTimer           *prometheus.HistogramVec
-	adaptBids            *prometheus.CounterVec
-	adaptPrices          *prometheus.HistogramVec
-	adaptErrors          *prometheus.CounterVec
-	adaptPanics          *prometheus.CounterVec
-	cookieSync           prometheus.Counter
-	adaptCookieSync      *prometheus.CounterVec
-	userID               *prometheus.CounterVec
-	prebidCacheReqTimer  *prometheus.HistogramVec
-	storedReqCacheResult *prometheus.CounterVec
-	storedImpCacheResult *prometheus.CounterVec
+	Registry *prometheus.Registry
+
+	// General Metrics
+	connectionsClosed            prometheus.Counter
+	connectionsError             *prometheus.CounterVec
+	connectionsOpened            prometheus.Counter
+	cookieSync                   prometheus.Counter
+	impressions                  *prometheus.CounterVec
+	impressionsLegacy            prometheus.Counter
+	prebidCacheWriteTimer        *prometheus.HistogramVec
+	requests                     *prometheus.CounterVec
+	requestsTimer                *prometheus.HistogramVec
+	requestsWithoutCookie        *prometheus.CounterVec
+	storedImpressionsCacheResult *prometheus.CounterVec
+	storedRequestCacheResult     *prometheus.CounterVec
+
+	// Adapter Metrics
+	adapterBids          *prometheus.CounterVec
+	adapterCookieSync    *prometheus.CounterVec
+	adapterErrors        *prometheus.CounterVec
+	adapterPanics        *prometheus.CounterVec
+	adapterPrices        *prometheus.HistogramVec
+	adapterRequests      *prometheus.CounterVec
+	adapterRequestsTimer *prometheus.HistogramVec
+	adapterUserSync      *prometheus.CounterVec
+
+	// Account Metrics
+	accountRequests *prometheus.CounterVec
 }
 
 const (
-	requestTypeLabel    = "request_type"
-	demandSourceLabel   = "demand_source"
-	browserLabel        = "browser"
-	cookieLabel         = "cookie"
-	responseStatusLabel = "response_status"
-	adapterLabel        = "adapter"
-	adapterBidLabel     = "adapter_bid"
-	markupTypeLabel     = "markup_type"
-	bidTypeLabel        = "bid_type"
-	adapterErrLabel     = "adapter_error"
-	cacheResultLabel    = "cache_result"
-	gdprBlockedLabel    = "gdpr_blocked"
-	bannerLabel         = "banner"
-	videoLabel          = "video"
-	audioLabel          = "audio"
-	nativeLabel         = "native"
-	accountLabel        = "account"
+	accountLabel         = "account"
+	actionLabel          = "action"
+	adapterErrorLabel    = "adapter_error"
+	adapterLabel         = "adapter"
+	bidTypeLabel         = "bid_type"
+	cacheResultLabel     = "cache_result"
+	connectionErrorLabel = "connection_error"
+	cookieLabel          = "cookie"
+	hasBidsLabel         = "has_bids"
+	isAudioLabel         = "audio"
+	isBannerLabel        = "banner"
+	isNativeLabel        = "native"
+	isVideoLabel         = "video"
+	markupDeliveryLabel  = "delivery"
+	privacyBlockedLabel  = "privacy_blocked"
+	requestStatusLabel   = "request_status"
+	requestTypeLabel     = "request_type"
+	successLabel         = "success"
 )
 
-// NewMetrics constructs the appropriate options for the Prometheus metrics. Needs to be fed the promethus config
-// Its own function to keep the metric creation function cleaner.
-func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
-	timerBuckets := prometheus.LinearBuckets(0.05, 0.05, 20)
-	timerBuckets = append(timerBuckets, []float64{1.5, 2.0, 3.0, 5.0, 10.0, 50.0}...)
-
-	timerBucketsQuickTasks := prometheus.LinearBuckets(0.005, 0.005, 20)
-	timerBucketsQuickTasks = append([]float64{0.001, 0.0015, 0.003}, timerBucketsQuickTasks...)
-
-	standardLabelNames := []string{demandSourceLabel, requestTypeLabel, browserLabel, cookieLabel, responseStatusLabel, accountLabel}
+const (
+	connectionAcceptError = "accept"
+	connectionCloseError  = "close"
+)
 
-	adapterLabelNames := []string{demandSourceLabel, requestTypeLabel, browserLabel, cookieLabel, adapterBidLabel, adapterLabel}
-	bidLabelNames := []string{demandSourceLabel, requestTypeLabel, browserLabel, cookieLabel, adapterBidLabel, adapterLabel, bidTypeLabel, markupTypeLabel}
-	errorLabelNames := []string{demandSourceLabel, requestTypeLabel, browserLabel, cookieLabel, adapterErrLabel, adapterLabel}
+const (
+	markupDeliveryAdm  = "adm"
+	markupDeliveryNurl = "nurl"
+)
 
-	impLabelNames := []string{bannerLabel, videoLabel, audioLabel, nativeLabel}
+// NewMetrics initializes a new Prometheus metrics instance with preloaded label values.
+func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
+	requestTimeBuckets := []float64{0.05, 0.1, 0.15, 0.20, 0.25, 0.3, 0.4, 0.5, 0.75, 1}
+	cacheWriteTimeBuckts := []float64{0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1}
+	priceBuckets := []float64{250, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000}
 
 	metrics := Metrics{}
 	metrics.Registry = prometheus.NewRegistry()
-	metrics.connCounter = newConnCounter(cfg)
-	metrics.Registry.MustRegister(metrics.connCounter)
-	metrics.connError = newCounter(cfg, "active_connections_total",
-		"Errors reported on the connections coming in.",
-		[]string{"ErrorType"},
-	)
-	metrics.Registry.MustRegister(metrics.connError)
-	metrics.imps = newCounter(cfg, "imps_requested",
-		"Count of Impressions by type and in total requested through PBS.",
-		impLabelNames,
-	)
-	metrics.Registry.MustRegister(metrics.imps)
-	metrics.legacyImps = newCounter(cfg, "legacy_imps_requested",
-		"Total number of impressions requested through legacy PBS.",
-		standardLabelNames,
-	)
-	metrics.Registry.MustRegister(metrics.legacyImps)
-	metrics.requests = newCounter(cfg, "requests_total",
-		"Total number of requests made to PBS.",
-		standardLabelNames,
-	)
-	metrics.Registry.MustRegister(metrics.requests)
-	metrics.reqTimer = newHistogram(cfg, "request_time_seconds",
-		"Seconds to resolve each PBS request.",
-		standardLabelNames, timerBuckets,
-	)
-	metrics.Registry.MustRegister(metrics.reqTimer)
-	metrics.adaptRequests = newCounter(cfg, "adapter_requests_total",
-		"Number of requests sent out to each bidder.",
-		adapterLabelNames,
-	)
-	metrics.Registry.MustRegister(metrics.adaptRequests)
-	metrics.adaptPanics = newCounter(cfg, "adapter_panics_total",
-		"Number of panics generated by each bidder.",
-		adapterLabelNames,
-	)
-	metrics.Registry.MustRegister(metrics.adaptPanics)
-	metrics.adaptTimer = newHistogram(cfg, "adapter_time_seconds",
-		"Seconds to resolve each request to a bidder.",
-		adapterLabelNames, timerBuckets,
-	)
-	metrics.Registry.MustRegister(metrics.adaptTimer)
-	metrics.adaptBids = newCounter(cfg, "adapter_bids_received_total",
-		"Number of bids received from each bidder.",
-		bidLabelNames,
-	)
-	metrics.Registry.MustRegister(metrics.adaptBids)
-	metrics.storedReqCacheResult = newCounter(cfg, "stored_request_cache_performance",
-		"Number of stored request cache hits vs miss",
-		[]string{"cache_result"},
-	)
-	metrics.Registry.MustRegister(metrics.storedReqCacheResult)
-	metrics.storedImpCacheResult = newCounter(cfg, "stored_imp_cache_performance",
-		"Number of stored imp cache hits vs miss",
-		[]string{"cache_result"},
-	)
-	metrics.Registry.MustRegister(metrics.storedImpCacheResult)
-	metrics.adaptPrices = newHistogram(cfg, "adapter_prices",
-		"Values of the bids from each bidder.",
-		adapterLabelNames, prometheus.LinearBuckets(0.1, 0.1, 200),
-	)
-	metrics.Registry.MustRegister(metrics.adaptPrices)
-	metrics.adaptErrors = newCounter(cfg, "adapter_errors_total",
-		"Number of unique error types seen in each request to an adapter.",
-		errorLabelNames,
-	)
-	metrics.Registry.MustRegister(metrics.adaptErrors)
-	metrics.cookieSync = newCookieSync(cfg)
-	metrics.Registry.MustRegister(metrics.cookieSync)
-	metrics.adaptCookieSync = newCounter(cfg, "cookie_sync_returns",
-		"Number of syncs generated for a bidder, and if they were subsequently blocked.",
-		[]string{adapterLabel, gdprBlockedLabel},
-	)
-	metrics.Registry.MustRegister(metrics.adaptCookieSync)
-	metrics.userID = newCounter(cfg, "setuid_calls",
-		"Number of user ID syncs performed",
-		[]string{"action", "bidder"},
-	)
-	metrics.Registry.MustRegister(metrics.userID)
-	metrics.prebidCacheReqTimer = newHistogram(cfg, "prebid_cache_request_time_seconds",
-		"Seconds to complete each PBC request.",
-		[]string{responseStatusLabel}, timerBucketsQuickTasks,
-	)
-	metrics.Registry.MustRegister(metrics.prebidCacheReqTimer)
-
-	initializeTimeSeries(&metrics)
 
-	return &metrics
-}
+	metrics.connectionsClosed = newCounterWithoutLabels(cfg, metrics.Registry,
+		"connections_closed",
+		"Count of successful connections closed to Prebid Server.")
+
+	metrics.connectionsError = newCounter(cfg, metrics.Registry,
+		"connections_error",
+		"Count of errors for connection open and close attempts to Prebid Server labeled by type.",
+		[]string{connectionErrorLabel})
+
+	metrics.connectionsOpened = newCounterWithoutLabels(cfg, metrics.Registry,
+		"connections_opened",
+		"Count of successful connections opened to Prebid Server.")
+
+	metrics.cookieSync = newCounterWithoutLabels(cfg, metrics.Registry,
+		"cookie_sync_requests",
+		"Count of cookie sync requests to Prebid Server.")
+
+	metrics.impressions = newCounter(cfg, metrics.Registry,
+		"impressions_requests",
+		"Count of requested impressions to Prebid Server labeled by type.",
+		[]string{isBannerLabel, isVideoLabel, isAudioLabel, isNativeLabel})
+
+	metrics.impressionsLegacy = newCounterWithoutLabels(cfg, metrics.Registry,
+		"impressions_requests_legacy",
+		"Count of requested impressions to Prebid Server using the legacy endpoint.")
+
+	metrics.prebidCacheWriteTimer = newHistogram(cfg, metrics.Registry,
+		"prebidcache_write_time_seconds",
+		"Seconds to write to Prebid Cache labeled by success or failure. Failure timing is limited by Prebid Server enforced timeouts.",
+		[]string{successLabel},
+		cacheWriteTimeBuckts)
+
+	metrics.requests = newCounter(cfg, metrics.Registry,
+		"requests",
+		"Count of total requests to Prebid Server labeled by type and status.",
+		[]string{requestTypeLabel, requestStatusLabel})
+
+	metrics.requestsTimer = newHistogram(cfg, metrics.Registry,
+		"request_time_seconds",
+		"Seconds to resolve successful Prebid Server requests labeled by type.",
+		[]string{requestTypeLabel},
+		requestTimeBuckets)
+
+	metrics.requestsWithoutCookie = newCounter(cfg, metrics.Registry,
+		"requests_without_cookie",
+		"Count of total requests to Prebid Server without a cookie labeled by type.",
+		[]string{requestTypeLabel})
+
+	metrics.storedImpressionsCacheResult = newCounter(cfg, metrics.Registry,
+		"stored_impressions_cache_performance",
+		"Count of stored impression cache requests attempts by hits or miss.",
+		[]string{cacheResultLabel})
+
+	metrics.storedRequestCacheResult = newCounter(cfg, metrics.Registry,
+		"stored_request_cache_performance",
+		"Count of stored request cache requests attempts by hits or miss.",
+		[]string{cacheResultLabel})
+
+	metrics.adapterBids = newCounter(cfg, metrics.Registry,
+		"adapter_bids",
+		"Count of bids labeled by adapter and markup delivery type (adm or nurl).",
+		[]string{adapterLabel, markupDeliveryLabel})
+
+	metrics.adapterCookieSync = newCounter(cfg, metrics.Registry,
+		"adapter_cookie_sync",
+		"Count of cookie sync requests received labeled by adapter and if the sync was blocked due to privacy regulation (GDPR, CCPA, etc...).",
+		[]string{adapterLabel, privacyBlockedLabel})
+
+	metrics.adapterErrors = newCounter(cfg, metrics.Registry,
+		"adapter_errors",
+		"Count of errors labeled by adapter and error type.",
+		[]string{adapterLabel, adapterErrorLabel})
+
+	metrics.adapterPanics = newCounter(cfg, metrics.Registry,
+		"adapter_panics",
+		"Count of panics labeled by adapter.",
+		[]string{adapterLabel})
+
+	metrics.adapterPrices = newHistogram(cfg, metrics.Registry,
+		"adapter_prices",
+		"Monetary value of the bids labeled by adapter.",
+		[]string{adapterLabel},
+		priceBuckets)
+
+	metrics.adapterRequests = newCounter(cfg, metrics.Registry,
+		"adapter_requests",
+		"Count of requests labeled by adapter, if has a cookie, and if it resulted in bids.",
+		[]string{adapterLabel, cookieLabel, hasBidsLabel})
+
+	metrics.adapterRequestsTimer = newHistogram(cfg, metrics.Registry,
+		"adapter_request_time_seconds",
+		"Seconds to resolve each successful request labeled by adapter.",
+		[]string{adapterLabel},
+		requestTimeBuckets)
+
+	metrics.adapterUserSync = newCounter(cfg, metrics.Registry,
+		"adapter_user_sync",
+		"Count of user ID sync requests received labeled by adapter and action.",
+		[]string{adapterLabel, actionLabel})
+
+	metrics.accountRequests = newCounter(cfg, metrics.Registry,
+		"account_requests",
+		"Count of total requests to Prebid Server labeled by account.",
+		[]string{accountLabel})
+
+	preloadLabelValues(&metrics)
 
-func newConnCounter(cfg config.PrometheusMetrics) prometheus.Gauge {
-	opts := prometheus.GaugeOpts{
-		Namespace: cfg.Namespace,
-		Subsystem: cfg.Subsystem,
-		Name:      "active_connections",
-		Help:      "Current number of active (open) connections.",
-	}
-	return prometheus.NewGauge(opts)
+	return &metrics
 }
 
-func newCookieSync(cfg config.PrometheusMetrics) prometheus.Counter {
+func newCounter(cfg config.PrometheusMetrics, registry *prometheus.Registry, name, help string, labels []string) *prometheus.CounterVec {
 	opts := prometheus.CounterOpts{
 		Namespace: cfg.Namespace,
 		Subsystem: cfg.Subsystem,
-		Name:      "cookie_sync_requests_total",
-		Help:      "Number of cookie sync requests received.",
+		Name:      name,
+		Help:      help,
 	}
-	return prometheus.NewCounter(opts)
+	counter := prometheus.NewCounterVec(opts, labels)
+	registry.MustRegister(counter)
+	return counter
 }
 
-func newCounter(cfg config.PrometheusMetrics, name string, help string, labels []string) *prometheus.CounterVec {
+func newCounterWithoutLabels(cfg config.PrometheusMetrics, registry *prometheus.Registry, name, help string) prometheus.Counter {
 	opts := prometheus.CounterOpts{
 		Namespace: cfg.Namespace,
 		Subsystem: cfg.Subsystem,
 		Name:      name,
 		Help:      help,
 	}
-	return prometheus.NewCounterVec(opts, labels)
+	counter := prometheus.NewCounter(opts)
+	registry.MustRegister(counter)
+	return counter
 }
 
-func newHistogram(cfg config.PrometheusMetrics, name string, help string, labels []string, buckets []float64) *prometheus.HistogramVec {
+func newHistogram(cfg config.PrometheusMetrics, registry *prometheus.Registry, name, help string, labels []string, buckets []float64) *prometheus.HistogramVec {
 	opts := prometheus.HistogramOpts{
 		Namespace: cfg.Namespace,
 		Subsystem: cfg.Subsystem,
@@ -200,386 +224,153 @@ func newHistogram(cfg config.PrometheusMetrics, name string, help string, labels
 		Help:      help,
 		Buckets:   buckets,
 	}
-	return prometheus.NewHistogramVec(opts, labels)
+	histogram := prometheus.NewHistogramVec(opts, labels)
+	registry.MustRegister(histogram)
+	return histogram
 }
 
-func (me *Metrics) RecordConnectionAccept(success bool) {
+func (m *Metrics) RecordConnectionAccept(success bool) {
 	if success {
-		me.connCounter.Inc()
+		m.connectionsOpened.Inc()
 	} else {
-		me.connError.WithLabelValues("accept_error").Inc()
+		m.connectionsError.With(prometheus.Labels{
+			connectionErrorLabel: connectionAcceptError,
+		}).Inc()
 	}
-
 }
 
-func (me *Metrics) RecordConnectionClose(success bool) {
+func (m *Metrics) RecordConnectionClose(success bool) {
 	if success {
-		me.connCounter.Dec()
+		m.connectionsClosed.Inc()
 	} else {
-		me.connError.WithLabelValues("close_error").Inc()
-	}
-}
-
-func (me *Metrics) RecordRequest(labels pbsmetrics.Labels) {
-	me.requests.With(resolveLabels(labels)).Inc()
-}
-
-func (me *Metrics) RecordImps(implabels pbsmetrics.ImpLabels) {
-	me.imps.With(resolveImpLabels(implabels)).Inc()
-}
-
-func (me *Metrics) RecordLegacyImps(labels pbsmetrics.Labels, numImps int) {
-	var lbls prometheus.Labels
-	lbls = resolveLabels(labels)
-	me.legacyImps.With(lbls).Add(float64(numImps))
-}
-
-func (me *Metrics) RecordRequestTime(labels pbsmetrics.Labels, length time.Duration) {
-	time := float64(length) / float64(time.Second)
-	me.reqTimer.With(resolveLabels(labels)).Observe(time)
-}
-
-func (me *Metrics) RecordAdapterPanic(labels pbsmetrics.AdapterLabels) {
-	me.adaptPanics.With(resolveAdapterLabels(labels)).Inc()
-}
-
-func (me *Metrics) RecordAdapterRequest(labels pbsmetrics.AdapterLabels) {
-	me.adaptRequests.With(resolveAdapterLabels(labels)).Inc()
-	for k := range labels.AdapterErrors {
-		me.adaptErrors.With(resolveAdapterErrorLabels(labels, string(k))).Inc()
-	}
-}
-
-func (me *Metrics) RecordAdapterBidReceived(labels pbsmetrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
-	me.adaptBids.With(resolveBidLabels(labels, bidType, hasAdm)).Inc()
-}
-
-func (me *Metrics) RecordAdapterPrice(labels pbsmetrics.AdapterLabels, cpm float64) {
-	me.adaptPrices.With(resolveAdapterLabels(labels)).Observe(cpm)
-}
-
-func (me *Metrics) RecordAdapterTime(labels pbsmetrics.AdapterLabels, length time.Duration) {
-	time := float64(length) / float64(time.Second)
-	me.adaptTimer.With(resolveAdapterLabels(labels)).Observe(time)
-}
-
-func (me *Metrics) RecordCookieSync(labels pbsmetrics.Labels) {
-	me.cookieSync.Inc()
-}
-
-func (me *Metrics) RecordAdapterCookieSync(adapter openrtb_ext.BidderName, gdprBlocked bool) {
-	labels := prometheus.Labels{
-		adapterLabel: string(adapter),
-	}
-	if gdprBlocked {
-		labels[gdprBlockedLabel] = "true"
-	} else {
-		labels[gdprBlockedLabel] = "false"
-	}
-	me.adaptCookieSync.With(labels).Inc()
-}
-
-// RecordStoredReqCacheResult records cache hits and misses when looking up stored requests
-func (me *Metrics) RecordStoredReqCacheResult(cacheResult pbsmetrics.CacheResult, inc int) {
-	labels := prometheus.Labels{
-		cacheResultLabel: string(cacheResult),
-	}
-
-	me.storedReqCacheResult.With(labels).Add(float64(inc))
-}
-
-// RecordStoredImpCacheResult records cache hits and misses when looking up stored imps
-func (me *Metrics) RecordStoredImpCacheResult(cacheResult pbsmetrics.CacheResult, inc int) {
-	labels := prometheus.Labels{
-		cacheResultLabel: string(cacheResult),
+		m.connectionsError.With(prometheus.Labels{
+			connectionErrorLabel: connectionCloseError,
+		}).Inc()
 	}
-
-	me.storedImpCacheResult.With(labels).Add(float64(inc))
 }
 
-func (me *Metrics) RecordUserIDSet(userLabels pbsmetrics.UserLabels) {
-	me.userID.With(resolveUserSyncLabels(userLabels)).Inc()
-}
+func (m *Metrics) RecordRequest(labels pbsmetrics.Labels) {
+	m.requests.With(prometheus.Labels{
+		requestTypeLabel:   string(labels.RType),
+		requestStatusLabel: string(labels.RequestStatus),
+	}).Inc()
 
-// RecordPrebidCacheRequestTime records amount of time taken to store the auction result in Prebid Cache
-func (me *Metrics) RecordPrebidCacheRequestTime(labels pbsmetrics.RequestLabels, length time.Duration) {
-	time := float64(length) / float64(time.Second)
-	me.prebidCacheReqTimer.With(resolveRequestLabels(labels)).Observe(time)
-}
-
-func resolveLabels(labels pbsmetrics.Labels) prometheus.Labels {
-	return prometheus.Labels{
-		demandSourceLabel:   string(labels.Source),
-		requestTypeLabel:    string(labels.RType),
-		accountLabel:        string(labels.PubID),
-		browserLabel:        string(labels.Browser),
-		cookieLabel:         string(labels.CookieFlag),
-		responseStatusLabel: string(labels.RequestStatus),
+	if labels.CookieFlag == pbsmetrics.CookieFlagNo {
+		m.requestsWithoutCookie.With(prometheus.Labels{
+			requestTypeLabel: string(labels.RType),
+		}).Inc()
 	}
-}
 
-func resolveAdapterLabels(labels pbsmetrics.AdapterLabels) prometheus.Labels {
-	return prometheus.Labels{
-		demandSourceLabel: string(labels.Source),
-		requestTypeLabel:  string(labels.RType),
-		// "pubid":   labels.PubID,
-		browserLabel:    string(labels.Browser),
-		cookieLabel:     string(labels.CookieFlag),
-		adapterBidLabel: string(labels.AdapterBids),
-		adapterLabel:    string(labels.Adapter),
+	if labels.PubID != pbsmetrics.PublisherUnknown {
+		m.accountRequests.With(prometheus.Labels{
+			accountLabel: labels.PubID,
+		}).Inc()
 	}
 }
 
-func resolveBidLabels(labels pbsmetrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) prometheus.Labels {
-	bidLabels := prometheus.Labels{
-		demandSourceLabel: string(labels.Source),
-		requestTypeLabel:  string(labels.RType),
-		// "pubid":   labels.PubID,
-		browserLabel:    string(labels.Browser),
-		cookieLabel:     string(labels.CookieFlag),
-		adapterBidLabel: string(labels.AdapterBids),
-		adapterLabel:    string(labels.Adapter),
-		bidTypeLabel:    string(bidType),
-		markupTypeLabel: "unknown",
-	}
-	if hasAdm {
-		bidLabels[markupTypeLabel] = "adm"
-	}
-	return bidLabels
+func (m *Metrics) RecordImps(labels pbsmetrics.ImpLabels) {
+	m.impressions.With(prometheus.Labels{
+		isBannerLabel: strconv.FormatBool(labels.BannerImps),
+		isVideoLabel:  strconv.FormatBool(labels.VideoImps),
+		isAudioLabel:  strconv.FormatBool(labels.AudioImps),
+		isNativeLabel: strconv.FormatBool(labels.NativeImps),
+	}).Inc()
 }
 
-func resolveAdapterErrorLabels(labels pbsmetrics.AdapterLabels, errorType string) prometheus.Labels {
-	return prometheus.Labels{
-		demandSourceLabel: string(labels.Source),
-		requestTypeLabel:  string(labels.RType),
-		// "pubid":   labels.PubID,
-		browserLabel:    string(labels.Browser),
-		cookieLabel:     string(labels.CookieFlag),
-		adapterErrLabel: errorType,
-		adapterLabel:    string(labels.Adapter),
-	}
+func (m *Metrics) RecordLegacyImps(labels pbsmetrics.Labels, numImps int) {
+	m.impressionsLegacy.Add(float64(numImps))
 }
 
-func resolveUserSyncLabels(userLabels pbsmetrics.UserLabels) prometheus.Labels {
-	return prometheus.Labels{
-		"action": string(userLabels.Action),
-		"bidder": string(userLabels.Bidder),
+func (m *Metrics) RecordRequestTime(labels pbsmetrics.Labels, length time.Duration) {
+	if labels.RequestStatus == pbsmetrics.RequestStatusOK {
+		m.requestsTimer.With(prometheus.Labels{
+			requestTypeLabel: string(labels.RType),
+		}).Observe(length.Seconds())
 	}
 }
 
-func resolveImpLabels(labels pbsmetrics.ImpLabels) prometheus.Labels {
-	var impLabels prometheus.Labels = prometheus.Labels{
-		bannerLabel: "no",
-		videoLabel:  "no",
-		audioLabel:  "no",
-		nativeLabel: "no",
-	}
-	if labels.BannerImps {
-		impLabels[bannerLabel] = "yes"
-	}
-	if labels.VideoImps {
-		impLabels[videoLabel] = "yes"
-	}
-	if labels.AudioImps {
-		impLabels[audioLabel] = "yes"
-	}
-	if labels.NativeImps {
-		impLabels[nativeLabel] = "yes"
-	}
-	return impLabels
-}
+func (m *Metrics) RecordAdapterRequest(labels pbsmetrics.AdapterLabels) {
+	m.adapterRequests.With(prometheus.Labels{
+		adapterLabel: string(labels.Adapter),
+		cookieLabel:  string(labels.CookieFlag),
+		hasBidsLabel: strconv.FormatBool(labels.AdapterBids == pbsmetrics.AdapterBidPresent),
+	}).Inc()
 
-func resolveRequestLabels(labels pbsmetrics.RequestLabels) prometheus.Labels {
-	return prometheus.Labels{
-		responseStatusLabel: string(labels.RequestStatus),
+	for err := range labels.AdapterErrors {
+		m.adapterErrors.With(prometheus.Labels{
+			adapterLabel:      string(labels.Adapter),
+			adapterErrorLabel: string(err),
+		}).Inc()
 	}
 }
 
-// initializeTimeSeries precreates all possible metric label values, so there is no locking needed at run time creating new instances
-func initializeTimeSeries(m *Metrics) {
-	// Connection errors
-	labels := addDimension([]prometheus.Labels{}, "ErrorType", []string{"accept_error", "close_error"})
-	for _, l := range labels {
-		_ = m.connError.With(l)
-	}
-
-	// Standard labels
-	labels = addDimension([]prometheus.Labels{}, demandSourceLabel, demandTypesAsString())
-	labels = addDimension(labels, requestTypeLabel, requestTypesAsString())
-	labels = addDimension(labels, browserLabel, browserTypesAsString())
-	labels = addDimension(labels, cookieLabel, cookieTypesAsString())
-	adapterLabels := labels // save regenerating these dimensions for adapter status
-	labels = addDimension(labels, responseStatusLabel, requestStatusesAsString())
-	// If we implement an account whitelist, we can seed the metrics with that list to redusce latency associated with registering new lable values on the fly.
-	labels = addDimension(labels, accountLabel, []string{pbsmetrics.PublisherUnknown})
-	for _, l := range labels {
-		_ = m.requests.With(l)
-		_ = m.reqTimer.With(l)
-	}
-
-	// Adapter labels
-	labels = addDimension(adapterLabels, adapterLabel, adaptersAsString())
-	errorLabels := labels // save regenerating these dimensions for adapter errors
-	labels = addDimension(labels, adapterBidLabel, adapterBidsAsString())
-	for _, l := range labels {
-		_ = m.adaptRequests.With(l)
-		_ = m.adaptTimer.With(l)
-		_ = m.adaptPrices.With(l)
-		_ = m.adaptPanics.With(l)
-	}
-
-	// AdapterBid labels
-	labels = addDimension(labels, bidTypeLabel, bidTypesAsString())
-	labels = addDimension(labels, markupTypeLabel, []string{"unknown", "adm"})
-	for _, l := range labels {
-		_ = m.adaptBids.With(l)
-	}
-	labels = addDimension(errorLabels, adapterErrLabel, adapterErrorsAsString())
-	for _, l := range labels {
-		_ = m.adaptErrors.With(l)
-	}
-	cookieLabels := addDimension([]prometheus.Labels{}, adapterLabel, adaptersAsString())
-	cookieLabels = addDimension(cookieLabels, gdprBlockedLabel, []string{"true", "false"})
-	for _, l := range cookieLabels {
-		_ = m.adaptCookieSync.With(l)
-	}
-	cacheLabels := addDimension([]prometheus.Labels{}, "cache_result", cacheResultAsString())
-	for _, l := range cacheLabels {
-		_ = m.storedImpCacheResult.With(l)
-		_ = m.storedReqCacheResult.With(l)
-	}
-
-	// ImpType labels
-	impTypeLabels := addDimension([]prometheus.Labels{}, bannerLabel, []string{"yes", "no"})
-	impTypeLabels = addDimension(impTypeLabels, videoLabel, []string{"yes", "no"})
-	impTypeLabels = addDimension(impTypeLabels, audioLabel, []string{"yes", "no"})
-	impTypeLabels = addDimension(impTypeLabels, nativeLabel, []string{"yes", "no"})
-	for _, l := range impTypeLabels {
-		_ = m.imps.With(l)
-	}
+func (m *Metrics) RecordAdapterPanic(labels pbsmetrics.AdapterLabels) {
+	m.adapterPanics.With(prometheus.Labels{
+		adapterLabel: string(labels.Adapter),
+	}).Inc()
 }
 
-// addDimesion will expand a slice of labels to add the dimension of a new set of values for a new label name
-func addDimension(labels []prometheus.Labels, field string, values []string) []prometheus.Labels {
-	if len(labels) == 0 {
-		// We are starting a new slice of labels, so we can't loop.
-		return addToLabel(make(prometheus.Labels), field, values)
-	}
-	newLabels := make([]prometheus.Labels, 0, len(labels)*len(values))
-	for _, l := range labels {
-		newLabels = append(newLabels, addToLabel(l, field, values)...)
+func (m *Metrics) RecordAdapterBidReceived(labels pbsmetrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
+	markupDelivery := markupDeliveryNurl
+	if hasAdm {
+		markupDelivery = markupDeliveryAdm
 	}
-	return newLabels
-}
 
-// addToLabel will create a slice of labels adding a set of values tied to a label name.
-func addToLabel(label prometheus.Labels, field string, values []string) []prometheus.Labels {
-	newLabels := make([]prometheus.Labels, len(values))
-	for i, v := range values {
-		l := copyLabel(label)
-		l[field] = v
-		newLabels[i] = l
-	}
-	return newLabels
+	m.adapterBids.With(prometheus.Labels{
+		adapterLabel:        string(labels.Adapter),
+		markupDeliveryLabel: markupDelivery,
+	}).Inc()
 }
 
-// Need to be able to deep copy prometheus labels.
-func copyLabel(label prometheus.Labels) prometheus.Labels {
-	newLabel := make(prometheus.Labels)
-	for k, v := range label {
-		newLabel[k] = v
-	}
-	return newLabel
+func (m *Metrics) RecordAdapterPrice(labels pbsmetrics.AdapterLabels, cpm float64) {
+	m.adapterPrices.With(prometheus.Labels{
+		adapterLabel: string(labels.Adapter),
+	}).Observe(cpm)
 }
 
-func demandTypesAsString() []string {
-	list := pbsmetrics.DemandTypes()
-	output := make([]string, len(list))
-	for i, s := range list {
-		output[i] = string(s)
+func (m *Metrics) RecordAdapterTime(labels pbsmetrics.AdapterLabels, length time.Duration) {
+	if len(labels.AdapterErrors) == 0 {
+		m.adapterRequestsTimer.With(prometheus.Labels{
+			adapterLabel: string(labels.Adapter),
+		}).Observe(length.Seconds())
 	}
-	return output
 }
 
-func requestTypesAsString() []string {
-	list := pbsmetrics.RequestTypes()
-	output := make([]string, len(list))
-	for i, s := range list {
-		output[i] = string(s)
-	}
-	return output
-}
-
-func browserTypesAsString() []string {
-	list := pbsmetrics.BrowserTypes()
-	output := make([]string, len(list))
-	for i, s := range list {
-		output[i] = string(s)
-	}
-	return output
+func (m *Metrics) RecordCookieSync() {
+	m.cookieSync.Inc()
 }
 
-func cookieTypesAsString() []string {
-	list := pbsmetrics.CookieTypes()
-	output := make([]string, len(list))
-	for i, s := range list {
-		output[i] = string(s)
-	}
-	return output
+func (m *Metrics) RecordAdapterCookieSync(adapter openrtb_ext.BidderName, privacyBlocked bool) {
+	m.adapterCookieSync.With(prometheus.Labels{
+		adapterLabel:        string(adapter),
+		privacyBlockedLabel: strconv.FormatBool(privacyBlocked),
+	}).Inc()
 }
 
-func requestStatusesAsString() []string {
-	list := pbsmetrics.RequestStatuses()
-	output := make([]string, len(list))
-	for i, s := range list {
-		output[i] = string(s)
+func (m *Metrics) RecordUserIDSet(labels pbsmetrics.UserLabels) {
+	adapter := string(labels.Bidder)
+	if adapter != "" {
+		m.adapterUserSync.With(prometheus.Labels{
+			adapterLabel: adapter,
+			actionLabel:  string(labels.Action),
+		}).Inc()
 	}
-	return output
 }
 
-func adapterBidsAsString() []string {
-	list := pbsmetrics.AdapterBids()
-	output := make([]string, len(list))
-	for i, s := range list {
-		output[i] = string(s)
-	}
-	return output
-}
-
-func adapterErrorsAsString() []string {
-	list := pbsmetrics.AdapterErrors()
-	output := make([]string, len(list))
-	for i, s := range list {
-		output[i] = string(s)
-	}
-	return output
-}
-
-func cacheResultAsString() []string {
-	list := pbsmetrics.CacheResults()
-	output := make([]string, len(list))
-	for i, s := range list {
-		output[i] = string(s)
-	}
-	return output
+func (m *Metrics) RecordStoredReqCacheResult(cacheResult pbsmetrics.CacheResult, inc int) {
+	m.storedRequestCacheResult.With(prometheus.Labels{
+		cacheResultLabel: string(cacheResult),
+	}).Add(float64(inc))
 }
 
-func adaptersAsString() []string {
-	list := openrtb_ext.BidderList()
-	output := make([]string, len(list))
-	for i, s := range list {
-		output[i] = string(s)
-	}
-	return output
-
+func (m *Metrics) RecordStoredImpCacheResult(cacheResult pbsmetrics.CacheResult, inc int) {
+	m.storedImpressionsCacheResult.With(prometheus.Labels{
+		cacheResultLabel: string(cacheResult),
+	}).Add(float64(inc))
 }
 
-func bidTypesAsString() []string {
-	list := openrtb_ext.BidTypes()
-	output := make([]string, len(list))
-	for i, s := range list {
-		output[i] = string(s)
-	}
-	return output
-
+func (m *Metrics) RecordPrebidCacheRequestTime(success bool, length time.Duration) {
+	m.prebidCacheWriteTimer.With(prometheus.Labels{
+		successLabel: strconv.FormatBool(success),
+	}).Observe(length.Seconds())
 }
diff --git a/pbsmetrics/prometheus/type_conversion.go b/pbsmetrics/prometheus/type_conversion.go
new file mode 100644
index 00000000..8294ede0
--- /dev/null
+++ b/pbsmetrics/prometheus/type_conversion.go
@@ -0,0 +1,78 @@
+package prometheusmetrics
+
+import (
+	"strconv"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/pbsmetrics"
+)
+
+func actionsAsString() []string {
+	values := pbsmetrics.RequestActions()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
+
+func adaptersAsString() []string {
+	values := openrtb_ext.BidderList()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
+
+func adapterErrorsAsString() []string {
+	values := pbsmetrics.AdapterErrors()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
+
+func boolValuesAsString() []string {
+	return []string{
+		strconv.FormatBool(true),
+		strconv.FormatBool(false),
+	}
+}
+
+func cookieTypesAsString() []string {
+	values := pbsmetrics.CookieTypes()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
+
+func cacheResultsAsString() []string {
+	values := pbsmetrics.CacheResults()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
+
+func requestStatusesAsString() []string {
+	values := pbsmetrics.RequestStatuses()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
+
+func requestTypesAsString() []string {
+	values := pbsmetrics.RequestTypes()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
diff --git a/prebid_cache_client/client.go b/prebid_cache_client/client.go
index 10acd363..6da69f68 100644
--- a/prebid_cache_client/client.go
+++ b/prebid_cache_client/client.go
@@ -72,9 +72,11 @@ type clientImpl struct {
 func (c *clientImpl) GetExtCacheData() (string, string) {
 	path := c.externalCachePath
 	if path == "/" {
+		// Only the slash for the path, remove it to empty
 		path = ""
-	} else if strings.Index(path, "/") == 0 {
-		path = strings.TrimLeft(path, "/")
+	} else if len(path) > 0 && !strings.HasPrefix(path, "/") {
+		// Path defined but does not start with "/", prepend it
+		path = "/" + path
 	}
 
 	return c.externalCacheHost, path
@@ -109,14 +111,14 @@ func (c *clientImpl) PutJson(ctx context.Context, values []Cacheable) (uuids []s
 	anResp, err := ctxhttp.Do(ctx, c.httpClient, httpReq)
 	elapsedTime := time.Since(startTime)
 	if err != nil {
-		c.metrics.RecordPrebidCacheRequestTime(pbsmetrics.RequestLabels{RequestStatus: pbsmetrics.RequestStatusErr}, elapsedTime)
+		c.metrics.RecordPrebidCacheRequestTime(false, elapsedTime)
 		friendlyErr := fmt.Errorf("Error sending the request to Prebid Cache: %v; Duration=%v", err, elapsedTime)
 		glog.Error(friendlyErr)
 		errs = append(errs, friendlyErr)
 		return uuidsToReturn, errs
 	}
 	defer anResp.Body.Close()
-	c.metrics.RecordPrebidCacheRequestTime(pbsmetrics.RequestLabels{RequestStatus: pbsmetrics.RequestStatusOK}, elapsedTime)
+	c.metrics.RecordPrebidCacheRequestTime(true, elapsedTime)
 
 	responseBody, err := ioutil.ReadAll(anResp.Body)
 	if anResp.StatusCode != 200 {
diff --git a/privacy/ccpa/ccpa.go b/privacy/ccpa/ccpa.go
new file mode 100644
index 00000000..612c1073
--- /dev/null
+++ b/privacy/ccpa/ccpa.go
@@ -0,0 +1,33 @@
+package ccpa
+
+import (
+	"encoding/json"
+
+	"github.com/buger/jsonparser"
+	"github.com/mxmCherry/openrtb"
+)
+
+// Policy represents the CCPA regulation for an OpenRTB bid request.
+type Policy struct {
+	Value string
+}
+
+// Write mutates an OpenRTB bid request with the context of the CCPA policy.
+func (p Policy) Write(req *openrtb.BidRequest) error {
+	if p.Value == "" {
+		return nil
+	}
+
+	if req.Regs == nil {
+		req.Regs = &openrtb.Regs{}
+	}
+
+	if req.Regs.Ext == nil {
+		req.Regs.Ext = json.RawMessage(`{"us_privacy":"` + p.Value + `"}`)
+		return nil
+	}
+
+	var err error
+	req.Regs.Ext, err = jsonparser.Set(req.Regs.Ext, []byte(`"`+p.Value+`"`), "us_privacy")
+	return err
+}
diff --git a/privacy/gdpr/gdpr.go b/privacy/gdpr/gdpr.go
new file mode 100644
index 00000000..60d6fe8e
--- /dev/null
+++ b/privacy/gdpr/gdpr.go
@@ -0,0 +1,33 @@
+package gdpr
+
+import (
+	"encoding/json"
+
+	"github.com/buger/jsonparser"
+	"github.com/mxmCherry/openrtb"
+)
+
+// Policy represents the GDPR regulation of an OpenRTB bid request.
+type Policy struct {
+	Consent string
+}
+
+// Write mutates an OpenRTB bid request with the context of the GDPR policy.
+func (p Policy) Write(req *openrtb.BidRequest) error {
+	if p.Consent == "" {
+		return nil
+	}
+
+	if req.User == nil {
+		req.User = &openrtb.User{}
+	}
+
+	if req.User.Ext == nil {
+		req.User.Ext = json.RawMessage(`{"consent":"` + p.Consent + `"}`)
+		return nil
+	}
+
+	var err error
+	req.User.Ext, err = jsonparser.Set(req.User.Ext, []byte(`"`+p.Consent+`"`), "consent")
+	return err
+}
diff --git a/privacy/policies.go b/privacy/policies.go
new file mode 100644
index 00000000..ebe34ef5
--- /dev/null
+++ b/privacy/policies.go
@@ -0,0 +1,35 @@
+package privacy
+
+import (
+	"github.com/mxmCherry/openrtb"
+
+	"github.com/prebid/prebid-server/privacy/ccpa"
+	"github.com/prebid/prebid-server/privacy/gdpr"
+)
+
+// Policies represents the privacy regulations for an OpenRTB bid request.
+type Policies struct {
+	GDPR gdpr.Policy
+	CCPA ccpa.Policy
+}
+
+type policyWriter interface {
+	Write(req *openrtb.BidRequest) error
+}
+
+// Write mutates an OpenRTB bid request with the policies applied.
+func (p Policies) Write(req *openrtb.BidRequest) error {
+	return writePolicies(req, []policyWriter{
+		p.GDPR, p.CCPA,
+	})
+}
+
+func writePolicies(req *openrtb.BidRequest, writers []policyWriter) error {
+	for _, writer := range writers {
+		if err := writer.Write(req); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}


Test patch:
diff --git a/adapters/datablocks/usersync_test.go b/adapters/datablocks/usersync_test.go
index e31fff74..f91fc94a 100644
--- a/adapters/datablocks/usersync_test.go
+++ b/adapters/datablocks/usersync_test.go
@@ -7,11 +7,11 @@ import (
 )
 
 func TestDatablocksSyncer(t *testing.T) {
-	temp := template.Must(template.New("sync-template").Parse("https://search.v5prebid.datablocks.net/s2ssync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r=https%3A%2F%2Flocalhost%3A8888%2Fsetuid%3Fbidder%3Ddatablocks%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7Buid%7D"))
+	temp := template.Must(template.New("sync-template").Parse("https://sync.v5prebid.datablocks.net/s2ssync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r=https%3A%2F%2Flocalhost%3A8888%2Fsetuid%3Fbidder%3Ddatablocks%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7Buid%7D"))
 	syncer := NewDatablocksSyncer(temp)
 	syncInfo, err := syncer.GetUsersyncInfo("1", "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw")
 	assert.NoError(t, err)
-	assert.Equal(t, "https://search.v5prebid.datablocks.net/s2ssync?gdpr=1&gdpr_consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw&r=https%3A%2F%2Flocalhost%3A8888%2Fsetuid%3Fbidder%3Ddatablocks%26gdpr%3D1%26gdpr_consent%3DBONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw%26uid%3D%24%7Buid%7D", syncInfo.URL)
+	assert.Equal(t, "https://sync.v5prebid.datablocks.net/s2ssync?gdpr=1&gdpr_consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw&r=https%3A%2F%2Flocalhost%3A8888%2Fsetuid%3Fbidder%3Ddatablocks%26gdpr%3D1%26gdpr_consent%3DBONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw%26uid%3D%24%7Buid%7D", syncInfo.URL)
 	assert.Equal(t, "redirect", syncInfo.Type)
 	assert.EqualValues(t, datablocksGDPRVendorID, syncer.GDPRVendorID())
 	assert.Equal(t, false, syncInfo.SupportCORS)
diff --git a/currencies/rates_test.go b/currencies/rates_test.go
index a8e043a0..915b817d 100644
--- a/currencies/rates_test.go
+++ b/currencies/rates_test.go
@@ -163,6 +163,60 @@ func TestGetRate(t *testing.T) {
 	}
 }
 
+func TestGetRate_ReverseConversion(t *testing.T) {
+
+	// Setup:
+	rates := currencies.NewRates(time.Now(), map[string]map[string]float64{
+		"USD": {
+			"GBP": 0.77208,
+		},
+		"EUR": {
+			"USD": 0.88723,
+		},
+	})
+
+	testCases := []struct {
+		from         string
+		to           string
+		expectedRate float64
+		description  string
+	}{
+		{
+			from:         "USD",
+			to:           "GBP",
+			expectedRate: 0.77208,
+			description:  "case 1 - Rate is present directly and will be returned as is",
+		},
+		{
+			from:         "EUR",
+			to:           "USD",
+			expectedRate: 0.88723,
+			description:  "case 2 - Rate is present directly and will be returned as is (2)",
+		},
+		{
+			from:         "GBP",
+			to:           "USD",
+			expectedRate: 1 / 0.77208,
+			description:  "case 3 - Rate is not present but the reverse one is, will return the computed rate from the reverse entry",
+		},
+		{
+			from:         "USD",
+			to:           "EUR",
+			expectedRate: 1 / 0.88723,
+			description:  "case 4 - Rate is not present but the reverse one is, will return the computed rate from the reverse entry (2)",
+		},
+	}
+
+	for _, tc := range testCases {
+		// Execute:
+		rate, err := rates.GetRate(tc.from, tc.to)
+
+		// Verify:
+		assert.Nil(t, err, "err should be nil: "+tc.description)
+		assert.Equal(t, tc.expectedRate, rate, "rate doesn't match the expected one: "+tc.description)
+	}
+}
+
 func TestGetRate_EmptyRates(t *testing.T) {
 
 	// Setup:
diff --git a/endpoints/auction_test.go b/endpoints/auction_test.go
index 2abee138..e968003a 100644
--- a/endpoints/auction_test.go
+++ b/endpoints/auction_test.go
@@ -341,7 +341,6 @@ func TestCacheVideoOnly(t *testing.T) {
 	bids = append(bids, &rbVideoBid2)
 
 	ctx := context.TODO()
-	w := httptest.NewRecorder()
 	v := viper.New()
 	config.SetupViper(v, "")
 	cfg, err := config.New(v)
@@ -353,7 +352,11 @@ func TestCacheVideoOnly(t *testing.T) {
 		HostVendorID: 0,
 	}, nil, nil)
 	prebid_cache_client.InitPrebidCache(server.URL)
-	cacheVideoOnly(bids, ctx, w, &auction{cfg: cfg, syncers: syncers, gdprPerms: gdprPerms, metricsEngine: &metricsConf.DummyMetricsEngine{}}, &pbsmetrics.Labels{})
+	var labels = &pbsmetrics.Labels{}
+	if err := cacheVideoOnly(bids, ctx, &auction{cfg: cfg, syncers: syncers, gdprPerms: gdprPerms, metricsEngine: &metricsConf.DummyMetricsEngine{}}, labels); err != nil {
+		t.Errorf("Prebid cache failed: %v \n", err)
+		return
+	}
 	if bids[0].CacheID != "UUID-1" {
 		t.Errorf("UUID was '%s', should have been 'UUID-1'", bids[0].CacheID)
 	}
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 49276f03..0eafaa05 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -739,6 +739,102 @@ func TestWidthOnly(t *testing.T) {
 	}.execute(t)
 }
 
+func TestCCPAPresent(t *testing.T) {
+	req, err := getTestBidRequest(false, false, "", "digitrustId")
+	if err != nil {
+		t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err)
+	}
+
+	reqStored := map[string]json.RawMessage{
+		"1": json.RawMessage(req),
+	}
+
+	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
+
+	exchange := &mockAmpExchange{}
+
+	endpoint, _ := NewAmpEndpoint(
+		exchange,
+		newParamsValidator(t),
+		&mockAmpStoredReqFetcher{reqStored},
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{MaxRequestSize: maxSize},
+		theMetrics,
+		analyticsConf.NewPBSAnalytics(&config.Analytics{}),
+		map[string]string{},
+		[]byte{},
+		openrtb_ext.BidderMap,
+	)
+
+	usPrivacy := "1YYN"
+	httpReq := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1&us_privacy="+usPrivacy, nil)
+	httpRecorder := httptest.NewRecorder()
+	endpoint(httpRecorder, httpReq, nil)
+
+	// Assert our bidRequest was valid
+	if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", httpRecorder.Code, httpRecorder.Body.String()) {
+		return
+	}
+	// Assert our bidRequest had a valid "Regs" field
+	if !assert.NotNil(t, exchange.lastRequest.Regs) {
+		return
+	}
+	// Assert our bidRequest had a valid "Regs.Ext" field
+	if !assert.NotNil(t, exchange.lastRequest.Regs.Ext) {
+		return
+	}
+
+	var regs openrtb_ext.ExtRegs
+	err = json.Unmarshal(exchange.lastRequest.Regs.Ext, &regs)
+	assert.NoError(t, err)
+	assert.Equal(t, usPrivacy, regs.USPrivacy)
+}
+
+func TestCCPANotPresent(t *testing.T) {
+	req, err := getTestBidRequest(false, false, "", "digitrustId")
+	if err != nil {
+		t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err)
+	}
+
+	reqStored := map[string]json.RawMessage{
+		"1": json.RawMessage(req),
+	}
+
+	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
+
+	exchange := &mockAmpExchange{}
+
+	endpoint, _ := NewAmpEndpoint(
+		exchange,
+		newParamsValidator(t),
+		&mockAmpStoredReqFetcher{reqStored},
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{MaxRequestSize: maxSize},
+		theMetrics,
+		analyticsConf.NewPBSAnalytics(&config.Analytics{}),
+		map[string]string{},
+		[]byte{},
+		openrtb_ext.BidderMap,
+	)
+
+	httpReq := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1", nil)
+	httpRecorder := httptest.NewRecorder()
+	endpoint(httpRecorder, httpReq, nil)
+
+	// Assert our bidRequest was valid
+	if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", httpRecorder.Code, httpRecorder.Body.String()) {
+		return
+	}
+
+	// Assert CCPA Signal Not Found
+	if exchange.lastRequest.Regs != nil && exchange.lastRequest.Regs.Ext != nil {
+		var regs openrtb_ext.ExtRegs
+		err = json.Unmarshal(exchange.lastRequest.Regs.Ext, &regs)
+		assert.NoError(t, err)
+		assert.Empty(t, regs.USPrivacy)
+	}
+}
+
 type formatOverrideSpec struct {
 	width          uint64
 	height         uint64
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 4818c556..7f3d3d4c 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -282,7 +282,6 @@ func TestRejectAccountRequired(t *testing.T) {
 func (gr *getResponseFromDirectory) assert(t *testing.T) {
 	//t *testing.T, dir string, payloadGetter func(*testing.T, []byte) []byte, messageGetter func(*testing.T, []byte) []byte, expectedCode int, aliased bool) {
 	t.Helper()
-	var filename string
 	var filesToAssert []string
 	if gr.file == "" {
 		// Append every file found in `gr.dir` to the `filesToAssert` array and test them all
@@ -300,14 +299,14 @@ func (gr *getResponseFromDirectory) assert(t *testing.T) {
 		fileData = readFile(t, testFile)
 		code, msg := gr.doRequest(t, gr.payloadGetter(t, fileData))
 		fmt.Printf("Processing %s\n", testFile)
-		assertResponseCode(t, filename, code, gr.expectedCode, msg)
+		assertResponseCode(t, testFile, code, gr.expectedCode, msg)
 
 		expectMsg := gr.messageGetter(t, fileData)
 		if gr.description != "" {
 			if len(expectMsg) > 0 {
-				assert.Equal(t, string(expectMsg), msg, "Test failed. %s. Filename: \n", gr.description, filename)
+				assert.Equal(t, string(expectMsg), msg, "Test failed. %s. Filename: \n", gr.description, testFile)
 			} else {
-				assert.Equal(t, string(expectMsg), msg, "file %s had bad response body", filename)
+				assert.Equal(t, string(expectMsg), msg, "file %s had bad response body", testFile)
 			}
 		}
 	}
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index 85285a30..6a8e913a 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -3,12 +3,14 @@ package openrtb2
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"io/ioutil"
 	"net/http/httptest"
 	"strings"
 	"testing"
 
 	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/analytics"
 	analyticsConf "github.com/prebid/prebid-server/analytics/config"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/exchange"
@@ -641,6 +643,91 @@ func TestMergeOpenRTBToVideoRequest(t *testing.T) {
 	assert.Equal(t, videoReq.Site.Page, bidReq.Site.Page, "Device.Site.Page is incorrect")
 }
 
+func TestHandleError(t *testing.T) {
+	ao := analytics.AuctionObject{
+		Status: 200,
+		Errors: make([]error, 0),
+	}
+
+	labels := pbsmetrics.Labels{
+		Source:        pbsmetrics.DemandUnknown,
+		RType:         pbsmetrics.ReqTypeVideo,
+		PubID:         pbsmetrics.PublisherUnknown,
+		Browser:       "test browser",
+		CookieFlag:    pbsmetrics.CookieFlagUnknown,
+		RequestStatus: pbsmetrics.RequestStatusOK,
+	}
+
+	recorder := httptest.NewRecorder()
+	err1 := errors.New("Error for testing handleError 1")
+	err2 := errors.New("Error for testing handleError 2")
+	handleError(&labels, recorder, []error{err1, err2}, &ao)
+
+	assert.Equal(t, pbsmetrics.RequestStatusErr, labels.RequestStatus, "labels.RequestStatus should indicate an error")
+	assert.Equal(t, 500, recorder.Code, "Error status should be written to writer")
+	assert.Equal(t, 500, ao.Status, "AnalyticsObject should have error status")
+	assert.Equal(t, 2, len(ao.Errors), "New errors should be appended to AnalyticsObject Errors")
+	assert.Equal(t, "Error for testing handleError 1", ao.Errors[0].Error(), "Error in AnalyticsObject should have test error message for first error")
+	assert.Equal(t, "Error for testing handleError 2", ao.Errors[1].Error(), "Error in AnalyticsObject should have test error message for second error")
+}
+
+func TestHandleErrorMetrics(t *testing.T) {
+	ex := &mockExchangeVideo{}
+	reqData, err := ioutil.ReadFile("sample-requests/video/video_invalid_sample.json")
+	if err != nil {
+		t.Fatalf("Failed to fetch a valid request: %v", err)
+	}
+	reqBody := string(getRequestPayload(t, reqData))
+	req := httptest.NewRequest("POST", "/openrtb2/video", strings.NewReader(reqBody))
+	recorder := httptest.NewRecorder()
+
+	deps, met, mod := mockDepsWithMetrics(t, ex)
+	deps.VideoAuctionEndpoint(recorder, req, nil)
+
+	assert.Equal(t, int64(0), met.RequestStatuses[pbsmetrics.ReqTypeVideo][pbsmetrics.RequestStatusOK].Count(), "OK requests count should be 0")
+	assert.Equal(t, int64(1), met.RequestStatuses[pbsmetrics.ReqTypeVideo][pbsmetrics.RequestStatusErr].Count(), "Error requests count should be 1")
+	assert.Equal(t, 1, len(mod.auctionObjects), "Mock AnalyticsModule should have 1 AuctionObject")
+	assert.Equal(t, 500, mod.auctionObjects[0].Status, "AnalyticsObject should have 500 status")
+	assert.Equal(t, 2, len(mod.auctionObjects[0].Errors), "AnalyticsObject should have Errors length of 2")
+	assert.Equal(t, "request missing required field: PodConfig.DurationRangeSec", mod.auctionObjects[0].Errors[0].Error(), "First error in AnalyticsObject should have message regarding DurationRangeSec")
+	assert.Equal(t, "request missing required field: PodConfig.Pods", mod.auctionObjects[0].Errors[1].Error(), "Second error in AnalyticsObject should have message regarding Pods")
+}
+
+func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *pbsmetrics.Metrics, *mockAnalyticsModule) {
+	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
+	mockModule := &mockAnalyticsModule{}
+	edep := &endpointDeps{
+		ex,
+		newParamsValidator(t),
+		&mockVideoStoredReqFetcher{},
+		&mockVideoStoredReqFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{MaxRequestSize: maxSize},
+		theMetrics,
+		mockModule,
+		map[string]string{},
+		false,
+		[]byte{},
+		openrtb_ext.BidderMap,
+	}
+
+	return edep, theMetrics, mockModule
+}
+
+type mockAnalyticsModule struct {
+	auctionObjects []*analytics.AuctionObject
+}
+
+func (m *mockAnalyticsModule) LogAuctionObject(ao *analytics.AuctionObject) {
+	m.auctionObjects = append(m.auctionObjects, ao)
+}
+
+func (m *mockAnalyticsModule) LogCookieSyncObject(cso *analytics.CookieSyncObject) { return }
+
+func (m *mockAnalyticsModule) LogSetUIDObject(so *analytics.SetUIDObject) { return }
+
+func (m *mockAnalyticsModule) LogAmpObject(ao *analytics.AmpObject) { return }
+
 func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps {
 	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
 	edep := &endpointDeps{
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index f5bf23d5..2b0e37e6 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -1291,7 +1291,7 @@ func mockHandler(statusCode int, getBody string, postBody string) http.Handler {
 type wellBehavedCache struct{}
 
 func (c *wellBehavedCache) GetExtCacheData() (string, string) {
-	return "www.pbcserver.com", "pbcache/endpoint"
+	return "www.pbcserver.com", "/pbcache/endpoint"
 }
 
 func (c *wellBehavedCache) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) ([]string, []error) {
diff --git a/exchange/exchangetest/include-brand-category.json b/exchange/exchangetest/include-brand-category.json
index f27c0e6d..68e93e3a 100644
--- a/exchange/exchangetest/include-brand-category.json
+++ b/exchange/exchangetest/include-brand-category.json
@@ -106,8 +106,8 @@
                      "hb_bidder_appnexus": "appnexus",
                      "hb_cache_host": "www.pbcserver.com",
                      "hb_cache_host_appnex": "www.pbcserver.com",
-                     "hb_cache_path": "pbcache/endpoint",
-                     "hb_cache_path_appnex": "pbcache/endpoint",
+                     "hb_cache_path": "/pbcache/endpoint",
+                     "hb_cache_path_appnex": "/pbcache/endpoint",
                      "hb_pb": "0.20",
                      "hb_pb_appnexus": "0.20",
                      "hb_pb_cat_dur": "0.20_VideoGames_0s",
@@ -128,8 +128,8 @@
                       "hb_bidder_appnexus": "appnexus",
                       "hb_cache_host": "www.pbcserver.com",
                       "hb_cache_host_appnex": "www.pbcserver.com",
-                      "hb_cache_path": "pbcache/endpoint",
-                      "hb_cache_path_appnex": "pbcache/endpoint",
+                      "hb_cache_path": "/pbcache/endpoint",
+                      "hb_cache_path_appnex": "/pbcache/endpoint",
                       "hb_pb": "0.50",
                       "hb_pb_appnexus": "0.50",
                       "hb_pb_cat_dur": "0.50_HomeDecor_0s",
diff --git a/exchange/exchangetest/targeting-cache-vast-banner.json b/exchange/exchangetest/targeting-cache-vast-banner.json
index 5d0db274..215485cc 100644
--- a/exchange/exchangetest/targeting-cache-vast-banner.json
+++ b/exchange/exchangetest/targeting-cache-vast-banner.json
@@ -71,8 +71,8 @@
                   "hb_bidder_appnexus": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_pb": "0.00",
                   "hb_pb_appnexus": "0.00",
                   "hb_size": "200x250",
diff --git a/exchange/exchangetest/targeting-cache-vast.json b/exchange/exchangetest/targeting-cache-vast.json
index 743f2ce3..f348dd1b 100644
--- a/exchange/exchangetest/targeting-cache-vast.json
+++ b/exchange/exchangetest/targeting-cache-vast.json
@@ -78,8 +78,8 @@
                   "hb_bidder_appnexus": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_uuid": "0",
                   "hb_uuid_appnexus": "0",
                   "hb_pb": "0.00",
diff --git a/exchange/exchangetest/targeting-cache-zero.json b/exchange/exchangetest/targeting-cache-zero.json
index abe9df10..51301530 100644
--- a/exchange/exchangetest/targeting-cache-zero.json
+++ b/exchange/exchangetest/targeting-cache-zero.json
@@ -83,8 +83,8 @@
                   "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_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_uuid": "1",
                   "hb_uuid_appnexus": "1",
                   "hb_pb": "0.00",
diff --git a/exchange/exchangetest/targeting-mobile.json b/exchange/exchangetest/targeting-mobile.json
index d5661565..f3962cff 100644
--- a/exchange/exchangetest/targeting-mobile.json
+++ b/exchange/exchangetest/targeting-mobile.json
@@ -123,7 +123,7 @@
                 "targeting": {
                   "hb_bidder_audienceNe": "audienceNetwork",
                   "hb_cache_host_audien": "www.pbcserver.com",
-                  "hb_cache_path_audien": "pbcache/endpoint",
+                  "hb_cache_path_audien": "/pbcache/endpoint",
                   "hb_pb_audienceNetwor": "0.50",
                   "hb_size_audienceNetw": "200x250",
                   "hb_env_audienceNetwo": "mobile-app"
@@ -148,8 +148,8 @@
                   "hb_bidder": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_bidder_appnexus": "appnexus",
                   "hb_pb": "0.70",
                   "hb_pb_appnexus": "0.70",
@@ -188,8 +188,8 @@
                   "hb_bidder": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_bidder_appnexus": "appnexus",
                   "hb_pb": "0.60",
                   "hb_pb_appnexus": "0.60",
diff --git a/exchange/exchangetest/targeting-no-winners.json b/exchange/exchangetest/targeting-no-winners.json
index 73bf812c..891ff0ff 100644
--- a/exchange/exchangetest/targeting-no-winners.json
+++ b/exchange/exchangetest/targeting-no-winners.json
@@ -125,7 +125,7 @@
                 "targeting": {
                   "hb_bidder_audienceNe": "audienceNetwork",
                   "hb_cache_host_audien": "www.pbcserver.com",
-                  "hb_cache_path_audien": "pbcache/endpoint",
+                  "hb_cache_path_audien": "/pbcache/endpoint",
                   "hb_pb_audienceNetwor": "0.50",
                   "hb_size_audienceNetw": "200x250"
                 }
@@ -148,7 +148,7 @@
                 "targeting": {
                   "hb_bidder_appnexus": "appnexus",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_pb_appnexus": "0.70",
                   "hb_size_appnexus": "200x250"
                 }
@@ -181,7 +181,7 @@
                 "targeting": {
                   "hb_bidder_appnexus": "appnexus",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_pb_appnexus": "0.60",
                   "hb_size_appnexus": "300x500"
                 }
diff --git a/exchange/exchangetest/targeting-only-winners.json b/exchange/exchangetest/targeting-only-winners.json
index 807b7b6c..5f449af3 100644
--- a/exchange/exchangetest/targeting-only-winners.json
+++ b/exchange/exchangetest/targeting-only-winners.json
@@ -141,7 +141,7 @@
                 "targeting": {
                   "hb_bidder": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
                   "hb_pb": "0.70",
                   "hb_size": "200x250"
                 }
@@ -174,7 +174,7 @@
                 "targeting": {
                   "hb_bidder": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
                   "hb_pb": "0.60",
                   "hb_size": "300x500"
                 }
diff --git a/exchange/exchangetest/targeting-with-winners.json b/exchange/exchangetest/targeting-with-winners.json
index 208b3feb..f081b7ee 100644
--- a/exchange/exchangetest/targeting-with-winners.json
+++ b/exchange/exchangetest/targeting-with-winners.json
@@ -123,7 +123,7 @@
                 "targeting": {
                   "hb_bidder_audienceNe": "audienceNetwork",
                   "hb_cache_host_audien": "www.pbcserver.com",
-                  "hb_cache_path_audien": "pbcache/endpoint",
+                  "hb_cache_path_audien": "/pbcache/endpoint",
                   "hb_pb_audienceNetwor": "0.50",
                   "hb_size_audienceNetw": "200x250"
                 }
@@ -148,8 +148,8 @@
                   "hb_bidder_appnexus": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_pb": "0.70",
                   "hb_pb_appnexus": "0.70",
                   "hb_size": "200x250",
@@ -186,8 +186,8 @@
                   "hb_bidder_appnexus": "appnexus",
                   "hb_cache_host": "www.pbcserver.com",
                   "hb_cache_host_appnex": "www.pbcserver.com",
-                  "hb_cache_path": "pbcache/endpoint",
-                  "hb_cache_path_appnex": "pbcache/endpoint",
+                  "hb_cache_path": "/pbcache/endpoint",
+                  "hb_cache_path_appnex": "/pbcache/endpoint",
                   "hb_pb": "0.60",
                   "hb_pb_appnexus": "0.60",
                   "hb_size": "300x500",
diff --git a/pbsmetrics/config/metrics_test.go b/pbsmetrics/config/metrics_test.go
index 8f4202ff..ad817ba7 100644
--- a/pbsmetrics/config/metrics_test.go
+++ b/pbsmetrics/config/metrics_test.go
@@ -75,9 +75,6 @@ func TestMultiMetricsEngine(t *testing.T) {
 		AudioImps:  true,
 		NativeImps: true,
 	}
-	requestLabels := pbsmetrics.RequestLabels{
-		RequestStatus: pbsmetrics.RequestStatusOK,
-	}
 	for i := 0; i < 5; i++ {
 		metricsEngine.RecordRequest(labels)
 		metricsEngine.RecordImps(impTypeLabels)
@@ -88,7 +85,7 @@ func TestMultiMetricsEngine(t *testing.T) {
 		metricsEngine.RecordAdapterPrice(pubLabels, 1.34)
 		metricsEngine.RecordAdapterBidReceived(pubLabels, openrtb_ext.BidTypeBanner, true)
 		metricsEngine.RecordAdapterTime(pubLabels, time.Millisecond*20)
-		metricsEngine.RecordPrebidCacheRequestTime(requestLabels, time.Millisecond*20)
+		metricsEngine.RecordPrebidCacheRequestTime(true, time.Millisecond*20)
 	}
 	labelsBlacklist := []pbsmetrics.Labels{
 		{
diff --git a/pbsmetrics/go_metrics_test.go b/pbsmetrics/go_metrics_test.go
index 390d49f4..b403733d 100644
--- a/pbsmetrics/go_metrics_test.go
+++ b/pbsmetrics/go_metrics_test.go
@@ -172,25 +172,21 @@ func TestNewMetricsWithDisabledConfig(t *testing.T) {
 	assert.True(t, m.MetricsDisabled.AccountAdapterDetails, "Accound adapter metrics should be disabled")
 }
 
-func TestRecordPrebidCacheRequestTimeWithSuccessLabel(t *testing.T) {
+func TestRecordPrebidCacheRequestTimeWithSuccess(t *testing.T) {
 	registry := metrics.NewRegistry()
 	m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AccountAdapterDetails: true})
 
-	m.RecordPrebidCacheRequestTime(RequestLabels{
-		RequestStatus: RequestStatusOK,
-	}, 42)
+	m.RecordPrebidCacheRequestTime(true, 42)
 
 	assert.Equal(t, m.PrebidCacheRequestTimerSuccess.Count(), int64(1))
 	assert.Equal(t, m.PrebidCacheRequestTimerError.Count(), int64(0))
 }
 
-func TestRecordPrebidCacheRequestTimeWithErrorLabel(t *testing.T) {
+func TestRecordPrebidCacheRequestTimeWithNotSuccess(t *testing.T) {
 	registry := metrics.NewRegistry()
 	m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AccountAdapterDetails: true})
 
-	m.RecordPrebidCacheRequestTime(RequestLabels{
-		RequestStatus: RequestStatusErr,
-	}, 42)
+	m.RecordPrebidCacheRequestTime(false, 42)
 
 	assert.Equal(t, m.PrebidCacheRequestTimerSuccess.Count(), int64(0))
 	assert.Equal(t, m.PrebidCacheRequestTimerError.Count(), int64(1))
diff --git a/pbsmetrics/prometheus/preload_test.go b/pbsmetrics/prometheus/preload_test.go
new file mode 100644
index 00000000..7adc2f01
--- /dev/null
+++ b/pbsmetrics/prometheus/preload_test.go
@@ -0,0 +1,64 @@
+package prometheusmetrics
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+func TestRegisterLabelPermutations(t *testing.T) {
+	testCases := []struct {
+		description      string
+		labelsWithValues map[string][]string
+		expectedLabels   []prometheus.Labels
+	}{
+		{
+			description:      "Empty set.",
+			labelsWithValues: map[string][]string{},
+			expectedLabels:   []prometheus.Labels{},
+		},
+		{
+			description: "Set of 1 label and 1 value.",
+			labelsWithValues: map[string][]string{
+				"1": {"A"},
+			},
+			expectedLabels: []prometheus.Labels{
+				{"1": "A"},
+			},
+		},
+		{
+			description: "Set of 1 label and 2 values.",
+			labelsWithValues: map[string][]string{
+				"1": {"A", "B"},
+			},
+			expectedLabels: []prometheus.Labels{
+				{"1": "A"},
+				{"1": "B"},
+			},
+		},
+		{
+			description: "Set of 2 labels and 2 values.",
+			labelsWithValues: map[string][]string{
+				"1": {"A", "B"},
+				"2": {"C", "D"},
+			},
+			expectedLabels: []prometheus.Labels{
+				{"1": "A", "2": "C"},
+				{"1": "A", "2": "D"},
+				{"1": "B", "2": "C"},
+				{"1": "B", "2": "D"},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		resultLabels := []prometheus.Labels{}
+		registerLabelPermutations(test.labelsWithValues, func(label prometheus.Labels) {
+			resultLabels = append(resultLabels, label)
+		})
+
+		assert.ElementsMatch(t, test.expectedLabels, resultLabels)
+	}
+}
diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go
index 0930e3fe..4cf9676e 100644
--- a/pbsmetrics/prometheus/prometheus_test.go
+++ b/pbsmetrics/prometheus/prometheus_test.go
@@ -1,9 +1,6 @@
 package prometheusmetrics
 
 import (
-	"fmt"
-	"regexp"
-	"strconv"
 	"testing"
 	"time"
 
@@ -15,619 +12,915 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-var gaugeValueRegexp = regexp.MustCompile("gauge:<value:([0-9]+) >")
-var counterValueRegexp = regexp.MustCompile("counter:<value:([0-9]+) >")
-var histogramValueRegexp = regexp.MustCompile("histogram:<sample_count:([0-9]+)")
+func createMetricsForTesting() *Metrics {
+	return NewMetrics(config.PrometheusMetrics{
+		Port:      8080,
+		Namespace: "prebid",
+		Subsystem: "server",
+	})
+}
 
-func TestConnectionMetrics(t *testing.T) {
-	proMetrics := newTestMetricsEngine()
-
-	metricConn := dto.Metric{}
-	metricConnErrA := dto.Metric{}
-	metricConnErrC := dto.Metric{}
-	proMetrics.RecordConnectionAccept(true)
-	proMetrics.RecordConnectionAccept(true)
-	proMetrics.RecordConnectionClose(true)
-	proMetrics.RecordConnectionAccept(true)
-	proMetrics.RecordConnectionAccept(false)
-	proMetrics.RecordConnectionClose(false)
-
-	proMetrics.connCounter.Write(&metricConn)
-	proMetrics.connError.WithLabelValues("accept_error").Write(&metricConnErrA)
-	proMetrics.connError.WithLabelValues("close_error").Write(&metricConnErrC)
-
-	assertGaugeValue(t, "connCounter", &metricConn, 2)
-	assertCounterValue(t, "connError[accept_error]", &metricConnErrA, 1)
-	assertCounterValue(t, "connError[close_error]", &metricConnErrC, 1)
+func TestMetricCountGatekeeping(t *testing.T) {
+	m := createMetricsForTesting()
+
+	// Gather All Metrics
+	metricFamilies, err := m.Registry.Gather()
+	assert.NoError(t, err, "gather metics")
+
+	// Summarize By Adapter Cardinality
+	// - This requires metrics to be preloaded. We don't preload account metrics, so we can't test those.
+	generalCardinalityCount := 0
+	adapterCardinalityCount := 0
+	for _, metricFamily := range metricFamilies {
+		for _, metric := range metricFamily.GetMetric() {
+			isPerAdapter := false
+			for _, label := range metric.GetLabel() {
+				if label.GetName() == adapterLabel {
+					isPerAdapter = true
+				}
+			}
+
+			if isPerAdapter {
+				adapterCardinalityCount++
+			} else {
+				generalCardinalityCount++
+			}
+		}
+	}
+
+	// Calculate Per-Adapter Cardinality
+	adapterCount := len(openrtb_ext.BidderList())
+	perAdapterCardinalityCount := adapterCardinalityCount / adapterCount
+
+	// Verify General Cardinality
+	// - This assertion provides a warning for newly added high-cardinality non-adapter specific metrics. The hardcoded limit
+	//   is an arbitrary soft ceiling. Thought should be given as to the value of the new metrics if you find yourself
+	//   needing to increase this number.
+	assert.True(t, generalCardinalityCount <= 500, "General Cardinality")
+
+	// 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 <= 22, "Per-Adapter Cardinality")
 }
 
-func TestRequestMetrics(t *testing.T) {
-	proMetrics := newTestMetricsEngine()
-
-	metrics0 := dto.Metric{}
-	metrics1 := dto.Metric{}
-	metrics2 := dto.Metric{}
-	metrics3 := dto.Metric{}
-	metrics4 := dto.Metric{}
-
-	proMetrics.RecordRequest(labels[0])
-	proMetrics.RecordRequest(labels[1])
-	proMetrics.RecordRequest(labels[0])
-	proMetrics.RecordRequest(labels[2])
-	proMetrics.RecordRequest(labels[0])
-	proMetrics.RecordRequest(labels[2])
-	proMetrics.RecordRequest(labels[4])
-
-	proMetrics.requests.With(resolveLabels(labels[0])).Write(&metrics0)
-	proMetrics.requests.With(resolveLabels(labels[1])).Write(&metrics1)
-	proMetrics.requests.With(resolveLabels(labels[2])).Write(&metrics2)
-	proMetrics.requests.With(resolveLabels(labels[3])).Write(&metrics3)
-	proMetrics.requests.With(resolveLabels(labels[4])).Write(&metrics4)
-
-	assertCounterValue(t, "requests[0]", &metrics0, 3)
-	assertCounterValue(t, "requests[1]", &metrics1, 1)
-	assertCounterValue(t, "requests[2]", &metrics2, 2)
-	assertCounterValue(t, "requests[3]", &metrics3, 0)
-	assertCounterValue(t, "requests[4]", &metrics4, 1)
+func TestConnectionMetrics(t *testing.T) {
+	testCases := []struct {
+		description              string
+		testCase                 func(m *Metrics)
+		expectedOpenedCount      float64
+		expectedOpenedErrorCount float64
+		expectedClosedCount      float64
+		expectedClosedErrorCount float64
+	}{
+		{
+			description: "Open Success",
+			testCase: func(m *Metrics) {
+				m.RecordConnectionAccept(true)
+			},
+			expectedOpenedCount:      1,
+			expectedOpenedErrorCount: 0,
+			expectedClosedCount:      0,
+			expectedClosedErrorCount: 0,
+		},
+		{
+			description: "Open Error",
+			testCase: func(m *Metrics) {
+				m.RecordConnectionAccept(false)
+			},
+			expectedOpenedCount:      0,
+			expectedOpenedErrorCount: 1,
+			expectedClosedCount:      0,
+			expectedClosedErrorCount: 0,
+		},
+		{
+			description: "Closed Success",
+			testCase: func(m *Metrics) {
+				m.RecordConnectionClose(true)
+			},
+			expectedOpenedCount:      0,
+			expectedOpenedErrorCount: 0,
+			expectedClosedCount:      1,
+			expectedClosedErrorCount: 0,
+		},
+		{
+			description: "Closed Error",
+			testCase: func(m *Metrics) {
+				m.RecordConnectionClose(false)
+			},
+			expectedOpenedCount:      0,
+			expectedOpenedErrorCount: 0,
+			expectedClosedCount:      0,
+			expectedClosedErrorCount: 1,
+		},
+	}
+
+	for _, test := range testCases {
+		m := createMetricsForTesting()
+
+		test.testCase(m)
+
+		assertCounterValue(t, test.description, "connectionsClosed", m.connectionsClosed,
+			test.expectedClosedCount)
+		assertCounterValue(t, test.description, "connectionsOpened", m.connectionsOpened,
+			test.expectedOpenedCount)
+		assertCounterVecValue(t, test.description, "connectionsError[type=accept]", m.connectionsError,
+			test.expectedOpenedErrorCount, prometheus.Labels{
+				connectionErrorLabel: connectionAcceptError,
+			})
+		assertCounterVecValue(t, test.description, "connectionsError[type=close]", m.connectionsError,
+			test.expectedClosedErrorCount, prometheus.Labels{
+				connectionErrorLabel: connectionCloseError,
+			})
+	}
 }
 
-func TestLegacyImpMetrics(t *testing.T) {
-	proMetrics := newTestMetricsEngine()
-
-	metrics0 := dto.Metric{}
-	metrics1 := dto.Metric{}
-	metrics2 := dto.Metric{}
-	metrics3 := dto.Metric{}
-	metrics4 := dto.Metric{}
-
-	proMetrics.RecordLegacyImps(labels[0], 1)
-	proMetrics.RecordLegacyImps(labels[1], 5)
-	proMetrics.RecordLegacyImps(labels[0], 2)
-	proMetrics.RecordLegacyImps(labels[2], 2)
-	proMetrics.RecordLegacyImps(labels[0], 7)
-	proMetrics.RecordLegacyImps(labels[2], 1)
-	proMetrics.RecordLegacyImps(labels[4], 1)
-
-	proMetrics.legacyImps.With(resolveLabels(labels[0])).Write(&metrics0)
-	proMetrics.legacyImps.With(resolveLabels(labels[1])).Write(&metrics1)
-	proMetrics.legacyImps.With(resolveLabels(labels[2])).Write(&metrics2)
-	proMetrics.legacyImps.With(resolveLabels(labels[3])).Write(&metrics3)
-	proMetrics.legacyImps.With(resolveLabels(labels[4])).Write(&metrics4)
-
-	assertCounterValue(t, "legacyImps_requested[0]", &metrics0, 10)
-	assertCounterValue(t, "legacyImps_requested[1]", &metrics1, 5)
-	assertCounterValue(t, "legacyImps_requested[2]", &metrics2, 3)
-	assertCounterValue(t, "legacyImps_requested[3]", &metrics3, 0)
-	assertCounterValue(t, "legacyImps_requested[4]", &metrics4, 1)
+func TestRequestMetric(t *testing.T) {
+	m := createMetricsForTesting()
+	requestType := pbsmetrics.ReqTypeORTB2Web
+	requestStatus := pbsmetrics.RequestStatusBlacklisted
+
+	m.RecordRequest(pbsmetrics.Labels{
+		RType:         requestType,
+		RequestStatus: requestStatus,
+	})
+
+	expectedCount := float64(1)
+	assertCounterVecValue(t, "", "requests", m.requests,
+		expectedCount,
+		prometheus.Labels{
+			requestTypeLabel:   string(requestType),
+			requestStatusLabel: string(requestStatus),
+		})
 }
 
-func TestImpMetrics(t *testing.T) {
-	proMetrics := newTestMetricsEngine()
-
-	metrics0 := dto.Metric{}
-	metrics1 := dto.Metric{}
-	metrics2 := dto.Metric{}
-	metrics3 := dto.Metric{}
-
-	proMetrics.RecordImps(impTypeLabels[0])
-	proMetrics.RecordImps(impTypeLabels[1])
-	proMetrics.RecordImps(impTypeLabels[0])
-	proMetrics.RecordImps(impTypeLabels[2])
-	proMetrics.RecordImps(impTypeLabels[0])
-	proMetrics.RecordImps(impTypeLabels[2])
-	proMetrics.RecordImps(impTypeLabels[3])
-
-	proMetrics.imps.With(resolveImpLabels(impTypeLabels[0])).Write(&metrics0)
-	proMetrics.imps.With(resolveImpLabels(impTypeLabels[1])).Write(&metrics1)
-	proMetrics.imps.With(resolveImpLabels(impTypeLabels[2])).Write(&metrics2)
-	proMetrics.imps.With(resolveImpLabels(impTypeLabels[3])).Write(&metrics3)
-
-	assertCounterValue(t, "imp_metrics[0]", &metrics0, 3)
-	assertCounterValue(t, "imp_metrics[1]", &metrics1, 1)
-	assertCounterValue(t, "imp_metrics[2]", &metrics2, 2)
-	assertCounterValue(t, "imp_metrics[3]", &metrics3, 1)
+func TestRequestMetricWithoutCookie(t *testing.T) {
+	requestType := pbsmetrics.ReqTypeORTB2Web
+	performTest := func(m *Metrics, cookieFlag pbsmetrics.CookieFlag) {
+		m.RecordRequest(pbsmetrics.Labels{
+			RType:         requestType,
+			RequestStatus: pbsmetrics.RequestStatusBlacklisted,
+			CookieFlag:    cookieFlag,
+		})
+	}
+
+	testCases := []struct {
+		description   string
+		testCase      func(m *Metrics)
+		cookieFlag    pbsmetrics.CookieFlag
+		expectedCount float64
+	}{
+		{
+			description: "Yes",
+			testCase: func(m *Metrics) {
+				performTest(m, pbsmetrics.CookieFlagYes)
+			},
+			expectedCount: 0,
+		},
+		{
+			description: "No",
+			testCase: func(m *Metrics) {
+				performTest(m, pbsmetrics.CookieFlagNo)
+			},
+			expectedCount: 1,
+		},
+		{
+			description: "Unknown",
+			testCase: func(m *Metrics) {
+				performTest(m, pbsmetrics.CookieFlagUnknown)
+			},
+			expectedCount: 0,
+		},
+	}
+
+	for _, test := range testCases {
+		m := createMetricsForTesting()
+
+		test.testCase(m)
+
+		assertCounterVecValue(t, test.description, "requestsWithoutCookie", m.requestsWithoutCookie,
+			test.expectedCount,
+			prometheus.Labels{
+				requestTypeLabel: string(requestType),
+			})
+	}
 }
 
-func TestRecordImps(t *testing.T) {
-	var sampleEngine *Metrics = newTestMetricsEngine()
-	var sampleMetrics dto.Metric = dto.Metric{}
-	var samplePBSImpLabels pbsmetrics.ImpLabels = pbsmetrics.ImpLabels{
-		BannerImps: false,
-		VideoImps:  false,
-		AudioImps:  false,
-		NativeImps: true,
+func TestAccountMetric(t *testing.T) {
+	knownPubID := "knownPublisher"
+	performTest := func(m *Metrics, pubID string) {
+		m.RecordRequest(pbsmetrics.Labels{
+			RType:         pbsmetrics.ReqTypeORTB2Web,
+			RequestStatus: pbsmetrics.RequestStatusBlacklisted,
+			PubID:         pubID,
+		})
 	}
-	var samplePrometheusMetrics prometheus.Labels = resolveImpLabels(samplePBSImpLabels)
-
-	sampleEngine.RecordImps(samplePBSImpLabels)
-	sampleEngine.imps.With(samplePrometheusMetrics).Write(&sampleMetrics)
-
-	for _, metricLabel := range sampleMetrics.Label {
-		var impType string = *metricLabel.Name
-		switch impType {
-		case "banner":
-			assert.Equal(t, *metricLabel.Value, "no", "'banner")
-		case "video":
-			assert.Equal(t, *metricLabel.Value, "no", "'video' should equal 'no'")
-		case "audio":
-			assert.Equal(t, *metricLabel.Value, "no", "'audio' should equal 'no'")
-		case "native":
-			assert.Equal(t, *metricLabel.Value, "yes", "'native' should equal 'yes'")
-		default:
-			fmt.Printf("'%s' is not recognized as a label \n", impType)
-		}
+
+	testCases := []struct {
+		description   string
+		testCase      func(m *Metrics)
+		expectedCount float64
+	}{
+		{
+			description: "Known",
+			testCase: func(m *Metrics) {
+				performTest(m, knownPubID)
+			},
+			expectedCount: 1,
+		},
+		{
+			description: "Unknown",
+			testCase: func(m *Metrics) {
+				performTest(m, pbsmetrics.PublisherUnknown)
+			},
+			expectedCount: 0,
+		},
 	}
 
-	assertCounterValue(t, "imps_types_banner_count", &sampleMetrics, 1)
+	for _, test := range testCases {
+		m := createMetricsForTesting()
+
+		test.testCase(m)
+
+		assertCounterVecValue(t, test.description, "accountRequests", m.accountRequests,
+			test.expectedCount,
+			prometheus.Labels{
+				accountLabel: knownPubID,
+			})
+	}
 }
 
-func TestTimerMetrics(t *testing.T) {
-	proMetrics := newTestMetricsEngine()
-
-	metrics0 := dto.Metric{}
-	metrics1 := dto.Metric{}
-	metrics2 := dto.Metric{}
-	metrics3 := dto.Metric{}
-	metrics4 := dto.Metric{}
-
-	proMetrics.RecordRequestTime(labels[0], 120*time.Millisecond)
-	proMetrics.RecordRequestTime(labels[1], 85*time.Millisecond)
-	proMetrics.RecordRequestTime(labels[0], 220*time.Millisecond)
-	proMetrics.RecordRequestTime(labels[2], 250*time.Millisecond)
-	proMetrics.RecordRequestTime(labels[0], 90*time.Millisecond)
-	proMetrics.RecordRequestTime(labels[2], 180*time.Millisecond)
-	proMetrics.RecordRequestTime(labels[4], 100*time.Millisecond)
-
-	// HistogramVec.With() now returns an observer interface, with no Write() method. The interface
-	// returned is still a reference to a Histogram, so this hack works. It may break in the future
-	// if the Prometheus team changes the observer to actually be its own thing.
-	proMetrics.reqTimer.With(resolveLabels(labels[0])).(prometheus.Histogram).Write(&metrics0)
-	proMetrics.reqTimer.With(resolveLabels(labels[1])).(prometheus.Histogram).Write(&metrics1)
-	proMetrics.reqTimer.With(resolveLabels(labels[2])).(prometheus.Histogram).Write(&metrics2)
-	proMetrics.reqTimer.With(resolveLabels(labels[3])).(prometheus.Histogram).Write(&metrics3)
-	proMetrics.reqTimer.With(resolveLabels(labels[4])).(prometheus.Histogram).Write(&metrics4)
-
-	assertHistogramValue(t, "request_time[0]", &metrics0, 3)
-	assertHistogramValue(t, "request_time[1]", &metrics1, 1)
-	assertHistogramValue(t, "request_time[2]", &metrics2, 2)
-	assertHistogramValue(t, "request_time[3]", &metrics3, 0)
-	assertHistogramValue(t, "request_time[4]", &metrics4, 1)
+func TestImpressionsMetric(t *testing.T) {
+	performTest := func(m *Metrics, isBanner, isVideo, isAudio, isNative bool) {
+		m.RecordImps(pbsmetrics.ImpLabels{
+			BannerImps: isBanner,
+			VideoImps:  isVideo,
+			AudioImps:  isAudio,
+			NativeImps: isNative,
+		})
+	}
+
+	testCases := []struct {
+		description         string
+		testCase            func(m *Metrics)
+		expectedBannerCount float64
+		expectedVideoCount  float64
+		expectedAudioCount  float64
+		expectedNativeCount float64
+	}{
+		{
+			description: "Banner Only",
+			testCase: func(m *Metrics) {
+				performTest(m, true, false, false, false)
+			},
+			expectedBannerCount: 1,
+			expectedVideoCount:  0,
+			expectedAudioCount:  0,
+			expectedNativeCount: 0,
+		},
+		{
+			description: "Video Only",
+			testCase: func(m *Metrics) {
+				performTest(m, false, true, false, false)
+			},
+			expectedBannerCount: 0,
+			expectedVideoCount:  1,
+			expectedAudioCount:  0,
+			expectedNativeCount: 0,
+		},
+		{
+			description: "Audio Only",
+			testCase: func(m *Metrics) {
+				performTest(m, false, false, true, false)
+			},
+			expectedBannerCount: 0,
+			expectedVideoCount:  0,
+			expectedAudioCount:  1,
+			expectedNativeCount: 0,
+		},
+		{
+			description: "Native Only",
+			testCase: func(m *Metrics) {
+				performTest(m, false, false, false, true)
+			},
+			expectedBannerCount: 0,
+			expectedVideoCount:  0,
+			expectedAudioCount:  0,
+			expectedNativeCount: 1,
+		},
+		{
+			description: "Multiple Types",
+			testCase: func(m *Metrics) {
+				performTest(m, true, false, false, true)
+			},
+			expectedBannerCount: 1,
+			expectedVideoCount:  0,
+			expectedAudioCount:  0,
+			expectedNativeCount: 1,
+		},
+	}
+
+	for _, test := range testCases {
+		m := createMetricsForTesting()
+
+		test.testCase(m)
+
+		var bannerCount float64
+		var videoCount float64
+		var audioCount float64
+		var nativeCount float64
+		processMetrics(m.impressions, func(m dto.Metric) {
+			value := m.GetCounter().GetValue()
+			for _, label := range m.GetLabel() {
+				if label.GetValue() == "true" {
+					switch label.GetName() {
+					case isBannerLabel:
+						bannerCount += value
+					case isVideoLabel:
+						videoCount += value
+					case isAudioLabel:
+						audioCount += value
+					case isNativeLabel:
+						nativeCount += value
+					}
+				}
+			}
+		})
+		assert.Equal(t, test.expectedBannerCount, bannerCount, test.description+":banner")
+		assert.Equal(t, test.expectedVideoCount, videoCount, test.description+":video")
+		assert.Equal(t, test.expectedAudioCount, audioCount, test.description+":audio")
+		assert.Equal(t, test.expectedNativeCount, nativeCount, test.description+":native")
+	}
 }
 
-func TestAdapterRequestMetrics(t *testing.T) {
-	proMetrics := newTestMetricsEngine()
-
-	metrics0 := dto.Metric{}
-	metrics1 := dto.Metric{}
-	metrics2 := dto.Metric{}
-	metrics3 := dto.Metric{}
-	metrics4 := dto.Metric{}
-	emetrics0 := dto.Metric{}
-	emetrics1a := dto.Metric{}
-	emetrics1b := dto.Metric{}
-	emetrics2 := dto.Metric{}
-	emetrics3 := dto.Metric{}
-	emetrics4 := dto.Metric{}
-
-	proMetrics.RecordAdapterRequest(adaptLabels[0])
-	proMetrics.RecordAdapterRequest(adaptLabels[1])
-	proMetrics.RecordAdapterRequest(adaptLabels[0])
-	proMetrics.RecordAdapterRequest(adaptLabels[2])
-	proMetrics.RecordAdapterRequest(adaptLabels[0])
-	proMetrics.RecordAdapterRequest(adaptLabels[2])
-	proMetrics.RecordAdapterRequest(adaptLabels[4])
-
-	proMetrics.adaptRequests.With(resolveAdapterLabels(adaptLabels[0])).Write(&metrics0)
-	proMetrics.adaptRequests.With(resolveAdapterLabels(adaptLabels[1])).Write(&metrics1)
-	proMetrics.adaptRequests.With(resolveAdapterLabels(adaptLabels[2])).Write(&metrics2)
-	proMetrics.adaptRequests.With(resolveAdapterLabels(adaptLabels[3])).Write(&metrics3)
-	proMetrics.adaptRequests.With(resolveAdapterLabels(adaptLabels[4])).Write(&metrics4)
-
-	proMetrics.adaptErrors.With(resolveAdapterErrorLabels(adaptLabels[0], string(pbsmetrics.AdapterErrorBadInput))).Write(&emetrics0)
-	proMetrics.adaptErrors.With(resolveAdapterErrorLabels(adaptLabels[1], string(pbsmetrics.AdapterErrorBadServerResponse))).Write(&emetrics1a)
-	proMetrics.adaptErrors.With(resolveAdapterErrorLabels(adaptLabels[1], string(pbsmetrics.AdapterErrorUnknown))).Write(&emetrics1b)
-	proMetrics.adaptErrors.With(resolveAdapterErrorLabels(adaptLabels[2], string(pbsmetrics.AdapterErrorBadInput))).Write(&emetrics2)
-	proMetrics.adaptErrors.With(resolveAdapterErrorLabels(adaptLabels[3], string(pbsmetrics.AdapterErrorBadInput))).Write(&emetrics3)
-	proMetrics.adaptErrors.With(resolveAdapterErrorLabels(adaptLabels[4], string(pbsmetrics.AdapterErrorBadInput))).Write(&emetrics4)
-
-	assertCounterValue(t, "adapter_requests[0]", &metrics0, 3)
-	assertCounterValue(t, "adapter_requests[1]", &metrics1, 1)
-	assertCounterValue(t, "adapter_requests[2]", &metrics2, 2)
-	assertCounterValue(t, "adapter_requests[3]", &metrics3, 0)
-	assertCounterValue(t, "adapter_requests[4]", &metrics4, 1)
-
-	assertCounterValue(t, "adapter_errors[0]", &emetrics0, 0)
-	assertCounterValue(t, "adapter_errors[1]a", &emetrics1a, 1)
-	assertCounterValue(t, "adapter_errors[1]b", &emetrics1b, 1)
-	assertCounterValue(t, "adapter_errors[2]", &emetrics2, 0)
-	assertCounterValue(t, "adapter_errors[3]", &emetrics3, 0)
-	assertCounterValue(t, "adapter_errors[4]", &emetrics4, 0)
+func TestLegacyImpressionsMetric(t *testing.T) {
+	m := createMetricsForTesting()
+
+	m.RecordLegacyImps(pbsmetrics.Labels{}, 42)
+
+	expectedCount := float64(42)
+	assertCounterValue(t, "", "impressionsLegacy", m.impressionsLegacy,
+		expectedCount)
 }
 
-func TestAdapterBidsMetrics(t *testing.T) {
-	proMetrics := newTestMetricsEngine()
-
-	metrics0 := dto.Metric{}
-	metrics1 := dto.Metric{}
-	metrics2 := dto.Metric{}
-	metrics3 := dto.Metric{}
-	metrics4 := dto.Metric{}
-
-	proMetrics.RecordAdapterBidReceived(adaptLabels[0], openrtb_ext.BidTypeBanner, true)
-	proMetrics.RecordAdapterBidReceived(adaptLabels[1], openrtb_ext.BidTypeBanner, false)
-	proMetrics.RecordAdapterBidReceived(adaptLabels[0], openrtb_ext.BidTypeBanner, true)
-	proMetrics.RecordAdapterBidReceived(adaptLabels[2], openrtb_ext.BidTypeVideo, true)
-	proMetrics.RecordAdapterBidReceived(adaptLabels[0], openrtb_ext.BidTypeBanner, true)
-	proMetrics.RecordAdapterBidReceived(adaptLabels[2], openrtb_ext.BidTypeVideo, true)
-	proMetrics.RecordAdapterBidReceived(adaptLabels[4], openrtb_ext.BidTypeVideo, true)
-
-	proMetrics.adaptBids.With(resolveBidLabels(adaptLabels[0], openrtb_ext.BidTypeBanner, true)).Write(&metrics0)
-	proMetrics.adaptBids.With(resolveBidLabels(adaptLabels[1], openrtb_ext.BidTypeBanner, false)).Write(&metrics1)
-	proMetrics.adaptBids.With(resolveBidLabels(adaptLabels[2], openrtb_ext.BidTypeVideo, true)).Write(&metrics2)
-	proMetrics.adaptBids.With(resolveBidLabels(adaptLabels[3], openrtb_ext.BidTypeNative, false)).Write(&metrics3)
-	proMetrics.adaptBids.With(resolveBidLabels(adaptLabels[4], openrtb_ext.BidTypeVideo, true)).Write(&metrics4)
-
-	assertCounterValue(t, "adapter_bids_received[0]", &metrics0, 3)
-	assertCounterValue(t, "adapter_bids_received[1]", &metrics1, 1)
-	assertCounterValue(t, "adapter_bids_received[2]", &metrics2, 2)
-	assertCounterValue(t, "adapter_bids_received[3]", &metrics3, 0)
-	assertCounterValue(t, "adapter_bids_received[4]", &metrics4, 1)
+func TestRequestTimeMetric(t *testing.T) {
+	requestType := pbsmetrics.ReqTypeORTB2Web
+	performTest := func(m *Metrics, requestStatus pbsmetrics.RequestStatus, timeInMs float64) {
+		m.RecordRequestTime(pbsmetrics.Labels{
+			RType:         requestType,
+			RequestStatus: requestStatus,
+		}, time.Duration(timeInMs)*time.Millisecond)
+	}
+
+	testCases := []struct {
+		description   string
+		testCase      func(m *Metrics)
+		expectedCount uint64
+		expectedSum   float64
+	}{
+		{
+			description: "Success",
+			testCase: func(m *Metrics) {
+				performTest(m, pbsmetrics.RequestStatusOK, 500)
+			},
+			expectedCount: 1,
+			expectedSum:   0.5,
+		},
+		{
+			description: "Error",
+			testCase: func(m *Metrics) {
+				performTest(m, pbsmetrics.RequestStatusErr, 500)
+			},
+			expectedCount: 0,
+			expectedSum:   0,
+		},
+	}
+
+	for _, test := range testCases {
+		m := createMetricsForTesting()
+
+		test.testCase(m)
+
+		result := getHistogramFromHistogramVec(m.requestsTimer, requestTypeLabel, string(requestType))
+		assertHistogram(t, test.description, result, test.expectedCount, test.expectedSum)
+	}
 }
 
-func TestAdapterPriceMetrics(t *testing.T) {
-	proMetrics := newTestMetricsEngine()
-	metrics0 := dto.Metric{}
-	metrics1 := dto.Metric{}
-	metrics2 := dto.Metric{}
-	metrics3 := dto.Metric{}
-	metrics4 := dto.Metric{}
-
-	proMetrics.RecordAdapterPrice(adaptLabels[0], 0.12)
-	proMetrics.RecordAdapterPrice(adaptLabels[1], 2.35)
-	proMetrics.RecordAdapterPrice(adaptLabels[0], 17.65)
-	proMetrics.RecordAdapterPrice(adaptLabels[2], 3.23333)
-	proMetrics.RecordAdapterPrice(adaptLabels[0], 6.564)
-	proMetrics.RecordAdapterPrice(adaptLabels[2], 0.03)
-	proMetrics.RecordAdapterPrice(adaptLabels[4], 1.23)
-
-	// HistogramVec.With() now returns an observer interface, with no Write() method. The interface
-	// returned is still a reference to a Histogram, so this hack works. It may break in the future
-	// if the Prometheus team changes the observer to actually be its own thing.
-	proMetrics.adaptPrices.With(resolveAdapterLabels(adaptLabels[0])).(prometheus.Histogram).Write(&metrics0)
-	proMetrics.adaptPrices.With(resolveAdapterLabels(adaptLabels[1])).(prometheus.Histogram).Write(&metrics1)
-	proMetrics.adaptPrices.With(resolveAdapterLabels(adaptLabels[2])).(prometheus.Histogram).Write(&metrics2)
-	proMetrics.adaptPrices.With(resolveAdapterLabels(adaptLabels[3])).(prometheus.Histogram).Write(&metrics3)
-	proMetrics.adaptPrices.With(resolveAdapterLabels(adaptLabels[4])).(prometheus.Histogram).Write(&metrics4)
-
-	assertHistogramValue(t, "adapter_prices[0]", &metrics0, 3)
-	assertHistogramValue(t, "adapter_prices[1]", &metrics1, 1)
-	assertHistogramValue(t, "adapter_prices[2]", &metrics2, 2)
-	assertHistogramValue(t, "adapter_prices[3]", &metrics3, 0)
-	assertHistogramValue(t, "adapter_prices[4]", &metrics4, 1)
+func TestAdapterBidReceivedMetric(t *testing.T) {
+	adapterName := "anyName"
+	performTest := func(m *Metrics, hasAdm bool) {
+		labels := pbsmetrics.AdapterLabels{
+			Adapter: openrtb_ext.BidderName(adapterName),
+		}
+		bidType := openrtb_ext.BidTypeBanner
+		m.RecordAdapterBidReceived(labels, bidType, hasAdm)
+	}
+
+	testCases := []struct {
+		description       string
+		testCase          func(m *Metrics)
+		expectedAdmCount  float64
+		expectedNurlCount float64
+	}{
+		{
+			description: "AdM",
+			testCase: func(m *Metrics) {
+				performTest(m, true)
+			},
+			expectedAdmCount:  1,
+			expectedNurlCount: 0,
+		},
+		{
+			description: "Nurl",
+			testCase: func(m *Metrics) {
+				performTest(m, false)
+			},
+			expectedAdmCount:  0,
+			expectedNurlCount: 1,
+		},
+	}
 
+	for _, test := range testCases {
+		m := createMetricsForTesting()
+
+		test.testCase(m)
+
+		assertCounterVecValue(t, test.description, "adapterBids[adm]", m.adapterBids,
+			test.expectedAdmCount,
+			prometheus.Labels{
+				adapterLabel:        adapterName,
+				markupDeliveryLabel: markupDeliveryAdm,
+			})
+		assertCounterVecValue(t, test.description, "adapterBids[nurl]", m.adapterBids,
+			test.expectedNurlCount,
+			prometheus.Labels{
+				adapterLabel:        adapterName,
+				markupDeliveryLabel: markupDeliveryNurl,
+			})
+	}
 }
 
-func TestAdapterTimeMetrics(t *testing.T) {
-	proMetrics := newTestMetricsEngine()
-
-	metrics0 := dto.Metric{}
-	metrics1 := dto.Metric{}
-	metrics2 := dto.Metric{}
-	metrics3 := dto.Metric{}
-	metrics4 := dto.Metric{}
-
-	proMetrics.RecordAdapterTime(adaptLabels[0], 85*time.Millisecond)
-	proMetrics.RecordAdapterTime(adaptLabels[1], 235*time.Millisecond)
-	proMetrics.RecordAdapterTime(adaptLabels[0], 177*time.Millisecond)
-	proMetrics.RecordAdapterTime(adaptLabels[2], 323*time.Millisecond)
-	proMetrics.RecordAdapterTime(adaptLabels[0], 664*time.Millisecond)
-	proMetrics.RecordAdapterTime(adaptLabels[2], 33*time.Millisecond)
-	proMetrics.RecordAdapterTime(adaptLabels[4], 333*time.Millisecond)
-
-	// HistogramVec.With() now returns an observer interface, with no Write() method. The interface
-	// returned is still a reference to a Histogram, so this hack works. It may break in the future
-	// if the Prometheus team changes the observer to actually be its own thing.
-	proMetrics.adaptTimer.With(resolveAdapterLabels(adaptLabels[0])).(prometheus.Histogram).Write(&metrics0)
-	proMetrics.adaptTimer.With(resolveAdapterLabels(adaptLabels[1])).(prometheus.Histogram).Write(&metrics1)
-	proMetrics.adaptTimer.With(resolveAdapterLabels(adaptLabels[2])).(prometheus.Histogram).Write(&metrics2)
-	proMetrics.adaptTimer.With(resolveAdapterLabels(adaptLabels[3])).(prometheus.Histogram).Write(&metrics3)
-	proMetrics.adaptTimer.With(resolveAdapterLabels(adaptLabels[4])).(prometheus.Histogram).Write(&metrics4)
-
-	assertHistogramValue(t, "adapter_time[0]", &metrics0, 3)
-	assertHistogramValue(t, "adapter_time[1]", &metrics1, 1)
-	assertHistogramValue(t, "adapter_time[2]", &metrics2, 2)
-	assertHistogramValue(t, "adapter_time[3]", &metrics3, 0)
-	assertHistogramValue(t, "adapter_time[4]", &metrics4, 1)
+func TestRecordAdapterPriceMetric(t *testing.T) {
+	m := createMetricsForTesting()
+	adapterName := "anyName"
+	cpm := float64(42)
+
+	m.RecordAdapterPrice(pbsmetrics.AdapterLabels{
+		Adapter: openrtb_ext.BidderName(adapterName),
+	}, cpm)
 
+	expectedCount := uint64(1)
+	expectedSum := cpm
+	result := getHistogramFromHistogramVec(m.adapterPrices, adapterLabel, adapterName)
+	assertHistogram(t, "adapterPrices", result, expectedCount, expectedSum)
 }
 
-func TestRecordStoredReqCacheResult(t *testing.T) {
-	proMetrics := newTestMetricsEngine()
+func TestAdapterRequestMetrics(t *testing.T) {
+	adapterName := "anyName"
+	performTest := func(m *Metrics, cookieFlag pbsmetrics.CookieFlag, adapterBids pbsmetrics.AdapterBid) {
+		labels := pbsmetrics.AdapterLabels{
+			Adapter:     openrtb_ext.BidderName(adapterName),
+			CookieFlag:  cookieFlag,
+			AdapterBids: adapterBids,
+		}
+		m.RecordAdapterRequest(labels)
+	}
 
-	metricCacheHit := dto.Metric{}
-	metricCacheMiss := dto.Metric{}
+	testCases := []struct {
+		description                string
+		testCase                   func(m *Metrics)
+		expectedCount              float64
+		expectedCookieNoCount      float64
+		expectedCookieYesCount     float64
+		expectedCookieUnknownCount float64
+		expectedHasBidsCount       float64
+	}{
+		{
+			description: "No Cookie & No Bids",
+			testCase: func(m *Metrics) {
+				performTest(m, pbsmetrics.CookieFlagNo, pbsmetrics.AdapterBidNone)
+			},
+			expectedCount:              1,
+			expectedCookieNoCount:      1,
+			expectedCookieYesCount:     0,
+			expectedCookieUnknownCount: 0,
+			expectedHasBidsCount:       0,
+		},
+		{
+			description: "Unknown Cookie & No Bids",
+			testCase: func(m *Metrics) {
+				performTest(m, pbsmetrics.CookieFlagUnknown, pbsmetrics.AdapterBidNone)
+			},
+			expectedCount:              1,
+			expectedCookieNoCount:      0,
+			expectedCookieYesCount:     0,
+			expectedCookieUnknownCount: 1,
+			expectedHasBidsCount:       0,
+		},
+		{
+			description: "Has Cookie & No Bids",
+			testCase: func(m *Metrics) {
+				performTest(m, pbsmetrics.CookieFlagYes, pbsmetrics.AdapterBidNone)
+			},
+			expectedCount:              1,
+			expectedCookieNoCount:      0,
+			expectedCookieYesCount:     1,
+			expectedCookieUnknownCount: 0,
+			expectedHasBidsCount:       0,
+		},
+		{
+			description: "No Cookie & Bids Present",
+			testCase: func(m *Metrics) {
+				performTest(m, pbsmetrics.CookieFlagNo, pbsmetrics.AdapterBidPresent)
+			},
+			expectedCount:              1,
+			expectedCookieNoCount:      1,
+			expectedCookieYesCount:     0,
+			expectedCookieUnknownCount: 0,
+			expectedHasBidsCount:       1,
+		},
+		{
+			description: "Unknown Cookie & Bids Present",
+			testCase: func(m *Metrics) {
+				performTest(m, pbsmetrics.CookieFlagUnknown, pbsmetrics.AdapterBidPresent)
+			},
+			expectedCount:              1,
+			expectedCookieNoCount:      0,
+			expectedCookieYesCount:     0,
+			expectedCookieUnknownCount: 1,
+			expectedHasBidsCount:       1,
+		},
+		{
+			description: "Has Cookie & Bids Present",
+			testCase: func(m *Metrics) {
+				performTest(m, pbsmetrics.CookieFlagYes, pbsmetrics.AdapterBidPresent)
+			},
+			expectedCount:              1,
+			expectedCookieNoCount:      0,
+			expectedCookieYesCount:     1,
+			expectedCookieUnknownCount: 0,
+			expectedHasBidsCount:       1,
+		},
+	}
 
-	proMetrics.RecordStoredReqCacheResult(pbsmetrics.CacheHit, 2)
-	proMetrics.RecordStoredReqCacheResult(pbsmetrics.CacheHit, 0)
-	proMetrics.RecordStoredReqCacheResult(pbsmetrics.CacheMiss, 1)
+	for _, test := range testCases {
+		m := createMetricsForTesting()
+
+		test.testCase(m)
+
+		var totalCount float64
+		var totalCookieNoCount float64
+		var totalCookieYesCount float64
+		var totalCookieUnknowmCount float64
+		var totalHasBidsCount float64
+		processMetrics(m.adapterRequests, func(m dto.Metric) {
+			isMetricForAdapter := false
+			for _, label := range m.GetLabel() {
+				if label.GetName() == adapterLabel && label.GetValue() == adapterName {
+					isMetricForAdapter = true
+				}
+			}
+
+			if isMetricForAdapter {
+				value := m.GetCounter().GetValue()
+				totalCount += value
+				for _, label := range m.GetLabel() {
+
+					if label.GetName() == hasBidsLabel && label.GetValue() == "true" {
+						totalHasBidsCount += value
+					}
+
+					if label.GetName() == cookieLabel {
+						switch label.GetValue() {
+						case string(pbsmetrics.CookieFlagNo):
+							totalCookieNoCount += value
+						case string(pbsmetrics.CookieFlagYes):
+							totalCookieYesCount += value
+						case string(pbsmetrics.CookieFlagUnknown):
+							totalCookieUnknowmCount += value
+						}
+					}
+				}
+			}
+		})
+		assert.Equal(t, test.expectedCount, totalCount, test.description+":total")
+		assert.Equal(t, test.expectedCookieNoCount, totalCookieNoCount, test.description+":cookie=no")
+		assert.Equal(t, test.expectedCookieYesCount, totalCookieYesCount, test.description+":cookie=yes")
+		assert.Equal(t, test.expectedCookieUnknownCount, totalCookieUnknowmCount, test.description+":cookie=unknown")
+		assert.Equal(t, test.expectedHasBidsCount, totalHasBidsCount, test.description+":hasBids")
+	}
+}
 
-	proMetrics.storedReqCacheResult.WithLabelValues(string(pbsmetrics.CacheHit)).Write(&metricCacheHit)
-	proMetrics.storedReqCacheResult.WithLabelValues(string(pbsmetrics.CacheMiss)).Write(&metricCacheMiss)
+func TestAdapterRequestErrorMetrics(t *testing.T) {
+	adapterName := "anyName"
+	performTest := func(m *Metrics, adapterErrors map[pbsmetrics.AdapterError]struct{}) {
+		labels := pbsmetrics.AdapterLabels{
+			Adapter:       openrtb_ext.BidderName(adapterName),
+			AdapterErrors: adapterErrors,
+			CookieFlag:    pbsmetrics.CookieFlagUnknown,
+			AdapterBids:   pbsmetrics.AdapterBidPresent,
+		}
+		m.RecordAdapterRequest(labels)
+	}
+
+	testCases := []struct {
+		description                 string
+		testCase                    func(m *Metrics)
+		expectedErrorsCount         float64
+		expectedBadInputErrorsCount float64
+	}{
+		{
+			description: "No Errors",
+			testCase: func(m *Metrics) {
+				performTest(m, nil)
+			},
+			expectedErrorsCount:         0,
+			expectedBadInputErrorsCount: 0,
+		},
+		{
+			description: "Bad Input Error",
+			testCase: func(m *Metrics) {
+				performTest(m, map[pbsmetrics.AdapterError]struct{}{
+					pbsmetrics.AdapterErrorBadInput: {},
+				})
+			},
+			expectedErrorsCount:         1,
+			expectedBadInputErrorsCount: 1,
+		},
+		{
+			description: "Other Error",
+			testCase: func(m *Metrics) {
+				performTest(m, map[pbsmetrics.AdapterError]struct{}{
+					pbsmetrics.AdapterErrorBadServerResponse: {},
+				})
+			},
+			expectedErrorsCount:         1,
+			expectedBadInputErrorsCount: 0,
+		},
+	}
 
-	assertCounterValue(t, "stored_request_cache_performance[hit]", &metricCacheHit, 2)
-	assertCounterValue(t, "stored_request_cache_performance[miss]", &metricCacheMiss, 1)
+	for _, test := range testCases {
+		m := createMetricsForTesting()
+
+		test.testCase(m)
+
+		var errorsCount float64
+		var badInputErrorsCount float64
+		processMetrics(m.adapterErrors, func(m dto.Metric) {
+			isMetricForAdapter := false
+			for _, label := range m.GetLabel() {
+				if label.GetName() == adapterLabel && label.GetValue() == adapterName {
+					isMetricForAdapter = true
+				}
+			}
+
+			if isMetricForAdapter {
+				value := m.GetCounter().GetValue()
+				errorsCount += value
+				for _, label := range m.GetLabel() {
+					if label.GetName() == adapterErrorLabel && label.GetValue() == string(pbsmetrics.AdapterErrorBadInput) {
+						badInputErrorsCount += value
+					}
+				}
+			}
+		})
+		assert.Equal(t, test.expectedErrorsCount, errorsCount, test.description+":errors")
+		assert.Equal(t, test.expectedBadInputErrorsCount, badInputErrorsCount, test.description+":badInputErrors")
+	}
 }
 
-func TestRecordStoredImpCacheResult(t *testing.T) {
-	proMetrics := newTestMetricsEngine()
+func TestAdapterTimeMetric(t *testing.T) {
+	adapterName := "anyName"
+	performTest := func(m *Metrics, timeInMs float64, adapterErrors map[pbsmetrics.AdapterError]struct{}) {
+		m.RecordAdapterTime(pbsmetrics.AdapterLabels{
+			Adapter:       openrtb_ext.BidderName(adapterName),
+			AdapterErrors: adapterErrors,
+		}, time.Duration(timeInMs)*time.Millisecond)
+	}
 
-	metricCacheHit := dto.Metric{}
-	metricCacheMiss := dto.Metric{}
+	testCases := []struct {
+		description   string
+		testCase      func(m *Metrics)
+		expectedCount uint64
+		expectedSum   float64
+	}{
+		{
+			description: "Success",
+			testCase: func(m *Metrics) {
+				performTest(m, 500, map[pbsmetrics.AdapterError]struct{}{})
+			},
+			expectedCount: 1,
+			expectedSum:   0.5,
+		},
+		{
+			description: "Error",
+			testCase: func(m *Metrics) {
+				performTest(m, 500, map[pbsmetrics.AdapterError]struct{}{
+					pbsmetrics.AdapterErrorTimeout: {},
+				})
+			},
+			expectedCount: 0,
+			expectedSum:   0,
+		},
+	}
 
-	proMetrics.RecordStoredImpCacheResult(pbsmetrics.CacheHit, 2)
-	proMetrics.RecordStoredImpCacheResult(pbsmetrics.CacheHit, 0)
-	proMetrics.RecordStoredImpCacheResult(pbsmetrics.CacheMiss, 1)
+	for _, test := range testCases {
+		m := createMetricsForTesting()
 
-	proMetrics.storedImpCacheResult.WithLabelValues(string(pbsmetrics.CacheHit)).Write(&metricCacheHit)
-	proMetrics.storedImpCacheResult.WithLabelValues(string(pbsmetrics.CacheMiss)).Write(&metricCacheMiss)
+		test.testCase(m)
 
-	assertCounterValue(t, "stored_imp_cache_performance[hit]", &metricCacheHit, 2)
-	assertCounterValue(t, "stored_imp_cache_performance[miss]", &metricCacheMiss, 1)
+		result := getHistogramFromHistogramVec(m.adapterRequestsTimer, adapterLabel, adapterName)
+		assertHistogram(t, test.description, result, test.expectedCount, test.expectedSum)
+	}
 }
 
-func TestCookieMetrics(t *testing.T) {
-	proMetrics := newTestMetricsEngine()
+func TestAdapterCookieSyncMetric(t *testing.T) {
+	m := createMetricsForTesting()
+	adapterName := "anyName"
+	privacyBlocked := true
 
-	metrics0 := dto.Metric{}
+	m.RecordAdapterCookieSync(openrtb_ext.BidderName(adapterName), privacyBlocked)
 
-	proMetrics.RecordCookieSync(labels[0])
-	proMetrics.RecordCookieSync(labels[1])
-	proMetrics.RecordCookieSync(labels[0])
-	proMetrics.RecordCookieSync(labels[2])
-	proMetrics.RecordCookieSync(labels[0])
-	proMetrics.RecordCookieSync(labels[2])
-	proMetrics.RecordCookieSync(labels[4])
+	expectedCount := float64(1)
+	assertCounterVecValue(t, "", "adapterCookieSync", m.adapterCookieSync,
+		expectedCount,
+		prometheus.Labels{
+			adapterLabel:        adapterName,
+			privacyBlockedLabel: "true",
+		})
+}
 
-	proMetrics.cookieSync.Write(&metrics0)
+func TestUserIDSetMetric(t *testing.T) {
+	m := createMetricsForTesting()
+	adapterName := "anyName"
+	action := pbsmetrics.RequestActionSet
 
-	assertCounterValue(t, "cookie_sync_requests", &metrics0, 7)
-}
+	m.RecordUserIDSet(pbsmetrics.UserLabels{
+		Bidder: openrtb_ext.BidderName(adapterName),
+		Action: action,
+	})
 
-func TestUserMetrics(t *testing.T) {
-	proMetrics := newTestMetricsEngine()
-
-	metrics0 := dto.Metric{}
-	metrics1 := dto.Metric{}
-	metrics2 := dto.Metric{}
-	metrics3 := dto.Metric{}
-
-	proMetrics.RecordUserIDSet(userLabels[0])
-	proMetrics.RecordUserIDSet(userLabels[1])
-	proMetrics.RecordUserIDSet(userLabels[0])
-	proMetrics.RecordUserIDSet(userLabels[2])
-	proMetrics.RecordUserIDSet(userLabels[0])
-	proMetrics.RecordUserIDSet(userLabels[2])
-
-	proMetrics.userID.With(resolveUserSyncLabels(userLabels[0])).Write(&metrics0)
-	proMetrics.userID.With(resolveUserSyncLabels(userLabels[1])).Write(&metrics1)
-	proMetrics.userID.With(resolveUserSyncLabels(userLabels[2])).Write(&metrics2)
-	proMetrics.userID.With(resolveUserSyncLabels(userLabels[3])).Write(&metrics3)
-
-	assertCounterValue(t, "usersync[0]", &metrics0, 3)
-	assertCounterValue(t, "usersync[1]", &metrics1, 1)
-	assertCounterValue(t, "usersync[2]", &metrics2, 2)
-	assertCounterValue(t, "usersync[3]", &metrics3, 0)
+	expectedCount := float64(1)
+	assertCounterVecValue(t, "", "adapterUserSync", m.adapterUserSync,
+		expectedCount,
+		prometheus.Labels{
+			adapterLabel: adapterName,
+			actionLabel:  string(action),
+		})
 }
 
-func TestMetricsExist(t *testing.T) {
-	// Initialize the metrics engine -> register the metrics to prometheus
-	metrics := newTestMetricsEngine()
+func TestUserIDSetMetricWhenBidderEmpty(t *testing.T) {
+	m := createMetricsForTesting()
+	action := pbsmetrics.RequestActionErr
 
-	// Get at the underlying metrics object
-	proMetrics := metrics
+	m.RecordUserIDSet(pbsmetrics.UserLabels{
+		Bidder: openrtb_ext.BidderName(""),
+		Action: action,
+	})
 
-	if err := proMetrics.Registry.Register(prometheus.NewCounter(prometheus.CounterOpts{
-		Namespace: "prebid",
-		Subsystem: "server",
-		Name:      "active_connections",
-		Help:      "Current number of active (open) connections.",
-	})); err == nil {
-		t.Error("connCounter not registered")
-	}
+	expectedTotalCount := float64(0)
+	actualTotalCount := float64(0)
+	processMetrics(m.adapterUserSync, func(m dto.Metric) {
+		actualTotalCount += m.GetCounter().GetValue()
+	})
+	assert.Equal(t, expectedTotalCount, actualTotalCount, "total count")
 }
 
-func newTestMetricsEngine() *Metrics {
-	return NewMetrics(config.PrometheusMetrics{
-		Port:      8080,
-		Namespace: "prebid",
-		Subsystem: "server",
+func TestAdapterPanicMetric(t *testing.T) {
+	m := createMetricsForTesting()
+	adapterName := "anyName"
+
+	m.RecordAdapterPanic(pbsmetrics.AdapterLabels{
+		Adapter: openrtb_ext.BidderName(adapterName),
 	})
+
+	expectedCount := float64(1)
+	assertCounterVecValue(t, "", "adapterPanics", m.adapterPanics,
+		expectedCount,
+		prometheus.Labels{
+			adapterLabel: adapterName,
+		})
 }
 
-var labels = []pbsmetrics.Labels{
-	{
-		Source:        pbsmetrics.DemandWeb,
-		RType:         pbsmetrics.ReqTypeLegacy,
-		PubID:         "Pub1",
-		Browser:       pbsmetrics.BrowserOther,
-		CookieFlag:    pbsmetrics.CookieFlagYes,
-		RequestStatus: pbsmetrics.RequestStatusOK,
-	},
-	{
-		Source:        pbsmetrics.DemandWeb,
-		RType:         pbsmetrics.ReqTypeLegacy,
-		PubID:         "Pub1",
-		Browser:       pbsmetrics.BrowserSafari,
-		CookieFlag:    pbsmetrics.CookieFlagYes,
-		RequestStatus: pbsmetrics.RequestStatusOK,
-	},
-	{
-		Source:        pbsmetrics.DemandApp,
-		RType:         pbsmetrics.ReqTypeORTB2Web,
-		PubID:         "Pub2",
-		Browser:       pbsmetrics.BrowserOther,
-		CookieFlag:    pbsmetrics.CookieFlagNo,
-		RequestStatus: pbsmetrics.RequestStatusOK,
-	},
-	{
-		Source:        pbsmetrics.DemandUnknown,
-		RType:         pbsmetrics.ReqTypeORTB2App,
-		PubID:         "Pub3",
-		Browser:       pbsmetrics.BrowserOther,
-		CookieFlag:    pbsmetrics.CookieFlagUnknown,
-		RequestStatus: pbsmetrics.RequestStatusBadInput,
-	},
-	{
-		Source:        pbsmetrics.DemandWeb,
-		RType:         pbsmetrics.ReqTypeVideo,
-		PubID:         "Pub4",
-		Browser:       pbsmetrics.BrowserOther,
-		CookieFlag:    pbsmetrics.CookieFlagUnknown,
-		RequestStatus: pbsmetrics.RequestStatusOK,
-	},
+func TestStoredReqCacheResultMetric(t *testing.T) {
+	m := createMetricsForTesting()
+
+	hitCount := 42
+	missCount := 108
+	m.RecordStoredReqCacheResult(pbsmetrics.CacheHit, hitCount)
+	m.RecordStoredReqCacheResult(pbsmetrics.CacheMiss, missCount)
+
+	assertCounterVecValue(t, "", "storedRequestCacheResult:hit", m.storedRequestCacheResult,
+		float64(hitCount),
+		prometheus.Labels{
+			cacheResultLabel: string(pbsmetrics.CacheHit),
+		})
+	assertCounterVecValue(t, "", "storedRequestCacheResult:miss", m.storedRequestCacheResult,
+		float64(missCount),
+		prometheus.Labels{
+			cacheResultLabel: string(pbsmetrics.CacheMiss),
+		})
 }
 
-var impTypeLabels = []pbsmetrics.ImpLabels{
-	{
-		//impType: "banner",
-		BannerImps: true,
-		VideoImps:  false,
-		AudioImps:  false,
-		NativeImps: false,
-	},
-	{
-		//impType: "audio",
-		BannerImps: false,
-		VideoImps:  true,
-		AudioImps:  false,
-		NativeImps: false,
-	},
-	{
-		//impType: "video",
-		BannerImps: false,
-		VideoImps:  false,
-		AudioImps:  true,
-		NativeImps: false,
-	},
-	{
-		//impType: "native",
-		BannerImps: false,
-		VideoImps:  false,
-		AudioImps:  false,
-		NativeImps: true,
-	},
+func TestStoredImpCacheResultMetric(t *testing.T) {
+	m := createMetricsForTesting()
+
+	hitCount := 42
+	missCount := 108
+	m.RecordStoredImpCacheResult(pbsmetrics.CacheHit, hitCount)
+	m.RecordStoredImpCacheResult(pbsmetrics.CacheMiss, missCount)
+
+	assertCounterVecValue(t, "", "storedImpressionsCacheResult:hit", m.storedImpressionsCacheResult,
+		float64(hitCount),
+		prometheus.Labels{
+			cacheResultLabel: string(pbsmetrics.CacheHit),
+		})
+	assertCounterVecValue(t, "", "storedImpressionsCacheResult:miss", m.storedImpressionsCacheResult,
+		float64(missCount),
+		prometheus.Labels{
+			cacheResultLabel: string(pbsmetrics.CacheMiss),
+		})
 }
 
-var s struct{}
-
-var adaptLabels = []pbsmetrics.AdapterLabels{
-	{
-		Source:        pbsmetrics.DemandWeb,
-		RType:         pbsmetrics.ReqTypeLegacy,
-		Adapter:       openrtb_ext.BidderAppnexus,
-		PubID:         "Pub1",
-		Browser:       pbsmetrics.BrowserOther,
-		CookieFlag:    pbsmetrics.CookieFlagYes,
-		AdapterBids:   pbsmetrics.AdapterBidPresent,
-		AdapterErrors: map[pbsmetrics.AdapterError]struct{}{},
-	},
-	{
-		Source:      pbsmetrics.DemandWeb,
-		RType:       pbsmetrics.ReqTypeLegacy,
-		Adapter:     openrtb_ext.BidderEPlanning,
-		PubID:       "Pub1",
-		Browser:     pbsmetrics.BrowserSafari,
-		CookieFlag:  pbsmetrics.CookieFlagYes,
-		AdapterBids: pbsmetrics.AdapterBidPresent,
-		AdapterErrors: map[pbsmetrics.AdapterError]struct{}{
-			pbsmetrics.AdapterErrorBadServerResponse: s,
-			pbsmetrics.AdapterErrorUnknown:           s,
-		},
-	},
-	{
-		Source:        pbsmetrics.DemandApp,
-		RType:         pbsmetrics.ReqTypeORTB2Web,
-		Adapter:       openrtb_ext.BidderIx,
-		PubID:         "Pub2",
-		Browser:       pbsmetrics.BrowserOther,
-		CookieFlag:    pbsmetrics.CookieFlagNo,
-		AdapterBids:   pbsmetrics.AdapterBidPresent,
-		AdapterErrors: map[pbsmetrics.AdapterError]struct{}{},
-	},
-	{
-		Source:        pbsmetrics.DemandUnknown,
-		RType:         pbsmetrics.ReqTypeORTB2App,
-		Adapter:       openrtb_ext.BidderAppnexus,
-		PubID:         "Pub3",
-		Browser:       pbsmetrics.BrowserOther,
-		CookieFlag:    pbsmetrics.CookieFlagUnknown,
-		AdapterBids:   pbsmetrics.AdapterBidPresent,
-		AdapterErrors: map[pbsmetrics.AdapterError]struct{}{},
-	},
-	{
-		Source:        pbsmetrics.DemandWeb,
-		RType:         pbsmetrics.ReqTypeVideo,
-		Adapter:       openrtb_ext.BidderAppnexus,
-		PubID:         "Pub4",
-		Browser:       pbsmetrics.BrowserOther,
-		CookieFlag:    pbsmetrics.CookieFlagUnknown,
-		AdapterBids:   pbsmetrics.AdapterBidPresent,
-		AdapterErrors: map[pbsmetrics.AdapterError]struct{}{},
-	},
+func TestCookieMetric(t *testing.T) {
+	m := createMetricsForTesting()
+
+	m.RecordCookieSync()
+
+	expectedCount := float64(1)
+	assertCounterValue(t, "", "cookieSync", m.cookieSync,
+		expectedCount)
+}
+
+func TestPrebidCacheRequestTimeMetric(t *testing.T) {
+	m := createMetricsForTesting()
+
+	m.RecordPrebidCacheRequestTime(true, time.Duration(100)*time.Millisecond)
+	m.RecordPrebidCacheRequestTime(false, time.Duration(200)*time.Millisecond)
+
+	successExpectedCount := uint64(1)
+	successExpectedSum := float64(0.1)
+	successResult := getHistogramFromHistogramVec(m.prebidCacheWriteTimer, successLabel, "true")
+	assertHistogram(t, "Success", successResult, successExpectedCount, successExpectedSum)
+
+	errorExpectedCount := uint64(1)
+	errorExpectedSum := float64(0.2)
+	errorResult := getHistogramFromHistogramVec(m.prebidCacheWriteTimer, successLabel, "false")
+	assertHistogram(t, "Error", errorResult, errorExpectedCount, errorExpectedSum)
 }
 
-var userLabels = []pbsmetrics.UserLabels{
-	{
-		Action: pbsmetrics.RequestActionSet,
-		Bidder: openrtb_ext.BidderAppnexus,
-	},
-	{
-		Action: pbsmetrics.RequestActionGDPR,
-		Bidder: openrtb_ext.BidderAppnexus,
-	},
-	{
-		Action: pbsmetrics.RequestActionSet,
-		Bidder: openrtb_ext.BidderRubicon,
-	},
-	{
-		Action: pbsmetrics.RequestActionOptOut,
-		Bidder: openrtb_ext.BidderOpenx,
-	},
+func TestMetricAccumulationSpotCheck(t *testing.T) {
+	m := createMetricsForTesting()
+
+	m.RecordLegacyImps(pbsmetrics.Labels{}, 1)
+	m.RecordLegacyImps(pbsmetrics.Labels{}, 2)
+	m.RecordLegacyImps(pbsmetrics.Labels{}, 3)
+
+	expectedValue := float64(1 + 2 + 3)
+	assertCounterValue(t, "", "impressionsLegacy", m.impressionsLegacy,
+		expectedValue)
 }
 
-func assertMetricValue(t *testing.T, name string, m *dto.Metric, expected string) {
-	v := m.String()
-	if v != expected {
-		t.Errorf("Bad value for metric %s: expected=\"%s\", found=\"%s\"", name, expected, v)
-	}
+func assertCounterValue(t *testing.T, description, name string, counter prometheus.Counter, expected float64) {
+	m := dto.Metric{}
+	counter.Write(&m)
+	actual := *m.GetCounter().Value
+
+	assert.Equal(t, expected, actual, description)
 }
 
-func assertGaugeValue(t *testing.T, name string, m *dto.Metric, expected int) {
-	v, err := strconv.Atoi(gaugeValueRegexp.FindStringSubmatch(m.String())[1])
-	if err != nil {
-		t.Errorf("Could not extract the value for metric %s. (output was %s, error was %v)", name, m.String(), err)
-	}
-	if v != expected {
-		t.Errorf("Bad value for metric %s: expected=\"%d\", found=\"%d\"", name, expected, v)
-	}
+func assertCounterVecValue(t *testing.T, description, name string, counterVec *prometheus.CounterVec, expected float64, labels prometheus.Labels) {
+	counter := counterVec.With(labels)
+	assertCounterValue(t, description, name, counter, expected)
 }
 
-func assertCounterValue(t *testing.T, name string, m *dto.Metric, expected int) {
-	v, err := strconv.Atoi(counterValueRegexp.FindStringSubmatch(m.String())[1])
-	if err != nil {
-		t.Errorf("Could not extract the value for metric %s. (output was %s, error was %v)", name, m.String(), err)
-	}
-	if v != expected {
-		t.Errorf("Bad value for metric %s: expected=\"%d\", found=\"%d\"", name, expected, v)
-	}
+func getHistogramFromHistogramVec(histogram *prometheus.HistogramVec, labelKey, labelValue string) dto.Histogram {
+	var result dto.Histogram
+	processMetrics(histogram, func(m dto.Metric) {
+		for _, label := range m.GetLabel() {
+			if label.GetName() == labelKey && label.GetValue() == labelValue {
+				result = *m.GetHistogram()
+			}
+		}
+	})
+	return result
 }
 
-func assertHistogramValue(t *testing.T, name string, m *dto.Metric, expected int) {
-	v, err := strconv.Atoi(histogramValueRegexp.FindStringSubmatch(m.String())[1])
-	if err != nil {
-		t.Errorf("Could not extract the value for metric %s. (output was %s, error was %v)", name, m.String(), err)
-	}
-	if v != expected {
-		t.Errorf("Bad value for metric %s: expected=\"%d\", found=\"%d\"", name, expected, v)
+func processMetrics(collector prometheus.Collector, handler func(m dto.Metric)) {
+	collectorChan := make(chan prometheus.Metric)
+	go func() {
+		collector.Collect(collectorChan)
+		close(collectorChan)
+	}()
+
+	for metric := range collectorChan {
+		dtoMetric := dto.Metric{}
+		metric.Write(&dtoMetric)
+		handler(dtoMetric)
 	}
 }
+
+func assertHistogram(t *testing.T, name string, histogram dto.Histogram, expectedCount uint64, expectedSum float64) {
+	assert.Equal(t, expectedCount, histogram.GetSampleCount(), name+":count")
+	assert.Equal(t, expectedSum, histogram.GetSampleSum(), name+":sum")
+}
diff --git a/prebid_cache_client/client_test.go b/prebid_cache_client/client_test.go
index 535d503f..d3b5ee4b 100644
--- a/prebid_cache_client/client_test.go
+++ b/prebid_cache_client/client_test.go
@@ -48,10 +48,7 @@ func TestBadResponse(t *testing.T) {
 	defer server.Close()
 
 	metricsMock := &pbsmetrics.MetricsEngineMock{}
-	successCacheCallResponseLabels := pbsmetrics.RequestLabels{
-		RequestStatus: pbsmetrics.RequestStatusOK,
-	}
-	metricsMock.On("RecordPrebidCacheRequestTime", successCacheCallResponseLabels, mock.Anything).Once()
+	metricsMock.On("RecordPrebidCacheRequestTime", true, mock.Anything).Once()
 
 	client := &clientImpl{
 		httpClient: server.Client(),
@@ -82,10 +79,7 @@ func TestCancelledContext(t *testing.T) {
 	defer server.Close()
 
 	metricsMock := &pbsmetrics.MetricsEngineMock{}
-	errorCacheCallResponseLabels := pbsmetrics.RequestLabels{
-		RequestStatus: pbsmetrics.RequestStatusErr,
-	}
-	metricsMock.On("RecordPrebidCacheRequestTime", errorCacheCallResponseLabels, mock.Anything).Once()
+	metricsMock.On("RecordPrebidCacheRequestTime", false, mock.Anything).Once()
 
 	client := &clientImpl{
 		httpClient: server.Client(),
@@ -111,10 +105,7 @@ func TestSuccessfulPut(t *testing.T) {
 	defer server.Close()
 
 	metricsMock := &pbsmetrics.MetricsEngineMock{}
-	successCacheCallResponseLabels := pbsmetrics.RequestLabels{
-		RequestStatus: pbsmetrics.RequestStatusOK,
-	}
-	metricsMock.On("RecordPrebidCacheRequestTime", successCacheCallResponseLabels, mock.Anything).Once()
+	metricsMock.On("RecordPrebidCacheRequestTime", true, mock.Anything).Once()
 
 	client := &clientImpl{
 		httpClient: server.Client(),
@@ -165,10 +156,10 @@ func TestStripCacheHostAndPath(t *testing.T) {
 		{
 			inExtCacheURL: config.ExternalCache{
 				Host: "prebid-server.prebid.org",
-				Path: "pbcache/endpoint",
+				Path: "/pbcache/endpoint",
 			},
 			expectedHost: "prebid-server.prebid.org",
-			expectedPath: "pbcache/endpoint",
+			expectedPath: "/pbcache/endpoint",
 		},
 		{
 			inExtCacheURL: config.ExternalCache{
@@ -186,6 +177,22 @@ func TestStripCacheHostAndPath(t *testing.T) {
 			expectedHost: "",
 			expectedPath: "",
 		},
+		{
+			inExtCacheURL: config.ExternalCache{
+				Host: "prebid-server.prebid.org",
+				Path: "pbcache/endpoint",
+			},
+			expectedHost: "prebid-server.prebid.org",
+			expectedPath: "/pbcache/endpoint",
+		},
+		{
+			inExtCacheURL: config.ExternalCache{
+				Host: "prebidcache.net",
+				Path: "/",
+			},
+			expectedHost: "prebidcache.net",
+			expectedPath: "",
+		},
 	}
 	for _, test := range testInput {
 		//start client
diff --git a/privacy/ccpa/ccpa_test.go b/privacy/ccpa/ccpa_test.go
new file mode 100644
index 00000000..a5b48d14
--- /dev/null
+++ b/privacy/ccpa/ccpa_test.go
@@ -0,0 +1,74 @@
+package ccpa
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestWrite(t *testing.T) {
+	testCases := []struct {
+		description   string
+		policy        Policy
+		request       *openrtb.BidRequest
+		expected      *openrtb.BidRequest
+		expectedError bool
+	}{
+		{
+			description: "Disabled",
+			policy:      Policy{Value: ""},
+			request:     &openrtb.BidRequest{},
+			expected:    &openrtb.BidRequest{},
+		},
+		{
+			description: "Enabled With Nil Request Regs Object",
+			policy:      Policy{Value: "anyValue"},
+			request:     &openrtb.BidRequest{},
+			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"us_privacy":"anyValue"}`)}},
+		},
+		{
+			description: "Enabled With Nil Request Regs Ext Object",
+			policy:      Policy{Value: "anyValue"},
+			request:     &openrtb.BidRequest{Regs: &openrtb.Regs{}},
+			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"us_privacy":"anyValue"}`)}},
+		},
+		{
+			description: "Enabled With Existing Request Regs Ext Object - Doesn't Overwrite",
+			policy:      Policy{Value: "anyValue"},
+			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"existing":"any"}`)}},
+			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"existing":"any","us_privacy":"anyValue"}`)}},
+		},
+		{
+			description: "Enabled With Existing Request Regs Ext Object - Overwrites",
+			policy:      Policy{Value: "anyValue"},
+			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"existing":"any","us_privacy":"toBeOverwritten"}`)}},
+			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"existing":"any","us_privacy":"anyValue"}`)}},
+		},
+		{
+			description: "Enabled With Existing Malformed Request Regs Ext Object",
+			policy:      Policy{Value: "anyValue"},
+			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`malformed`)}},
+			expectedError: true,
+		},
+	}
+
+	for _, test := range testCases {
+		err := test.policy.Write(test.request)
+
+		if test.expectedError {
+			assert.Error(t, err, test.description)
+		} else {
+			assert.NoError(t, err, test.description)
+			assert.Equal(t, test.expected, test.request, test.description)
+		}
+	}
+}
diff --git a/privacy/gdpr/gdpr_test.go b/privacy/gdpr/gdpr_test.go
new file mode 100644
index 00000000..34605f07
--- /dev/null
+++ b/privacy/gdpr/gdpr_test.go
@@ -0,0 +1,74 @@
+package gdpr
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestWrite(t *testing.T) {
+	testCases := []struct {
+		description   string
+		policy        Policy
+		request       *openrtb.BidRequest
+		expected      *openrtb.BidRequest
+		expectedError bool
+	}{
+		{
+			description: "Disabled",
+			policy:      Policy{Consent: ""},
+			request:     &openrtb.BidRequest{},
+			expected:    &openrtb.BidRequest{},
+		},
+		{
+			description: "Enabled With Nil Request User Object",
+			policy:      Policy{Consent: "anyValue"},
+			request:     &openrtb.BidRequest{},
+			expected: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"consent":"anyValue"}`)}},
+		},
+		{
+			description: "Enabled With Nil Request User Ext Object",
+			policy:      Policy{Consent: "anyValue"},
+			request:     &openrtb.BidRequest{User: &openrtb.User{}},
+			expected: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"consent":"anyValue"}`)}},
+		},
+		{
+			description: "Enabled With Existing Request User Ext Object - Doesn't Overwrite",
+			policy:      Policy{Consent: "anyValue"},
+			request: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"existing":"any"}`)}},
+			expected: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"existing":"any","consent":"anyValue"}`)}},
+		},
+		{
+			description: "Enabled With Existing Request User Ext Object - Overwrites",
+			policy:      Policy{Consent: "anyValue"},
+			request: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"existing":"any","consent":"toBeOverwritten"}`)}},
+			expected: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"existing":"any","consent":"anyValue"}`)}},
+		},
+		{
+			description: "Enabled With Existing Malformed Request User Ext Object",
+			policy:      Policy{Consent: "anyValue"},
+			request: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`malformed`)}},
+			expectedError: true,
+		},
+	}
+
+	for _, test := range testCases {
+		err := test.policy.Write(test.request)
+
+		if test.expectedError {
+			assert.Error(t, err, test.description)
+		} else {
+			assert.NoError(t, err, test.description)
+			assert.Equal(t, test.expected, test.request, test.description)
+		}
+	}
+}
diff --git a/privacy/policies_test.go b/privacy/policies_test.go
new file mode 100644
index 00000000..69794252
--- /dev/null
+++ b/privacy/policies_test.go
@@ -0,0 +1,77 @@
+package privacy
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func TestWritePoliciesNone(t *testing.T) {
+	request := &openrtb.BidRequest{}
+	policyWriters := []policyWriter{}
+
+	err := writePolicies(request, policyWriters)
+
+	assert.NoError(t, err)
+}
+
+func TestWritePoliciesOne(t *testing.T) {
+	request := &openrtb.BidRequest{}
+	mockWriter := new(mockPolicyWriter)
+	policyWriters := []policyWriter{
+		mockWriter,
+	}
+
+	mockWriter.On("Write", request).Return(nil).Once()
+
+	err := writePolicies(request, policyWriters)
+
+	assert.NoError(t, err)
+	mockWriter.AssertExpectations(t)
+}
+
+func TestWritePoliciesMany(t *testing.T) {
+	request := &openrtb.BidRequest{}
+	mockWriter1 := new(mockPolicyWriter)
+	mockWriter2 := new(mockPolicyWriter)
+	policyWriters := []policyWriter{
+		mockWriter1, mockWriter2,
+	}
+
+	mockWriter1.On("Write", request).Return(nil).Once()
+	mockWriter2.On("Write", request).Return(nil).Once()
+
+	err := writePolicies(request, policyWriters)
+
+	assert.NoError(t, err)
+	mockWriter1.AssertExpectations(t)
+	mockWriter2.AssertExpectations(t)
+}
+
+func TestWritePoliciesError(t *testing.T) {
+	request := &openrtb.BidRequest{}
+	mockWriter := new(mockPolicyWriter)
+	policyWriters := []policyWriter{
+		mockWriter,
+	}
+
+	expectedErr := errors.New("anyError")
+	mockWriter.On("Write", request).Return(expectedErr).Once()
+
+	err := writePolicies(request, policyWriters)
+
+	assert.Error(t, err, expectedErr)
+	mockWriter.AssertExpectations(t)
+}
+
+type mockPolicyWriter struct {
+	mock.Mock
+}
+
+func (m *mockPolicyWriter) Write(req *openrtb.BidRequest) error {
+	args := m.Called(req)
+	return args.Error(0)
+}