Repository: prebid/prebid-server
Pull request #1113 (2019-11-14 18:46:12)

facebook: removed Auth-Token from header (replaced by authentication_id in the request body)


Patch:
diff --git a/.travis.yml b/.travis.yml
index 2ad1f6c7..3cc55e5f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,6 @@
 language: go
 
 go:
-  - '1.11.4'
   - '1.12'
   - '1.13'
 
diff --git a/Dockerfile b/Dockerfile
index 76aef2af..a8fea9c3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,12 +16,16 @@ RUN apt-get update && \
     apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
 ENV CGO_ENABLED 0
 COPY ./ ./
+RUN go mod vendor
+RUN go mod tidy
+ARG TEST="true"
+RUN if [ "$TEST" != "false" ]; then ./validate.sh ; fi
 RUN go build -mod=vendor .
 
 FROM ubuntu:18.04 AS release
 LABEL maintainer="hans.hjort@xandr.com" 
 WORKDIR /usr/local/bin/
-COPY --from=build /app/prebid-server/ .
+COPY --from=build /app/prebid-server/prebid-server .
 COPY static static/
 COPY stored_requests/data stored_requests/data
 RUN apt-get update && \
diff --git a/Makefile b/Makefile
index d58fd313..8ffea91f 100644
--- a/Makefile
+++ b/Makefile
@@ -1,35 +1,27 @@
 # Makefile
 
-all:
-	@echo ""
-	@echo "  deps: grab dependencies using go modules"
-	@echo "  test: test prebid-server (via validate.sh)"
-	@echo "  build: build prebid-server"
-	@echo "  image: build docker image"
-	@echo ""
+all: deps test build
 
-.PHONY: install deps test build image
+.PHONY: deps test build image
 
 # deps will clean out the vendor directory and use go mod for a fresh install
 deps:
-	rm -rf vendor
 	GOPROXY="https://proxy.golang.org" go mod vendor -v && go mod tidy -v
 	
 # test will ensure that all of our dependencies are available and run validate.sh
 test: deps
+# If there is no indentation, Make will treat it as a directive for itself; otherwise, it's regarded as a shell script.
+# https://stackoverflow.com/a/4483467
+ifeq "$(adapter)" ""
 	./validate.sh
-
-	# TODO: when adapters are in their own packages we can enable adapter-specific testing by passing the "adapter" argument
-	#ifeq ($(adapter),"all")
-	#	./validate.sh
-	#else
-	#	go test github.com/prebid/prebid-server/adapters/$(adapter) -bench=.
-	#endif
+else
+	go test github.com/prebid/prebid-server/adapters/$(adapter) -bench=.
+endif
 
 # build will ensure all of our tests pass and then build the go binary
 build: test
 	go build -mod=vendor ./...
 
 # image will build a docker image
-image: build
+image:
 	docker build -t prebid-server .
diff --git a/README.md b/README.md
index 9ba58d89..a59bf5f6 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ For more information, see:
 
 ## Installation
 
-First install [Go 1.11](https://golang.org/doc/install) latest version (1.11.4) or later.
+First install [Go 1.12](https://golang.org/doc/install) latest version.
 Note that prebid-server is using [Go modules](https://blog.golang.org/using-go-modules).
 If using Go version <1.13 and are inside GOPATH `GO111MODULE` needs to be set to `GO111MODULE=on`.
 
diff --git a/adapters/audienceNetwork/facebook.go b/adapters/audienceNetwork/facebook.go
index f8c993ba..a20a2393 100644
--- a/adapters/audienceNetwork/facebook.go
+++ b/adapters/audienceNetwork/facebook.go
@@ -23,7 +23,6 @@ type FacebookAdapter struct {
 	URI          string
 	nonSecureUri string
 	platformID   string
-	appID        string
 	appSecret    string
 }
 
@@ -75,7 +74,6 @@ func (this *FacebookAdapter) buildRequests(request *openrtb.BidRequest) ([]*adap
 
 	headers.Add("Content-Type", "application/json;charset=utf-8")
 	headers.Add("Accept", "application/json")
-	headers.Add("Auth-Token", this.appID+"|"+this.appSecret)
 	headers.Add("X-Fb-Pool-Routing-Token", request.User.BuyerUID)
 
 	for _, imp := range request.Imp {
@@ -406,7 +404,7 @@ func resolveImpType(imp *openrtb.Imp) (openrtb_ext.BidType, bool) {
 	return openrtb_ext.BidTypeBanner, false
 }
 
-func NewFacebookBidder(client *http.Client, platformID string, appID string, appSecret string) adapters.Bidder {
+func NewFacebookBidder(client *http.Client, platformID string, appSecret string) adapters.Bidder {
 	if platformID == "" {
 		glog.Errorf("No facebook partnerID specified. Calls to the Audience Network will fail. Did you set adapters.facebook.platform_id in the app config?")
 		return &adapters.MisconfiguredBidder{
@@ -415,14 +413,6 @@ func NewFacebookBidder(client *http.Client, platformID string, appID string, app
 		}
 	}
 
-	if appID == "" {
-		glog.Errorf("No facebook app ID specified. Calls to the Audience Network will fail. Did you set adapters.facebook.app_id in the app config?")
-		return &adapters.MisconfiguredBidder{
-			Name:  "audienceNetwork",
-			Error: errors.New("Audience Network is not configured properly on this Prebid Server deploy. If you believe this should work, contact the company hosting the service and tell them to check their configuration."),
-		}
-	}
-
 	if appSecret == "" {
 		glog.Errorf("No facebook app secret specified. Calls to the Audience Network will fail. Did you set adapters.facebook.app_secret in the app config?")
 		return &adapters.MisconfiguredBidder{
@@ -439,7 +429,6 @@ func NewFacebookBidder(client *http.Client, platformID string, appID string, app
 		//for AB test
 		nonSecureUri: "http://an.facebook.com/placementbid.ortb",
 		platformID:   platformID,
-		appID:        appID,
 		appSecret:    appSecret,
 	}
 }
diff --git a/adapters/triplelift_native/triplelift_native.go b/adapters/triplelift_native/triplelift_native.go
new file mode 100644
index 00000000..a2576032
--- /dev/null
+++ b/adapters/triplelift_native/triplelift_native.go
@@ -0,0 +1,202 @@
+package triplelift_native
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/golang/glog"
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"net/http"
+)
+
+type TripleliftNativeAdapter struct {
+	endpoint string
+	extInfo  TripleliftNativeExtInfo
+}
+
+type TripleliftInnerExt struct {
+	Format int `json:"format"`
+}
+
+type TripleliftRespExt struct {
+	Triplelift TripleliftInnerExt `json:"triplelift_pb"`
+}
+
+type TripleliftNativeExtInfo struct {
+	// Array is used for deserialization.
+	PublisherWhitelist []string `json:"publisher_whitelist"`
+
+	Endpoint string `json:"endpoint"`
+
+	// Map is used for optimized memory access and should be constructed after deserialization.
+	PublisherWhitelistMap map[string]bool
+}
+
+func getBidType(ext TripleliftRespExt) openrtb_ext.BidType {
+	return openrtb_ext.BidTypeNative
+}
+
+func processImp(imp *openrtb.Imp) error {
+	// get the triplelift extension
+	var ext adapters.ExtImpBidder
+	var tlext openrtb_ext.ExtImpTriplelift
+	if err := json.Unmarshal(imp.Ext, &ext); err != nil {
+		return err
+	}
+	if err := json.Unmarshal(ext.Bidder, &tlext); err != nil {
+		return err
+	}
+	if imp.Native == nil {
+		return fmt.Errorf("no native object specified")
+	}
+	if tlext.InvCode == "" {
+		return fmt.Errorf("no inv_code specified")
+	}
+	imp.TagID = tlext.InvCode
+	// floor is optional
+	if tlext.Floor == nil {
+		return nil
+	}
+	imp.BidFloor = *tlext.Floor
+	// no error
+	return nil
+}
+
+// Returns the effective publisher ID
+func effectivePubID(pub *openrtb.Publisher) string {
+	if pub != nil {
+		if pub.Ext != nil {
+			var pubExt openrtb_ext.ExtPublisher
+			err := json.Unmarshal(pub.Ext, &pubExt)
+			if err == nil && pubExt.Prebid != nil && pubExt.Prebid.ParentAccount != nil && *pubExt.Prebid.ParentAccount != "" {
+				return *pubExt.Prebid.ParentAccount
+			}
+		}
+		if pub.ID != "" {
+			return pub.ID
+		}
+	}
+	return "unknown"
+}
+
+func (a *TripleliftNativeAdapter) MakeRequests(request *openrtb.BidRequest, extra *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	errs := make([]error, 0, len(request.Imp)+1)
+	reqs := make([]*adapters.RequestData, 0, 1)
+	// copy the request, because we are going to mutate it
+	tlRequest := *request
+	// this will contain all the valid impressions
+	var validImps []openrtb.Imp
+	// pre-process the imps
+	for _, imp := range tlRequest.Imp {
+		if err := processImp(&imp); err == nil {
+			validImps = append(validImps, imp)
+		} else {
+			errs = append(errs, err)
+		}
+	}
+	publisher := getPublisher(request)
+	publisherID := effectivePubID(publisher)
+	if _, exists := a.extInfo.PublisherWhitelistMap[publisherID]; !exists {
+		err := fmt.Errorf("Unsupported publisher for triplelift_native")
+		return nil, []error{err}
+	}
+	if len(validImps) == 0 {
+		err := fmt.Errorf("No valid impressions for triplelift")
+		errs = append(errs, err)
+		return nil, errs
+	}
+	tlRequest.Imp = validImps
+	reqJSON, err := json.Marshal(tlRequest)
+	if err != nil {
+		errs = append(errs, err)
+		return nil, errs
+	}
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+	ad := a.endpoint
+	reqs = append(reqs, &adapters.RequestData{
+		Method:  "POST",
+		Uri:     ad,
+		Body:    reqJSON,
+		Headers: headers})
+	return reqs, errs
+}
+
+func getPublisher(request *openrtb.BidRequest) *openrtb.Publisher {
+	if request.App != nil {
+		return request.App.Publisher
+	}
+	return request.Site.Publisher
+}
+
+func getBidCount(bidResponse openrtb.BidResponse) int {
+	c := 0
+	for _, sb := range bidResponse.SeatBid {
+		c = c + len(sb.Bid)
+	}
+	return c
+}
+
+func (a *TripleliftNativeAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if response.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+
+	if response.StatusCode == http.StatusBadRequest {
+		return nil, []error{&errortypes.BadInput{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
+		}}
+	}
+
+	if response.StatusCode != http.StatusOK {
+		return nil, []error{&errortypes.BadServerResponse{Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode)}}
+	}
+	var bidResp openrtb.BidResponse
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+	var errs []error
+	count := getBidCount(bidResp)
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(count)
+
+	for _, sb := range bidResp.SeatBid {
+		for i := 0; i < len(sb.Bid); i++ {
+			bid := sb.Bid[i]
+			var bidExt TripleliftRespExt
+			bidType := getBidType(bidExt)
+			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+				Bid:     &bid,
+				BidType: bidType,
+			})
+		}
+	}
+	return bidResponse, errs
+}
+
+func NewTripleliftNativeBidder(client *http.Client, endpoint string, extraInfo string) adapters.Bidder {
+	var extInfo TripleliftNativeExtInfo
+
+	if len(extraInfo) == 0 {
+		extraInfo = "{\"publisher_whitelist\":[]}"
+	}
+	if err := json.Unmarshal([]byte(extraInfo), &extInfo); err != nil {
+		glog.Errorf("Invalid TripleLife Native extra adapter info: " + err.Error())
+		return &adapters.MisconfiguredBidder{
+			Name:  "TripleliftNativeAdapter",
+			Error: fmt.Errorf("TripleliftNativeAdapter could not unmarshal config json"),
+		}
+	}
+
+	// Populate map for faster memory access
+	extInfo.PublisherWhitelistMap = make(map[string]bool)
+	for _, v := range extInfo.PublisherWhitelist {
+		extInfo.PublisherWhitelistMap[v] = true
+	}
+
+	return &TripleliftNativeAdapter{
+		extInfo:  extInfo,
+		endpoint: extInfo.Endpoint}
+}
diff --git a/adapters/triplelift_native/usersync.go b/adapters/triplelift_native/usersync.go
new file mode 100644
index 00000000..b7587b77
--- /dev/null
+++ b/adapters/triplelift_native/usersync.go
@@ -0,0 +1,12 @@
+package triplelift_native
+
+import (
+	"text/template"
+
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/usersync"
+)
+
+func NewTripleliftSyncer(temp *template.Template) usersync.Usersyncer {
+	return adapters.NewSyncer("triplelift", 28, temp, adapters.SyncTypeRedirect)
+}
diff --git a/config/config.go b/config/config.go
index 64e16cbe..c4aa788e 100644
--- a/config/config.go
+++ b/config/config.go
@@ -231,7 +231,6 @@ type Adapter struct {
 
 	// needed for Facebook
 	PlatformID string `mapstructure:"platform_id"`
-	AppID      string `mapstructure:"app_id"`
 	AppSecret  string `mapstructure:"app_secret"`
 }
 
@@ -485,12 +484,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")
@@ -506,11 +505,12 @@ func (cfg *Configuration) setDerivedDefaults() {
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSomoaudience, "https://publisher-east.mobileadtrading.com/usersync?ru="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsomoaudience%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUID%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSovrn, "https://ap.lijit.com/pixel?redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsovrn%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSonobi, "https://sync.go.sonobi.com/us.gif?loc="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsonobi%26consent_string%3D{{.GDPR}}%26gdpr%3D{{.GDPRConsent}}%26uid%3D%5BUID%5D")
-	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSynacormedia, "http://sync.technoratimedia.com/services?srv=cs&pid=70&cb="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsynacormedia%26uid%3D%5BUSER_ID%5D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSynacormedia, "https://sync.technoratimedia.com/services?srv=cs&pid=70&cb="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsynacormedia%26uid%3D%5BUSER_ID%5D")
 	// openrtb_ext.BidderVrtcal doesn't have a good default.
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderYieldmo, "https://ads.yieldmo.com/pbsync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dyieldmo%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGamoshi, "https://rtb.gamoshi.io/pix/0000/scm?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgamoshi%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bgusr%5D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderMgid, "https://cm.mgid.com/m?cdsp=363893&adu="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmgid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Bmuidn%7D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderTripleliftNative, "https://eb2.3lift.com/sync?gpdr={{.GDPR}}&cmp_cs={{.GDPRConsent}}")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderTriplelift, "https://eb2.3lift.com/getuid?gpdr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dtriplelift%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderUnruly, "https://usermatch.targeting.unrulymedia.com/pbsync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dunruly%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderVisx, "https://t.visx.net/s2s_sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dvisx%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D")
@@ -674,6 +674,8 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.rtbhouse.endpoint", "http://prebidserver-s2s-ams.creativecdn.com/bidder/prebidserver/bids")
 	v.SetDefault("adapters.somoaudience.endpoint", "http://publisher-east.mobileadtrading.com/rtb/bid")
 	v.SetDefault("adapters.sovrn.endpoint", "http://ap.lijit.com/rtb/bid?src=prebid_server")
+	v.SetDefault("adapters.triplelift_native.disabled", true)
+	v.SetDefault("adapters.triplelift_native.extra_info", "{\"publisher_whitelist\":[]}")
 	v.SetDefault("adapters.synacormedia.endpoint", "http://{{.Host}}.technoratimedia.com/openrtb/bids/{{.Host}}")
 	v.SetDefault("adapters.triplelift.endpoint", "https://tlx.3lift.com/s2s/auction?supplier_id=20")
 	v.SetDefault("adapters.adkernel.endpoint", "http://{{.Host}}/hb?zone={{.ZoneID}}")
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/docs/developers/currency-converter.md b/docs/developers/currency-converter.md
index 9acb31e7..64f77060 100644
--- a/docs/developers/currency-converter.md
+++ b/docs/developers/currency-converter.md
@@ -53,4 +53,4 @@ This configuration can be changed:
 ## Debug
 
 A dedicated endpoint will allow you to see what's happening within the currency converter.
-See [currency rates endpoint](../endpoints/currency_rates.md) for more details.
\ No newline at end of file
+See [currency rates endpoint](../endpoints/currency_rates.md) for more details.
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/adapter_map.go b/exchange/adapter_map.go
index e38c0edf..7febfd5e 100644
--- a/exchange/adapter_map.go
+++ b/exchange/adapter_map.go
@@ -2,10 +2,11 @@ package exchange
 
 import (
 	"fmt"
-	"github.com/prebid/prebid-server/adapters/kubient"
 	"net/http"
 	"strings"
 
+	"github.com/prebid/prebid-server/adapters/kubient"
+
 	"github.com/prebid/prebid-server/adapters"
 	ttx "github.com/prebid/prebid-server/adapters/33across"
 	"github.com/prebid/prebid-server/adapters/adform"
@@ -46,6 +47,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/synacormedia"
 	"github.com/prebid/prebid-server/adapters/tappx"
 	"github.com/prebid/prebid-server/adapters/triplelift"
+	"github.com/prebid/prebid-server/adapters/triplelift_native"
 	"github.com/prebid/prebid-server/adapters/unruly"
 	"github.com/prebid/prebid-server/adapters/verizonmedia"
 	"github.com/prebid/prebid-server/adapters/visx"
@@ -87,29 +89,29 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 			cfg.Adapters[string(openrtb_ext.BidderRubicon)].XAPI.Username,
 			cfg.Adapters[string(openrtb_ext.BidderRubicon)].XAPI.Password,
 			cfg.Adapters[string(openrtb_ext.BidderRubicon)].XAPI.Tracker),
-		openrtb_ext.BidderSharethrough:   sharethrough.NewSharethroughBidder(cfg.Adapters[string(openrtb_ext.BidderSharethrough)].Endpoint),
-		openrtb_ext.BidderSomoaudience:   somoaudience.NewSomoaudienceBidder(cfg.Adapters[string(openrtb_ext.BidderSomoaudience)].Endpoint),
-		openrtb_ext.BidderSovrn:          sovrn.NewSovrnBidder(client, cfg.Adapters[string(openrtb_ext.BidderSovrn)].Endpoint),
-		openrtb_ext.Bidder33Across:       ttx.New33AcrossBidder(cfg.Adapters[string(openrtb_ext.Bidder33Across)].Endpoint),
-		openrtb_ext.BidderGrid:           grid.NewGridBidder(cfg.Adapters[string(openrtb_ext.BidderGrid)].Endpoint),
-		openrtb_ext.BidderSonobi:         sonobi.NewSonobiBidder(client, cfg.Adapters[string(openrtb_ext.BidderSonobi)].Endpoint),
-		openrtb_ext.BidderSynacormedia:   synacormedia.NewSynacorMediaBidder(cfg.Adapters[string(openrtb_ext.BidderSynacormedia)].Endpoint),
-		openrtb_ext.BidderTriplelift:     triplelift.NewTripleliftBidder(client, cfg.Adapters[string(openrtb_ext.BidderTriplelift)].Endpoint),
-		openrtb_ext.BidderUnruly:         unruly.NewUnrulyBidder(client, cfg.Adapters[string(openrtb_ext.BidderUnruly)].Endpoint),
-		openrtb_ext.BidderVrtcal:         vrtcal.NewVrtcalBidder(cfg.Adapters[string(openrtb_ext.BidderVrtcal)].Endpoint),
-		openrtb_ext.BidderYieldmo:        yieldmo.NewYieldmoBidder(cfg.Adapters[string(openrtb_ext.BidderYieldmo)].Endpoint),
-		openrtb_ext.BidderGamma:          gamma.NewGammaBidder(cfg.Adapters[string(openrtb_ext.BidderGamma)].Endpoint),
-		openrtb_ext.BidderVisx:           visx.NewVisxBidder(cfg.Adapters[string(openrtb_ext.BidderVisx)].Endpoint),
-		openrtb_ext.BidderGamoshi:        gamoshi.NewGamoshiBidder(cfg.Adapters[string(openrtb_ext.BidderGamoshi)].Endpoint),
-		openrtb_ext.BidderMgid:           mgid.NewMgidBidder(cfg.Adapters[string(openrtb_ext.BidderMgid)].Endpoint),
-		openrtb_ext.BidderImprovedigital: improvedigital.NewImprovedigitalBidder(cfg.Adapters[string(openrtb_ext.BidderImprovedigital)].Endpoint),
-		openrtb_ext.BidderTappx:          tappx.NewTappxBidder(client, cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderTappx))].Endpoint),
-		openrtb_ext.BidderVerizonMedia:   verizonmedia.NewVerizonMediaBidder(client, cfg.Adapters[string(openrtb_ext.BidderVerizonMedia)].Endpoint),
-		openrtb_ext.BidderKubient:        kubient.NewKubientBidder(cfg.Adapters[string(openrtb_ext.BidderKubient)].Endpoint),
+		openrtb_ext.BidderSharethrough:     sharethrough.NewSharethroughBidder(cfg.Adapters[string(openrtb_ext.BidderSharethrough)].Endpoint),
+		openrtb_ext.BidderSomoaudience:     somoaudience.NewSomoaudienceBidder(cfg.Adapters[string(openrtb_ext.BidderSomoaudience)].Endpoint),
+		openrtb_ext.BidderSovrn:            sovrn.NewSovrnBidder(client, cfg.Adapters[string(openrtb_ext.BidderSovrn)].Endpoint),
+		openrtb_ext.Bidder33Across:         ttx.New33AcrossBidder(cfg.Adapters[string(openrtb_ext.Bidder33Across)].Endpoint),
+		openrtb_ext.BidderGrid:             grid.NewGridBidder(cfg.Adapters[string(openrtb_ext.BidderGrid)].Endpoint),
+		openrtb_ext.BidderSonobi:           sonobi.NewSonobiBidder(client, cfg.Adapters[string(openrtb_ext.BidderSonobi)].Endpoint),
+		openrtb_ext.BidderSynacormedia:     synacormedia.NewSynacorMediaBidder(cfg.Adapters[string(openrtb_ext.BidderSynacormedia)].Endpoint),
+		openrtb_ext.BidderTriplelift:       triplelift.NewTripleliftBidder(client, cfg.Adapters[string(openrtb_ext.BidderTriplelift)].Endpoint),
+		openrtb_ext.BidderTripleliftNative: triplelift_native.NewTripleliftNativeBidder(client, cfg.Adapters[string(openrtb_ext.BidderTripleliftNative)].Endpoint, cfg.Adapters[string(openrtb_ext.BidderTripleliftNative)].ExtraAdapterInfo),
+		openrtb_ext.BidderUnruly:           unruly.NewUnrulyBidder(client, cfg.Adapters[string(openrtb_ext.BidderUnruly)].Endpoint),
+		openrtb_ext.BidderVrtcal:           vrtcal.NewVrtcalBidder(cfg.Adapters[string(openrtb_ext.BidderVrtcal)].Endpoint),
+		openrtb_ext.BidderYieldmo:          yieldmo.NewYieldmoBidder(cfg.Adapters[string(openrtb_ext.BidderYieldmo)].Endpoint),
+		openrtb_ext.BidderGamma:            gamma.NewGammaBidder(cfg.Adapters[string(openrtb_ext.BidderGamma)].Endpoint),
+		openrtb_ext.BidderVisx:             visx.NewVisxBidder(cfg.Adapters[string(openrtb_ext.BidderVisx)].Endpoint),
+		openrtb_ext.BidderGamoshi:          gamoshi.NewGamoshiBidder(cfg.Adapters[string(openrtb_ext.BidderGamoshi)].Endpoint),
+		openrtb_ext.BidderMgid:             mgid.NewMgidBidder(cfg.Adapters[string(openrtb_ext.BidderMgid)].Endpoint),
+		openrtb_ext.BidderImprovedigital:   improvedigital.NewImprovedigitalBidder(cfg.Adapters[string(openrtb_ext.BidderImprovedigital)].Endpoint),
+		openrtb_ext.BidderTappx:            tappx.NewTappxBidder(client, cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderTappx))].Endpoint),
+		openrtb_ext.BidderVerizonMedia:     verizonmedia.NewVerizonMediaBidder(client, cfg.Adapters[string(openrtb_ext.BidderVerizonMedia)].Endpoint),
+		openrtb_ext.BidderKubient:          kubient.NewKubientBidder(cfg.Adapters[string(openrtb_ext.BidderKubient)].Endpoint),
 		openrtb_ext.BidderFacebook: audienceNetwork.NewFacebookBidder(
 			client,
 			cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderFacebook))].PlatformID,
-			cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderFacebook))].AppID,
 			cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderFacebook))].AppSecret),
 	}
 
diff --git a/exchange/exchange.go b/exchange/exchange.go
index ec6fdd80..e92e813f 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -129,7 +129,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 
 	// If we need to cache bids, then it will take some time to call prebid cache.
 	// We should reduce the amount of time the bidders have, to compensate.
-	auctionCtx, cancel := e.makeAuctionContext(ctx, shouldCacheBids)
+	auctionCtx, cancel := e.makeAuctionContext(ctx, shouldCacheBids) //Why no context for `shouldCacheVast`?
 	defer cancel()
 
 	// Get currency rates conversions for the auction
@@ -137,6 +137,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 
 	adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, cleanRequests, aliases, bidAdjustmentFactors, blabels, conversions)
 
+	var auc *auction = nil
 	if anyBidsReturned {
 
 		var bidCategory map[string]string
@@ -149,7 +150,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 			}
 		}
 
-		auc := newAuction(adapterBids, len(bidRequest.Imp))
+		auc = newAuction(adapterBids, len(bidRequest.Imp))
 
 		if targData != nil {
 			auc.setRoundedPrices(targData.priceGranularity)
@@ -162,7 +163,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 	}
 
 	// Build the response
-	return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, errs)
+	return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, auc, errs)
 }
 
 func (e *exchange) makeAuctionContext(ctx context.Context, needsCache bool) (auctionCtx context.Context, cancel context.CancelFunc) {
@@ -304,7 +305,7 @@ func errsToBidderErrors(errs []error) []openrtb_ext.ExtBidderError {
 }
 
 // This piece takes all the bids supplied by the adapters and crafts an openRTB response to send back to the requester
-func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, bidRequest *openrtb.BidRequest, resolvedRequest json.RawMessage, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, errList []error) (*openrtb.BidResponse, error) {
+func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, bidRequest *openrtb.BidRequest, resolvedRequest json.RawMessage, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, errList []error) (*openrtb.BidResponse, error) {
 	bidResponse := new(openrtb.BidResponse)
 
 	bidResponse.ID = bidRequest.ID
@@ -319,7 +320,7 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_
 	for _, a := range liveAdapters {
 		//while processing every single bib, do we need to handle categories here?
 		if adapterBids[a] != nil && len(adapterBids[a].bids) > 0 {
-			sb := e.makeSeatBid(adapterBids[a], a, adapterExtra)
+			sb := e.makeSeatBid(adapterBids[a], a, adapterExtra, auc)
 			seatBids = append(seatBids, *sb)
 		}
 	}
@@ -542,7 +543,7 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pb
 
 // Return an openrtb seatBid for a bidder
 // BuildBidResponse is responsible for ensuring nil bid seatbids are not included
-func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra) *openrtb.SeatBid {
+func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction) *openrtb.SeatBid {
 	seatBid := new(openrtb.SeatBid)
 	seatBid.Seat = adapter.String()
 	// Prebid cannot support roadblocking
@@ -565,7 +566,7 @@ func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.B
 	}
 
 	var errList []error
-	seatBid.Bid, errList = e.makeBid(adapterBid.bids, adapter)
+	seatBid.Bid, errList = e.makeBid(adapterBid.bids, adapter, auc)
 	if len(errList) > 0 {
 		adapterExtra[adapter].Errors = append(adapterExtra[adapter].Errors, errsToBidderErrors(errList)...)
 	}
@@ -574,7 +575,7 @@ func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.B
 }
 
 // Create the Bid array inside of SeatBid
-func (e *exchange) makeBid(Bids []*pbsOrtbBid, adapter openrtb_ext.BidderName) ([]openrtb.Bid, []error) {
+func (e *exchange) makeBid(Bids []*pbsOrtbBid, adapter openrtb_ext.BidderName, auc *auction) ([]openrtb.Bid, []error) {
 	bids := make([]openrtb.Bid, 0, len(Bids))
 	errList := make([]error, 0, 1)
 	for _, thisBid := range Bids {
@@ -586,7 +587,11 @@ func (e *exchange) makeBid(Bids []*pbsOrtbBid, adapter openrtb_ext.BidderName) (
 				Video:     thisBid.bidVideo,
 			},
 		}
-
+		if cacheInfo, found := e.getBidCacheInfo(thisBid, auc); found {
+			bidExt.Prebid.Cache = &openrtb_ext.ExtBidPrebidCache{
+				Bids: &cacheInfo,
+			}
+		}
 		ext, err := json.Marshal(bidExt)
 		if err != nil {
 			errList = append(errList, err)
@@ -598,6 +603,28 @@ func (e *exchange) makeBid(Bids []*pbsOrtbBid, adapter openrtb_ext.BidderName) (
 	return bids, errList
 }
 
+// If bid got cached inside `(a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, targData *targetData, bidRequest *openrtb.BidRequest, ttlBuffer int64, defaultTTLs *config.DefaultTTLs, bidCategory map[string]string)`,
+// a UUID should be found inside `a.cacheIds` or `a.vastCacheIds`. This function returns the UUID along with the internal cache URL
+func (e *exchange) getBidCacheInfo(bid *pbsOrtbBid, auc *auction) (openrtb_ext.ExtBidPrebidCacheBids, bool) {
+	var cacheInfo openrtb_ext.ExtBidPrebidCacheBids
+	var cacheUUID string
+	var found bool = false
+
+	if auc != nil {
+		var extCacheHost, extCachePath string
+		if cacheUUID, found = auc.cacheIds[bid.bid]; found {
+			cacheInfo.CacheId = cacheUUID
+			extCacheHost, extCachePath = e.cache.GetExtCacheData()
+			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
+		}
+	}
+	return cacheInfo, found
+}
+
 // Returns a snapshot of resolved bid request for debug if test field is set in the incomming request
 func buildResolvedRequest(bidRequest *openrtb.BidRequest) (json.RawMessage, error) {
 	if bidRequest.Test == 1 {
diff --git a/go.mod b/go.mod
index b0c0ee67..5c837c2e 100644
--- a/go.mod
+++ b/go.mod
@@ -32,6 +32,7 @@ require (
 	github.com/onsi/ginkgo v1.10.1 // indirect
 	github.com/onsi/gomega v1.7.0 // indirect
 	github.com/pelletier/go-toml v1.2.0 // indirect
+	github.com/prebid/go-gdpr v0.6.0
 	github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed
 	github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910
 	github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e // indirect
diff --git a/go.sum b/go.sum
index 02d21a8d..57fcbb76 100644
--- a/go.sum
+++ b/go.sum
@@ -70,10 +70,14 @@ github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+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/bid.go b/openrtb_ext/bid.go
index 91542d9a..c9c6f363 100644
--- a/openrtb_ext/bid.go
+++ b/openrtb_ext/bid.go
@@ -21,8 +21,14 @@ type ExtBidPrebid struct {
 
 // ExtBidPrebidCache defines the contract for  bidresponse.seatbid.bid[i].ext.prebid.cache
 type ExtBidPrebidCache struct {
-	Key string `json:"key"`
-	Url string `json:"url"`
+	Key  string                 `json:"key"`
+	Url  string                 `json:"url"`
+	Bids *ExtBidPrebidCacheBids `json:"bids,omitempty"`
+}
+
+type ExtBidPrebidCacheBids struct {
+	Url     string `json:"url"`
+	CacheId string `json:"cacheId"`
 }
 
 // ExtBidPrebidVideo defines the contract for bidresponse.seatbid.bid[i].ext.prebid.video
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 099cdc96..f7f54976 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -20,100 +20,102 @@ type BidderName string
 // These names _must_ coincide with the bidder code in Prebid.js, if an adapter also exists in that project.
 // Please keep these (and the BidderMap) alphabetized to minimize merge conflicts among adapter submissions.
 const (
-	Bidder33Across       BidderName = "33across"
-	BidderAdkernel       BidderName = "adkernel"
-	BidderAdkernelAdn    BidderName = "adkernelAdn"
-	BidderAdpone         BidderName = "adpone"
-	BidderAdtelligent    BidderName = "adtelligent"
-	BidderAdform         BidderName = "adform"
-	BidderAdvangelists   BidderName = "advangelists"
-	BidderAppnexus       BidderName = "appnexus"
-	BidderBeachfront     BidderName = "beachfront"
-	BidderBrightroll     BidderName = "brightroll"
-	BidderConsumable     BidderName = "consumable"
-	BidderConversant     BidderName = "conversant"
-	BidderDatablocks     BidderName = "datablocks"
-	BidderEmxDigital     BidderName = "emx_digital"
-	BidderEPlanning      BidderName = "eplanning"
-	BidderFacebook       BidderName = "audienceNetwork"
-	BidderGamma          BidderName = "gamma"
-	BidderGamoshi        BidderName = "gamoshi"
-	BidderGrid           BidderName = "grid"
-	BidderGumGum         BidderName = "gumgum"
-	BidderImprovedigital BidderName = "improvedigital"
-	BidderIx             BidderName = "ix"
-	BidderKubient        BidderName = "kubient"
-	BidderLifestreet     BidderName = "lifestreet"
-	BidderLockerDome     BidderName = "lockerdome"
-	BidderMgid           BidderName = "mgid"
-	BidderOpenx          BidderName = "openx"
-	BidderPubmatic       BidderName = "pubmatic"
-	BidderPulsepoint     BidderName = "pulsepoint"
-	BidderRhythmone      BidderName = "rhythmone"
-	BidderRTBHouse       BidderName = "rtbhouse"
-	BidderRubicon        BidderName = "rubicon"
-	BidderSharethrough   BidderName = "sharethrough"
-	BidderSomoaudience   BidderName = "somoaudience"
-	BidderSovrn          BidderName = "sovrn"
-	BidderSonobi         BidderName = "sonobi"
-	BidderSynacormedia   BidderName = "synacormedia"
-	BidderTriplelift     BidderName = "triplelift"
-	BidderUnruly         BidderName = "unruly"
-	BidderVerizonMedia   BidderName = "verizonmedia"
-	BidderVrtcal         BidderName = "vrtcal"
-	BidderYieldmo        BidderName = "yieldmo"
-	BidderVisx           BidderName = "visx"
-	BidderTappx          BidderName = "tappx"
-	BidderEngageBDR      BidderName = "engagebdr"
+	Bidder33Across         BidderName = "33across"
+	BidderAdkernel         BidderName = "adkernel"
+	BidderAdkernelAdn      BidderName = "adkernelAdn"
+	BidderAdpone           BidderName = "adpone"
+	BidderAdtelligent      BidderName = "adtelligent"
+	BidderAdform           BidderName = "adform"
+	BidderAdvangelists     BidderName = "advangelists"
+	BidderAppnexus         BidderName = "appnexus"
+	BidderBeachfront       BidderName = "beachfront"
+	BidderBrightroll       BidderName = "brightroll"
+	BidderConsumable       BidderName = "consumable"
+	BidderConversant       BidderName = "conversant"
+	BidderDatablocks       BidderName = "datablocks"
+	BidderEmxDigital       BidderName = "emx_digital"
+	BidderEPlanning        BidderName = "eplanning"
+	BidderFacebook         BidderName = "audienceNetwork"
+	BidderGamma            BidderName = "gamma"
+	BidderGamoshi          BidderName = "gamoshi"
+	BidderGrid             BidderName = "grid"
+	BidderGumGum           BidderName = "gumgum"
+	BidderImprovedigital   BidderName = "improvedigital"
+	BidderIx               BidderName = "ix"
+	BidderKubient          BidderName = "kubient"
+	BidderLifestreet       BidderName = "lifestreet"
+	BidderLockerDome       BidderName = "lockerdome"
+	BidderMgid             BidderName = "mgid"
+	BidderOpenx            BidderName = "openx"
+	BidderPubmatic         BidderName = "pubmatic"
+	BidderPulsepoint       BidderName = "pulsepoint"
+	BidderRhythmone        BidderName = "rhythmone"
+	BidderRTBHouse         BidderName = "rtbhouse"
+	BidderRubicon          BidderName = "rubicon"
+	BidderSharethrough     BidderName = "sharethrough"
+	BidderSomoaudience     BidderName = "somoaudience"
+	BidderSovrn            BidderName = "sovrn"
+	BidderSonobi           BidderName = "sonobi"
+	BidderSynacormedia     BidderName = "synacormedia"
+	BidderTriplelift       BidderName = "triplelift"
+	BidderTripleliftNative BidderName = "triplelift_native"
+	BidderUnruly           BidderName = "unruly"
+	BidderVerizonMedia     BidderName = "verizonmedia"
+	BidderVrtcal           BidderName = "vrtcal"
+	BidderYieldmo          BidderName = "yieldmo"
+	BidderVisx             BidderName = "visx"
+	BidderTappx            BidderName = "tappx"
+	BidderEngageBDR        BidderName = "engagebdr"
 )
 
 // BidderMap stores all the valid OpenRTB 2.x Bidders in the project. This map *must not* be mutated.
 var BidderMap = map[string]BidderName{
-	"33across":        Bidder33Across,
-	"adkernel":        BidderAdkernel,
-	"adkernelAdn":     BidderAdkernelAdn,
-	"adpone":          BidderAdpone,
-	"adtelligent":     BidderAdtelligent,
-	"adform":          BidderAdform,
-	"advangelists":    BidderAdvangelists,
-	"appnexus":        BidderAppnexus,
-	"beachfront":      BidderBeachfront,
-	"audienceNetwork": BidderFacebook,
-	"brightroll":      BidderBrightroll,
-	"consumable":      BidderConsumable,
-	"conversant":      BidderConversant,
-	"datablocks":      BidderDatablocks,
-	"emx_digital":     BidderEmxDigital,
-	"eplanning":       BidderEPlanning,
-	"gamma":           BidderGamma,
-	"gamoshi":         BidderGamoshi,
-	"grid":            BidderGrid,
-	"gumgum":          BidderGumGum,
-	"improvedigital":  BidderImprovedigital,
-	"ix":              BidderIx,
-	"lifestreet":      BidderLifestreet,
-	"lockerdome":      BidderLockerDome,
-	"openx":           BidderOpenx,
-	"mgid":            BidderMgid,
-	"pubmatic":        BidderPubmatic,
-	"pulsepoint":      BidderPulsepoint,
-	"rhythmone":       BidderRhythmone,
-	"rtbhouse":        BidderRTBHouse,
-	"rubicon":         BidderRubicon,
-	"sharethrough":    BidderSharethrough,
-	"somoaudience":    BidderSomoaudience,
-	"sovrn":           BidderSovrn,
-	"sonobi":          BidderSonobi,
-	"synacormedia":    BidderSynacormedia,
-	"triplelift":      BidderTriplelift,
-	"unruly":          BidderUnruly,
-	"verizonmedia":    BidderVerizonMedia,
-	"vrtcal":          BidderVrtcal,
-	"yieldmo":         BidderYieldmo,
-	"visx":            BidderVisx,
-	"tappx":           BidderTappx,
-	"engagebdr":       BidderEngageBDR,
-	"kubient":         BidderKubient,
+	"33across":          Bidder33Across,
+	"adkernel":          BidderAdkernel,
+	"adkernelAdn":       BidderAdkernelAdn,
+	"adpone":            BidderAdpone,
+	"adtelligent":       BidderAdtelligent,
+	"adform":            BidderAdform,
+	"advangelists":      BidderAdvangelists,
+	"appnexus":          BidderAppnexus,
+	"beachfront":        BidderBeachfront,
+	"audienceNetwork":   BidderFacebook,
+	"brightroll":        BidderBrightroll,
+	"consumable":        BidderConsumable,
+	"conversant":        BidderConversant,
+	"datablocks":        BidderDatablocks,
+	"emx_digital":       BidderEmxDigital,
+	"eplanning":         BidderEPlanning,
+	"gamma":             BidderGamma,
+	"gamoshi":           BidderGamoshi,
+	"grid":              BidderGrid,
+	"gumgum":            BidderGumGum,
+	"improvedigital":    BidderImprovedigital,
+	"ix":                BidderIx,
+	"lifestreet":        BidderLifestreet,
+	"lockerdome":        BidderLockerDome,
+	"openx":             BidderOpenx,
+	"mgid":              BidderMgid,
+	"pubmatic":          BidderPubmatic,
+	"pulsepoint":        BidderPulsepoint,
+	"rhythmone":         BidderRhythmone,
+	"rtbhouse":          BidderRTBHouse,
+	"rubicon":           BidderRubicon,
+	"sharethrough":      BidderSharethrough,
+	"somoaudience":      BidderSomoaudience,
+	"sovrn":             BidderSovrn,
+	"sonobi":            BidderSonobi,
+	"synacormedia":      BidderSynacormedia,
+	"triplelift":        BidderTriplelift,
+	"triplelift_native": BidderTripleliftNative,
+	"unruly":            BidderUnruly,
+	"verizonmedia":      BidderVerizonMedia,
+	"vrtcal":            BidderVrtcal,
+	"yieldmo":           BidderYieldmo,
+	"visx":              BidderVisx,
+	"tappx":             BidderTappx,
+	"engagebdr":         BidderEngageBDR,
+	"kubient":           BidderKubient,
 }
 
 // BidderList returns the values of the BidderMap
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 699eb198..6da69f68 100644
--- a/prebid_cache_client/client.go
+++ b/prebid_cache_client/client.go
@@ -8,6 +8,7 @@ import (
 	"io/ioutil"
 	"net/http"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/prebid/prebid-server/config"
@@ -69,7 +70,16 @@ type clientImpl struct {
 }
 
 func (c *clientImpl) GetExtCacheData() (string, string) {
-	return c.externalCacheHost, c.externalCachePath
+	path := c.externalCachePath
+	if path == "/" {
+		// Only the slash for the path, remove it to empty
+		path = ""
+	} else if len(path) > 0 && !strings.HasPrefix(path, "/") {
+		// Path defined but does not start with "/", prepend it
+		path = "/" + path
+	}
+
+	return c.externalCacheHost, path
 }
 
 func (c *clientImpl) PutJson(ctx context.Context, values []Cacheable) (uuids []string, errs []error) {
@@ -101,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
+}
diff --git a/static/bidder-info/audienceNetwork.yaml b/static/bidder-info/audienceNetwork.yaml
index fb03c684..34700f2f 100644
--- a/static/bidder-info/audienceNetwork.yaml
+++ b/static/bidder-info/audienceNetwork.yaml
@@ -5,3 +5,9 @@ capabilities:
     mediaTypes:
       - banner
       - video
+      - native
+  app:
+    mediaTypes:
+      - banner
+      - video
+      - native
diff --git a/static/bidder-info/triplelift_native.yaml b/static/bidder-info/triplelift_native.yaml
new file mode 100644
index 00000000..abe91659
--- /dev/null
+++ b/static/bidder-info/triplelift_native.yaml
@@ -0,0 +1,9 @@
+maintainer:
+  email: "prebid@triplelift.com"
+capabilities:
+  app:
+    mediaTypes:
+      - native
+  site:
+    mediaTypes:
+      - native
diff --git a/static/bidder-params/triplelift_native.json b/static/bidder-params/triplelift_native.json
new file mode 100644
index 00000000..c81230aa
--- /dev/null
+++ b/static/bidder-params/triplelift_native.json
@@ -0,0 +1,21 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Triplelift Adapter Params",
+  "description": "A schema which validates params accepted by the Triplelift adapter",
+
+  "type": "object",
+  "properties": {
+    "inventoryCode": {
+      "type": "string",
+      "description": "TripleLift inventory code for this ad unit (provided to you by your partner manager)"
+    },
+    "floor" :  {"description" : "the bid floor, in usd", "type": "number" }
+  },
+  "oneOf": [{
+    "oneOf": [{
+      "required": ["inventoryCode"]
+    }] }],
+  "not": {
+    "required": ["floor"]
+  }
+}
diff --git a/usersync/cookie.go b/usersync/cookie.go
index 6ced4b62..13aa7f38 100644
--- a/usersync/cookie.go
+++ b/usersync/cookie.go
@@ -193,19 +193,24 @@ func (cookie *PBSCookie) SetCookieOnResponse(w http.ResponseWriter, setSiteCooki
 		currSize = len([]byte(httpCookie.String()))
 	}
 
-	uidsCookieStr := httpCookie.String()
+	var uidsCookieStr string
 	var sameSiteCookie *http.Cookie
 	if setSiteCookie {
+		httpCookie.Secure = true
+		uidsCookieStr = httpCookie.String()
 		uidsCookieStr += SameSiteAttribute
 		sameSiteCookie = &http.Cookie{
 			Name:    SameSiteCookieName,
 			Value:   SameSiteCookieValue,
 			Expires: time.Now().Add(ttl),
 			Path:    "/",
+			Secure:  true,
 		}
 		sameSiteCookieStr := sameSiteCookie.String()
 		sameSiteCookieStr += SameSiteAttribute
 		w.Header().Add("Set-Cookie", sameSiteCookieStr)
+	} else {
+		uidsCookieStr = httpCookie.String()
 	}
 	w.Header().Add("Set-Cookie", uidsCookieStr)
 }
diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go
index 250f5a62..7a43fe83 100644
--- a/usersync/usersyncers/syncer.go
+++ b/usersync/usersyncers/syncer.go
@@ -43,6 +43,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/sovrn"
 	"github.com/prebid/prebid-server/adapters/synacormedia"
 	"github.com/prebid/prebid-server/adapters/triplelift"
+	"github.com/prebid/prebid-server/adapters/triplelift_native"
 	"github.com/prebid/prebid-server/adapters/unruly"
 	"github.com/prebid/prebid-server/adapters/verizonmedia"
 	"github.com/prebid/prebid-server/adapters/visx"
@@ -95,6 +96,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderSonobi, sonobi.NewSonobiSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderSynacormedia, synacormedia.NewSynacorMediaSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderTriplelift, triplelift.NewTripleliftSyncer)
+	insertIntoMap(cfg, syncers, openrtb_ext.BidderTripleliftNative, triplelift_native.NewTripleliftSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderVrtcal, vrtcal.NewVrtcalSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderYieldmo, yieldmo.NewYieldmoSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderVisx, visx.NewVisxSyncer)


Test patch:
diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json
index d0a85aa6..632629b5 100644
--- a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json
+++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json
@@ -43,9 +43,6 @@
                     "Accept": [
                         "application/json"
                     ],
-                    "Auth-Token": [
-                        "test-app-id|test-app-secret"
-                    ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
                     ],
diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json
index d31714ec..630e26d3 100644
--- a/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json
+++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json
@@ -44,9 +44,6 @@
                     "Accept": [
                         "application/json"
                     ],
-                    "Auth-Token": [
-                        "test-app-id|test-app-secret"
-                    ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
                     ],
diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json
index 2223422c..288c7c14 100644
--- a/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json
+++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json
@@ -37,9 +37,6 @@
                     "Accept": [
                         "application/json"
                     ],
-                    "Auth-Token": [
-                        "test-app-id|test-app-secret"
-                    ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
                     ],
diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json
index 070bcabf..15563c2a 100644
--- a/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json
+++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json
@@ -42,9 +42,6 @@
                     "Accept": [
                         "application/json"
                     ],
-                    "Auth-Token": [
-                        "test-app-id|test-app-secret"
-                    ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
                     ],
diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json
index 4fd1eb33..0fe836af 100644
--- a/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json
+++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json
@@ -62,9 +62,6 @@
                     "Accept": [
                         "application/json"
                     ],
-                    "Auth-Token": [
-                        "test-app-id|test-app-secret"
-                    ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
                     ],
@@ -142,9 +139,6 @@
                     "Accept": [
                         "application/json"
                     ],
-                    "Auth-Token": [
-                        "test-app-id|test-app-secret"
-                    ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
                     ],
diff --git a/adapters/audienceNetwork/facebook_test.go b/adapters/audienceNetwork/facebook_test.go
index 2f685234..2ce0ef3b 100644
--- a/adapters/audienceNetwork/facebook_test.go
+++ b/adapters/audienceNetwork/facebook_test.go
@@ -38,5 +38,5 @@ type FacebookExt struct {
 }
 
 func TestJsonSamples(t *testing.T) {
-	adapterstest.RunJSONBidderTest(t, "audienceNetworktest", NewFacebookBidder(nil, "test-platform-id", "test-app-id", "test-app-secret"))
+	adapterstest.RunJSONBidderTest(t, "audienceNetworktest", NewFacebookBidder(nil, "test-platform-id", "test-app-secret"))
 }
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/adapters/triplelift_native/triplelift_native_test.go b/adapters/triplelift_native/triplelift_native_test.go
new file mode 100644
index 00000000..0beb18ed
--- /dev/null
+++ b/adapters/triplelift_native/triplelift_native_test.go
@@ -0,0 +1,23 @@
+package triplelift_native
+
+import (
+	"fmt"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestBadConfig(t *testing.T) {
+	badServer := NewTripleliftNativeBidder(nil, "", "{foo:2}")
+	assert.NotEmpty(t, badServer, "NewTripleliftBidder should not return nil")
+	expected := &adapters.MisconfiguredBidder{
+		Name:  "TripleliftNativeAdapter",
+		Error: fmt.Errorf("TripleliftNativeAdapter could not unmarshal config json"),
+	}
+	assert.IsType(t, expected, badServer, "expected misconfigured server")
+}
+
+func TestJsonSamples(t *testing.T) {
+	adapterstest.RunJSONBidderTest(t, "triplelift_nativetest", NewTripleliftNativeBidder(nil, "http://tlx.3lift.net/s2s/auction?supplier_id=19", "{\"publisher_whitelist\":[\"foo\",\"bar\",\"baz\"], \"endpoint\":\"http://tlx.3lift.net/s2sn/auction?supplier_id=20\"}"))
+}
diff --git a/adapters/triplelift_native/triplelift_nativetest/exemplary/optional-params.json b/adapters/triplelift_native/triplelift_nativetest/exemplary/optional-params.json
new file mode 100644
index 00000000..d25f72e2
--- /dev/null
+++ b/adapters/triplelift_native/triplelift_nativetest/exemplary/optional-params.json
@@ -0,0 +1,55 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "site": { "publisher": {"id":"foo","name":"foo"}},
+    "imp": [
+      {
+   "native":{
+    "request" : "{\"plcmtcnt\":1,\"plcmttype\":2,\"privacy\":1,\"context\":1,\"contextsubtype\":12,\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]},{\"event\":2,\"methods\":[1]}],\"assets\":[{\"data\":{\"type\":12},\"required\":1},{\"title\":{\"len\":50},\"required\":1},{\"img\":{\"w\":80,\"h\":80,\"type\":1},\"required\":1},{\"img\":{\"w\":1200,\"h\":627,\"type\":3},\"required\":1},{\"data\":{\"type\":3},\"required\":0},{\"data\":{\"len\":100,\"type\":2},\"required\":1},{\"video\":{\"mimes\":[\"video/mpeg\",\"video/mp4\"],\"minduration\":2,\"protocols\":[2,5],\"maxduration\":2,\"ext\":{\"playbackmethod\":[1,2]}},\"required\":1}],\"ver\":\"1.2\"}"
+   },
+        "id": "test-imp-id",
+        "ext": {
+          "bidder": {
+            "inventoryCode": "foo",
+            "floor" : 20
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://tlx.3lift.net/s2sn/auction?supplier_id=20",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "native": {
+    "request" : "{\"plcmtcnt\":1,\"plcmttype\":2,\"privacy\":1,\"context\":1,\"contextsubtype\":12,\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]},{\"event\":2,\"methods\":[1]}],\"assets\":[{\"data\":{\"type\":12},\"required\":1},{\"title\":{\"len\":50},\"required\":1},{\"img\":{\"w\":80,\"h\":80,\"type\":1},\"required\":1},{\"img\":{\"w\":1200,\"h\":627,\"type\":3},\"required\":1},{\"data\":{\"type\":3},\"required\":0},{\"data\":{\"len\":100,\"type\":2},\"required\":1},{\"video\":{\"mimes\":[\"video/mpeg\",\"video/mp4\"],\"minduration\":2,\"protocols\":[2,5],\"maxduration\":2,\"ext\":{\"playbackmethod\":[1,2]}},\"required\":1}],\"ver\":\"1.2\"}"
+              },
+              "tagid": "foo",
+              "bidfloor": 20,
+              "ext": {
+                "bidder": {
+                  "inventoryCode": "foo",
+                   "floor" : 20
+                }
+              }
+            }
+          ],
+        "site": {
+            "publisher": {
+              "id": "foo",
+              "name": "foo"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 204
+      }
+    }
+  ]
+}
diff --git a/adapters/triplelift_native/triplelift_nativetest/supplemental/badext.json b/adapters/triplelift_native/triplelift_nativetest/supplemental/badext.json
new file mode 100644
index 00000000..7df178bd
--- /dev/null
+++ b/adapters/triplelift_native/triplelift_nativetest/supplemental/badext.json
@@ -0,0 +1,36 @@
+{
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "json: cannot unmarshal string into Go value of type adapters.ExtImpBidder",
+      "comparison": "literal"
+    },
+    {
+      "value": "No valid impressions for triplelift",
+      "comparison": "literal"
+    }
+  ],
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "app": { "publisher": {"ext":{"prebid":{"parentAccount":"foo"}}, "id":"far","name":"bar"}},
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext":  "aaa"      
+    }
+    ]
+  },
+  "httpCalls": [
+  ]
+}
diff --git a/adapters/triplelift_native/triplelift_nativetest/supplemental/badextbidder.json b/adapters/triplelift_native/triplelift_nativetest/supplemental/badextbidder.json
new file mode 100644
index 00000000..59ebe048
--- /dev/null
+++ b/adapters/triplelift_native/triplelift_nativetest/supplemental/badextbidder.json
@@ -0,0 +1,38 @@
+{
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "json: cannot unmarshal string into Go value of type openrtb_ext.ExtImpTriplelift",
+      "comparison": "literal"
+    },
+    {
+      "value": "No valid impressions for triplelift",
+      "comparison": "literal"
+    }
+  ],
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "site": { "publisher": {"id":"foo","name":"foo"}},
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": "aaa"
+        }
+      }
+    ]
+  },
+  "httpCalls":[],
+  "expectedBidResponses": []
+}
diff --git a/adapters/triplelift_native/triplelift_nativetest/supplemental/badlyformed.json b/adapters/triplelift_native/triplelift_nativetest/supplemental/badlyformed.json
new file mode 100644
index 00000000..c38e9c91
--- /dev/null
+++ b/adapters/triplelift_native/triplelift_nativetest/supplemental/badlyformed.json
@@ -0,0 +1,19 @@
+{
+  "expectedMakeRequestsErrors" : [{"comparison":"literal","value": "no native object specified"},{"comparison":"literal","value":"No valid impressions for triplelift"}],
+  "mockBidRequest": {
+    "site": { "publisher": {"id":"foo","name":"foo"}},
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "ext": {
+          "bidder": {
+            "inventoryCode": "aa" 
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+  ]
+}
diff --git a/adapters/triplelift_native/triplelift_nativetest/supplemental/badresponseext.json b/adapters/triplelift_native/triplelift_nativetest/supplemental/badresponseext.json
new file mode 100644
index 00000000..c9bb90f8
--- /dev/null
+++ b/adapters/triplelift_native/triplelift_nativetest/supplemental/badresponseext.json
@@ -0,0 +1,103 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "site": { "publisher": {"id":"foo","name":"foo"}},
+    "imp": [
+      {
+        "id": "test-imp-id",
+   "native":{
+    "request" : "{\"plcmtcnt\":1,\"plcmttype\":2,\"privacy\":1,\"context\":1,\"contextsubtype\":12,\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]},{\"event\":2,\"methods\":[1]}],\"assets\":[{\"data\":{\"type\":12},\"required\":1},{\"title\":{\"len\":50},\"required\":1},{\"img\":{\"w\":80,\"h\":80,\"type\":1},\"required\":1},{\"img\":{\"w\":1200,\"h\":627,\"type\":3},\"required\":1},{\"data\":{\"type\":3},\"required\":0},{\"data\":{\"len\":100,\"type\":2},\"required\":1},{\"video\":{\"mimes\":[\"video/mpeg\",\"video/mp4\"],\"minduration\":2,\"protocols\":[2,5],\"maxduration\":2,\"ext\":{\"playbackmethod\":[1,2]}},\"required\":1}],\"ver\":\"1.2\"}"
+   },
+        "ext": {
+          "bidder": {
+            "inventoryCode": "aa" 
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://tlx.3lift.net/s2sn/auction?supplier_id=20",
+        "body": {
+          "site": { "publisher": {"id":"foo","name":"foo"}},
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "native" : {
+    "request" : "{\"plcmtcnt\":1,\"plcmttype\":2,\"privacy\":1,\"context\":1,\"contextsubtype\":12,\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]},{\"event\":2,\"methods\":[1]}],\"assets\":[{\"data\":{\"type\":12},\"required\":1},{\"title\":{\"len\":50},\"required\":1},{\"img\":{\"w\":80,\"h\":80,\"type\":1},\"required\":1},{\"img\":{\"w\":1200,\"h\":627,\"type\":3},\"required\":1},{\"data\":{\"type\":3},\"required\":0},{\"data\":{\"len\":100,\"type\":2},\"required\":1},{\"video\":{\"mimes\":[\"video/mpeg\",\"video/mp4\"],\"minduration\":2,\"protocols\":[2,5],\"maxduration\":2,\"ext\":{\"playbackmethod\":[1,2]}},\"required\":1}],\"ver\":\"1.2\"}"
+              },
+              "tagid" : "aa",
+              "ext": {
+                "bidder": {
+                  "inventoryCode": "aa"
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "958",
+              "bid": [
+                {
+                  "id": "7706636740145184841",
+                  "impid": "test-imp-id",
+                  "price": 0.5,
+                  "adid": "29681110",
+                  "adm": "some-test-ad",
+                  "adomain": [
+                    "triplelift.com"
+                  ],
+                  "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
+                  "cid": "958",
+                  "crid": "29681110",
+                  "h": 250,
+                  "w": 300,
+                  "ext": "aaa"                 
+                }
+              ]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [ {"id": "test-request-id",
+            "type":"native",
+              "bid": 
+                {
+                  "id": "7706636740145184841",
+                  "impid": "test-imp-id",
+                  "price": 0.5,
+                  "adid": "29681110",
+                  "adm": "some-test-ad",
+                  "adomain": [
+                    "triplelift.com"
+                  ],
+                  "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
+                  "cid": "958",
+                  "crid": "29681110",
+                  "h": 250,
+                  "w": 300,
+                  "ext": "aaa"
+                }
+              ,
+          "bidid": "5778926625248726496",
+          "cur": "USD" }
+      ]
+    }
+  ]
+}
diff --git a/adapters/triplelift_native/triplelift_nativetest/supplemental/badstatuscode.json b/adapters/triplelift_native/triplelift_nativetest/supplemental/badstatuscode.json
new file mode 100644
index 00000000..175ec038
--- /dev/null
+++ b/adapters/triplelift_native/triplelift_nativetest/supplemental/badstatuscode.json
@@ -0,0 +1,50 @@
+{
+  "mockBidRequest": {
+    "site": { "publisher": {"id":"foo","name":"foo"}},
+    "id": "test-request-id",
+    "imp": [
+      {
+   "native":{
+    "request" : "{\"plcmtcnt\":1,\"plcmttype\":2,\"privacy\":1,\"context\":1,\"contextsubtype\":12,\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]},{\"event\":2,\"methods\":[1]}],\"assets\":[{\"data\":{\"type\":12},\"required\":1},{\"title\":{\"len\":50},\"required\":1},{\"img\":{\"w\":80,\"h\":80,\"type\":1},\"required\":1},{\"img\":{\"w\":1200,\"h\":627,\"type\":3},\"required\":1},{\"data\":{\"type\":3},\"required\":0},{\"data\":{\"len\":100,\"type\":2},\"required\":1},{\"video\":{\"mimes\":[\"video/mpeg\",\"video/mp4\"],\"minduration\":2,\"protocols\":[2,5],\"maxduration\":2,\"ext\":{\"playbackmethod\":[1,2]}},\"required\":1}],\"ver\":\"1.2\"}"
+   },
+        "id": "test-imp-id",
+        "ext": {
+          "bidder": {
+            "inventoryCode": "aa" 
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://tlx.3lift.net/s2sn/auction?supplier_id=20",
+        "body": {
+          "site": { "publisher": {"id":"foo","name":"foo"}},
+          "id": "test-request-id",
+          "imp": [
+            {
+   "native":{
+    "request" : "{\"plcmtcnt\":1,\"plcmttype\":2,\"privacy\":1,\"context\":1,\"contextsubtype\":12,\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]},{\"event\":2,\"methods\":[1]}],\"assets\":[{\"data\":{\"type\":12},\"required\":1},{\"title\":{\"len\":50},\"required\":1},{\"img\":{\"w\":80,\"h\":80,\"type\":1},\"required\":1},{\"img\":{\"w\":1200,\"h\":627,\"type\":3},\"required\":1},{\"data\":{\"type\":3},\"required\":0},{\"data\":{\"len\":100,\"type\":2},\"required\":1},{\"video\":{\"mimes\":[\"video/mpeg\",\"video/mp4\"],\"minduration\":2,\"protocols\":[2,5],\"maxduration\":2,\"ext\":{\"playbackmethod\":[1,2]}},\"required\":1}],\"ver\":\"1.2\"}"
+   },
+              "id": "test-imp-id",
+              "tagid" : "aa",
+              "ext": {
+                "bidder": {
+                  "inventoryCode": "aa"
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 400
+      }
+    }
+  ],
+  "expectedBidResponses": [
+  ],
+  "expectedMakeBidsErrors": [{"comparison":"literal", "value" : "Unexpected status code: 400. Run with request.debug = 1 for more info"}]
+}
diff --git a/adapters/triplelift_native/triplelift_nativetest/supplemental/nopub.json b/adapters/triplelift_native/triplelift_nativetest/supplemental/nopub.json
new file mode 100644
index 00000000..cdeaff39
--- /dev/null
+++ b/adapters/triplelift_native/triplelift_nativetest/supplemental/nopub.json
@@ -0,0 +1,37 @@
+{
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Unsupported publisher for triplelift_native",
+      "comparison": "literal"
+    }
+  ],
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "app": { "publisher": {"ext":{"prebid":{"parentAccount":"faz"}}, "id":"far","name":"bar"}},
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "inventoryCode": "foo",
+            "floor": 20
+          }
+        }
+    }
+    ]
+  },
+  "httpCalls": [
+  ]
+}
diff --git a/adapters/triplelift_native/triplelift_nativetest/supplemental/notgoodstatuscode.json b/adapters/triplelift_native/triplelift_nativetest/supplemental/notgoodstatuscode.json
new file mode 100644
index 00000000..ce40d36f
--- /dev/null
+++ b/adapters/triplelift_native/triplelift_nativetest/supplemental/notgoodstatuscode.json
@@ -0,0 +1,86 @@
+{
+  "mockBidRequest": {
+    "site": { "publisher": {"id":"foo","name":"foo"}},
+    "id": "test-request-id",
+    "imp": [
+      {
+   "native":{
+    "request" : "{\"plcmtcnt\":1,\"plcmttype\":2,\"privacy\":1,\"context\":1,\"contextsubtype\":12,\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]},{\"event\":2,\"methods\":[1]}],\"assets\":[{\"data\":{\"type\":12},\"required\":1},{\"title\":{\"len\":50},\"required\":1},{\"img\":{\"w\":80,\"h\":80,\"type\":1},\"required\":1},{\"img\":{\"w\":1200,\"h\":627,\"type\":3},\"required\":1},{\"data\":{\"type\":3},\"required\":0},{\"data\":{\"len\":100,\"type\":2},\"required\":1},{\"video\":{\"mimes\":[\"video/mpeg\",\"video/mp4\"],\"minduration\":2,\"protocols\":[2,5],\"maxduration\":2,\"ext\":{\"playbackmethod\":[1,2]}},\"required\":1}],\"ver\":\"1.2\"}"
+   },
+        "id": "test-imp-id",
+        "ext": {
+          "bidder": {
+            "inventoryCode": "aa"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://tlx.3lift.net/s2sn/auction?supplier_id=20",
+        "body": {
+          "site": { "publisher": {"id":"foo","name":"foo"}},
+          "id": "test-request-id",
+          "imp": [
+            {
+   "native":{
+    "request" : "{\"plcmtcnt\":1,\"plcmttype\":2,\"privacy\":1,\"context\":1,\"contextsubtype\":12,\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]},{\"event\":2,\"methods\":[1]}],\"assets\":[{\"data\":{\"type\":12},\"required\":1},{\"title\":{\"len\":50},\"required\":1},{\"img\":{\"w\":80,\"h\":80,\"type\":1},\"required\":1},{\"img\":{\"w\":1200,\"h\":627,\"type\":3},\"required\":1},{\"data\":{\"type\":3},\"required\":0},{\"data\":{\"len\":100,\"type\":2},\"required\":1},{\"video\":{\"mimes\":[\"video/mpeg\",\"video/mp4\"],\"minduration\":2,\"protocols\":[2,5],\"maxduration\":2,\"ext\":{\"playbackmethod\":[1,2]}},\"required\":1}],\"ver\":\"1.2\"}"
+   },
+              "id": "test-imp-id",
+              "tagid" : "aa",
+              "ext": {
+                "bidder": {
+                  "inventoryCode": "aa"
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 302,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "958",
+              "bid": [
+                {
+                  "id": "7706636740145184841",
+                  "impid": "test-imp-id",
+                  "price": 0.5,
+                  "adid": "29681110",
+                  "adm": "some-test-ad",
+                  "adomain": [
+                    "triplelift.com"
+                  ],
+                  "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
+                  "cid": "958",
+                  "crid": "29681110",
+                  "h": 250,
+                  "w": 300,
+                  "ext": {
+                    "triplelift_pb": {
+                      "format": 2
+                    }
+                  }
+                }
+              ]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 302. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/triplelift_native/usersync_test.go b/adapters/triplelift_native/usersync_test.go
new file mode 100644
index 00000000..85610f61
--- /dev/null
+++ b/adapters/triplelift_native/usersync_test.go
@@ -0,0 +1,19 @@
+package triplelift_native
+
+import (
+	"testing"
+	"text/template"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestTripleliftSyncer(t *testing.T) {
+	temp := template.Must(template.New("sync-template").Parse("//eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&redir=https%3A%2F%2Feb2.3lift.com%2Fsetuid%3Fbidder%3Dtriplelift%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID"))
+	syncer := NewTripleliftSyncer(temp)
+	syncInfo, err := syncer.GetUsersyncInfo("", "")
+	assert.NoError(t, err)
+	assert.Equal(t, "//eb2.3lift.com/getuid?gdpr=&cmp_cs=&redir=https%3A%2F%2Feb2.3lift.com%2Fsetuid%3Fbidder%3Dtriplelift%26gdpr%3D%26gdpr_consent%3D%26uid%3D%24UID", syncInfo.URL)
+	assert.Equal(t, "redirect", syncInfo.Type)
+	assert.EqualValues(t, 28, 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/docs/developers/automated-tests.md b/docs/developers/automated-tests.md
index c8c19d3d..12532237 100644
--- a/docs/developers/automated-tests.md
+++ b/docs/developers/automated-tests.md
@@ -1,8 +1,5 @@
 # Automated Tests
 
-**NOTE**: The automated tests will not pass on Go 1.11 due to [this bug](https://github.com/golang/go/issues/27275) in Go.
-This issue has been fixed in Go 1.11.1.
-
 This project uses [TravisCI](https://travis-ci.org/) to make sure that every PR passes automated tests.
 To reproduce these tests locally, use:
 
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/auction_test.go b/exchange/auction_test.go
index 925fc570..ea19732d 100644
--- a/exchange/auction_test.go
+++ b/exchange/auction_test.go
@@ -253,6 +253,9 @@ type cacheComparator struct {
 func (c *mockCache) GetExtCacheData() (string, string) {
 	return "", ""
 }
+func (c *mockCache) GetPutUrl() string {
+	return ""
+}
 func (c *mockCache) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) ([]string, []error) {
 	c.items = values
 	return []string{"", "", "", "", ""}, nil
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 39497d61..2b0e37e6 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -27,6 +27,7 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbsmetrics"
 	metricsConf "github.com/prebid/prebid-server/pbsmetrics/config"
+	pbc "github.com/prebid/prebid-server/prebid_cache_client"
 	"github.com/rcrowley/go-metrics"
 	"github.com/stretchr/testify/assert"
 	"github.com/yudai/gojsondiff"
@@ -125,7 +126,7 @@ func TestCharacterEscape(t *testing.T) {
 	var errList []error
 
 	/* 	4) Build bid response 									*/
-	bid_resp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, errList)
+	bidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, nil, errList)
 
 	/* 	5) Assert we have no errors and one '&' character as we are supposed to 	*/
 	if err != nil {
@@ -134,11 +135,232 @@ func TestCharacterEscape(t *testing.T) {
 	if len(errList) > 0 {
 		t.Errorf("exchange.buildBidResponse returned %d errors", len(errList))
 	}
-	if bytes.Contains(bid_resp.Ext, []byte("u0026")) {
-		t.Errorf("exchange.buildBidResponse() did not correctly print the '&' characters %s", string(bid_resp.Ext))
+	if bytes.Contains(bidResp.Ext, []byte("u0026")) {
+		t.Errorf("exchange.buildBidResponse() did not correctly print the '&' characters %s", string(bidResp.Ext))
 	}
 }
 
+func TestGetBidCacheInfo(t *testing.T) {
+	testUUID := "CACHE_UUID_1234"
+	testExternalCacheHost := "https://www.externalprebidcache.net"
+	testExternalCachePath := "endpoints/cache"
+
+	/* 1) An adapter 											*/
+	bidderName := openrtb_ext.BidderName("appnexus")
+
+	cfg := &config.Configuration{
+		Adapters: map[string]config.Adapter{
+			string(bidderName): {
+				Endpoint: "http://ib.adnxs.com/endpoint",
+			},
+		},
+		CacheURL: config.Cache{
+			Host: "www.internalprebidcache.net",
+		},
+		ExtCacheURL: config.ExternalCache{
+			Host: testExternalCacheHost,
+			Path: testExternalCachePath,
+		},
+	}
+	adapterList := make([]openrtb_ext.BidderName, 0, 2)
+	testEngine := metricsConf.NewMetricsEngine(cfg, adapterList)
+
+	/* 	2) Init new exchange with said configuration			*/
+	handlerNoBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) }
+	server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer))
+	defer server.Close()
+
+	e := NewExchange(server.Client(), pbc.NewClient(&cfg.CacheURL, &cfg.ExtCacheURL, testEngine), cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencies.NewRateConverterDefault()).(*exchange)
+
+	/* 	3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs */
+	liveAdapters := []openrtb_ext.BidderName{bidderName}
+
+	//adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid,
+	bids := []*openrtb.Bid{
+		{
+			ID:             "some-imp-id",
+			ImpID:          "",
+			Price:          9.517803,
+			NURL:           "",
+			BURL:           "",
+			LURL:           "",
+			AdM:            "",
+			AdID:           "",
+			ADomain:        nil,
+			Bundle:         "",
+			IURL:           "",
+			CID:            "",
+			CrID:           "",
+			Tactic:         "",
+			Cat:            nil,
+			Attr:           nil,
+			API:            0,
+			Protocol:       0,
+			QAGMediaRating: 0,
+			Language:       "",
+			DealID:         "",
+			W:              300,
+			H:              250,
+			WRatio:         0,
+			HRatio:         0,
+			Exp:            0,
+			Ext:            nil,
+		},
+	}
+	auc := &auction{
+		cacheIds: map[*openrtb.Bid]string{
+			bids[0]: testUUID,
+		},
+	}
+	aPbsOrtbBidArr := []*pbsOrtbBid{
+		{
+			bid:     bids[0],
+			bidType: openrtb_ext.BidTypeBanner,
+			bidTargets: map[string]string{
+				"pricegranularity":  "med",
+				"includewinners":    "true",
+				"includebidderkeys": "false",
+			},
+		},
+	}
+	adapterBids := map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+		bidderName: {
+			bids:     aPbsOrtbBidArr,
+			currency: "USD",
+		},
+	}
+
+	//resolvedRequest json.RawMessage
+	resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`)
+
+	//adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra,
+	adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{
+		bidderName: {
+			ResponseTimeMillis: 5,
+			Errors: []openrtb_ext.ExtBidderError{
+				{
+					Code:    999,
+					Message: "Post ib.adnxs.com/openrtb2?query1&query2: unsupported protocol scheme \"\"",
+				},
+			},
+		},
+	}
+	bidRequest := &openrtb.BidRequest{
+		ID:   "some-request-id",
+		TMax: 1000,
+		Imp: []openrtb.Imp{
+			{
+				ID:     "test-div",
+				Secure: openrtb.Int8Ptr(0),
+				Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}},
+				Ext: json.RawMessage(` {
+    "rubicon": {
+        "accountId": 1001,
+        "siteId": 113932,
+        "zoneId": 535510
+    },
+    "appnexus": { "placementId": 10433394 },
+    "pubmatic": { "publisherId": "156209", "adSlot": "pubmatic_test2@300x250" },
+    "pulsepoint": { "cf": "300X250", "cp": 512379, "ct": 486653 },
+    "conversant": { "site_id": "108060" },
+    "ix": { "siteId": "287415" }
+}`),
+			},
+		},
+		Site: &openrtb.Site{
+			Page:      "http://rubitest.com/index.html",
+			Publisher: &openrtb.Publisher{ID: "1001"},
+		},
+		Test: 1,
+		Ext:  json.RawMessage(`{"prebid": { "cache": { "bids": {}, "vastxml": {} }, "targeting": { "pricegranularity": "med", "includewinners": true, "includebidderkeys": false } }}`),
+	}
+
+	var errList []error
+
+	/* 	4) Build bid response 									*/
+	bid_resp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, auc, errList)
+
+	/* 	5) Assert we have no errors and the bid response we expected*/
+	assert.NoError(t, err, "[TestGetBidCacheInfo] buildBidResponse() threw an error")
+
+	expectedBidResponse := &openrtb.BidResponse{
+		SeatBid: []openrtb.SeatBid{
+			{
+				Seat: string(bidderName),
+				Bid: []openrtb.Bid{
+					{
+						Ext: json.RawMessage(`{ "prebid": { "cache": { "bids": { "cacheId": "` + testUUID + `", "url": "` + testExternalCacheHost + `/` + testExternalCachePath + `?uuid=` + testUUID + `" }, "key": "", "url": "" }`),
+					},
+				},
+			},
+		},
+	}
+	// compare cache UUID
+	expCacheUUID, err := jsonparser.GetString(expectedBidResponse.SeatBid[0].Bid[0].Ext, "prebid", "cache", "bids", "cacheId")
+	assert.NoErrorf(t, err, "[TestGetBidCacheInfo] Error found while trying to json parse the cacheId field from expected build response. Message: %v \n", err)
+
+	cacheUUID, err := jsonparser.GetString(bid_resp.SeatBid[0].Bid[0].Ext, "prebid", "cache", "bids", "cacheId")
+	assert.NoErrorf(t, err, "[TestGetBidCacheInfo] bid_resp.SeatBid[0].Bid[0].Ext = %s \n", bid_resp.SeatBid[0].Bid[0].Ext)
+
+	assert.Equal(t, expCacheUUID, cacheUUID, "[TestGetBidCacheInfo] cacheId field in ext should equal \"%s\" \n", expCacheUUID)
+
+	// compare cache UUID
+	expCacheURL, err := jsonparser.GetString(expectedBidResponse.SeatBid[0].Bid[0].Ext, "prebid", "cache", "bids", "url")
+	assert.NoErrorf(t, err, "[TestGetBidCacheInfo] Error found while trying to json parse the url field from expected build response. Message: %v \n", err)
+
+	cacheURL, err := jsonparser.GetString(bid_resp.SeatBid[0].Bid[0].Ext, "prebid", "cache", "bids", "url")
+	assert.NoErrorf(t, err, "[TestGetBidCacheInfo] Error found while trying to json parse the url field from actual build response. Message: %v \n", err)
+
+	assert.Equal(t, expCacheURL, cacheURL, "[TestGetBidCacheInfo] cacheId field in ext should equal \"%s\" \n", expCacheURL)
+}
+
+func buildBidResponseParams(bidderName openrtb_ext.BidderName, bidRequest *openrtb.BidRequest) ([]openrtb_ext.BidderName, map[openrtb_ext.BidderName]*pbsOrtbSeatBid, json.RawMessage, map[openrtb_ext.BidderName]*seatResponseExtra) {
+	//liveAdapters []openrtb_ext.BidderName,
+	liveAdapters := []openrtb_ext.BidderName{bidderName}
+
+	//adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid,
+	adapterBids := map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+		bidderName: {
+			bids: []*pbsOrtbBid{
+				{
+					bid: &openrtb.Bid{
+						ID:    "some-imp-id",
+						Price: 9.517803,
+						W:     300,
+						H:     250,
+					},
+					bidType: openrtb_ext.BidTypeBanner,
+					bidTargets: map[string]string{
+						"pricegranularity":  "med",
+						"includewinners":    "true",
+						"includebidderkeys": "false",
+					},
+				},
+			},
+			currency: "USD",
+			//ext:      bidRequest.Ext,
+		},
+	}
+
+	//resolvedRequest json.RawMessage
+	resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`)
+
+	//adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra,
+	adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{
+		bidderName: {
+			ResponseTimeMillis: 5,
+			Errors: []openrtb_ext.ExtBidderError{
+				{
+					Code:    999,
+					Message: "Post ib.adnxs.com/openrtb2?query1&query2: unsupported protocol scheme \"\"",
+				},
+			},
+		},
+	}
+
+	return liveAdapters, adapterBids, resolvedRequest, adapterExtra
+}
+
 // TestRaceIntegration runs an integration test using all the sample params from
 // adapters/{bidder}/{bidder}test/params/race/*.json files.
 //
diff --git a/exchange/exchangetest/targeting-cache-vast.json b/exchange/exchangetest/targeting-cache-vast.json
index d18dfd6c..f348dd1b 100644
--- a/exchange/exchangetest/targeting-cache-vast.json
+++ b/exchange/exchangetest/targeting-cache-vast.json
@@ -64,6 +64,14 @@
             "crid": "creative-1",
             "ext": {
               "prebid": {
+                "cache": {
+                  "bids": {
+                    "cacheId": "0",
+                    "url": "www.pbcserver.com/pbcache/endpoint?uuid=0"
+                  },
+                  "key": "",
+                  "url": ""
+                },
                 "type": "video",
                 "targeting": {
                   "hb_bidder": "appnexus",
diff --git a/exchange/exchangetest/targeting-cache-zero.json b/exchange/exchangetest/targeting-cache-zero.json
index 51a38941..51301530 100644
--- a/exchange/exchangetest/targeting-cache-zero.json
+++ b/exchange/exchangetest/targeting-cache-zero.json
@@ -67,6 +67,14 @@
             "crid": "creative-1",
             "ext": {
               "prebid": {
+                "cache": {
+                  "bids": {
+                    "cacheId": "0",
+                    "url": "www.pbcserver.com/pbcache/endpoint?uuid=0"
+                  },
+                  "key": "",
+                  "url": ""
+                },
                 "type": "video",
                 "targeting": {
                   "hb_bidder": "appnexus",
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)
+}
diff --git a/usersync/cookie_test.go b/usersync/cookie_test.go
index a59f7def..ef2e9911 100644
--- a/usersync/cookie_test.go
+++ b/usersync/cookie_test.go
@@ -425,6 +425,9 @@ func TestSetCookieOnResponseForSameSiteNone(t *testing.T) {
 	if !strings.Contains(writtenCookie, "SSCookie=1") {
 		t.Error("Set-Cookie should contain SSCookie=1")
 	}
+	if !strings.Contains(writtenCookie, "; Secure;") {
+		t.Error("Set-Cookie should contain Secure")
+	}
 }
 
 func TestSetCookieOnResponseForOlderChromeVersion(t *testing.T) {
diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go
index 8d9cc644..1c4b3b07 100644
--- a/usersync/usersyncers/syncer_test.go
+++ b/usersync/usersyncers/syncer_test.go
@@ -14,49 +14,50 @@ func TestNewSyncerMap(t *testing.T) {
 	}
 	cfg := &config.Configuration{
 		Adapters: map[string]config.Adapter{
-			string(openrtb_ext.BidderAdform):         syncConfig,
-			string(openrtb_ext.BidderAdkernel):       syncConfig,
-			string(openrtb_ext.BidderAdkernelAdn):    syncConfig,
-			string(openrtb_ext.BidderAdpone):         syncConfig,
-			string(openrtb_ext.BidderAdtelligent):    syncConfig,
-			string(openrtb_ext.BidderAdvangelists):   syncConfig,
-			string(openrtb_ext.BidderAppnexus):       syncConfig,
-			string(openrtb_ext.BidderBeachfront):     syncConfig,
-			string(openrtb_ext.BidderFacebook):       syncConfig,
-			string(openrtb_ext.BidderBrightroll):     syncConfig,
-			string(openrtb_ext.BidderConsumable):     syncConfig,
-			string(openrtb_ext.BidderConversant):     syncConfig,
-			string(openrtb_ext.BidderDatablocks):     syncConfig,
-			string(openrtb_ext.BidderEmxDigital):     syncConfig,
-			string(openrtb_ext.BidderEPlanning):      syncConfig,
-			string(openrtb_ext.BidderGrid):           syncConfig,
-			string(openrtb_ext.BidderGumGum):         syncConfig,
-			string(openrtb_ext.BidderImprovedigital): syncConfig,
-			string(openrtb_ext.BidderIx):             syncConfig,
-			string(openrtb_ext.BidderLifestreet):     syncConfig,
-			string(openrtb_ext.BidderLockerDome):     syncConfig,
-			string(openrtb_ext.BidderMgid):           syncConfig,
-			string(openrtb_ext.BidderOpenx):          syncConfig,
-			string(openrtb_ext.BidderPubmatic):       syncConfig,
-			string(openrtb_ext.BidderPulsepoint):     syncConfig,
-			string(openrtb_ext.BidderRhythmone):      syncConfig,
-			string(openrtb_ext.BidderRTBHouse):       syncConfig,
-			string(openrtb_ext.BidderRubicon):        syncConfig,
-			string(openrtb_ext.BidderSharethrough):   syncConfig,
-			string(openrtb_ext.BidderSomoaudience):   syncConfig,
-			string(openrtb_ext.BidderSovrn):          syncConfig,
-			string(openrtb_ext.BidderSynacormedia):   syncConfig,
-			string(openrtb_ext.BidderTriplelift):     syncConfig,
-			string(openrtb_ext.Bidder33Across):       syncConfig,
-			string(openrtb_ext.BidderSonobi):         syncConfig,
-			string(openrtb_ext.BidderVrtcal):         syncConfig,
-			string(openrtb_ext.BidderYieldmo):        syncConfig,
-			string(openrtb_ext.BidderGamma):          syncConfig,
-			string(openrtb_ext.BidderVisx):           syncConfig,
-			string(openrtb_ext.BidderGamoshi):        syncConfig,
-			string(openrtb_ext.BidderUnruly):         syncConfig,
-			string(openrtb_ext.BidderVerizonMedia):   syncConfig,
-			string(openrtb_ext.BidderEngageBDR):      syncConfig,
+			string(openrtb_ext.BidderAdform):           syncConfig,
+			string(openrtb_ext.BidderAdkernel):         syncConfig,
+			string(openrtb_ext.BidderAdkernelAdn):      syncConfig,
+			string(openrtb_ext.BidderAdpone):           syncConfig,
+			string(openrtb_ext.BidderAdtelligent):      syncConfig,
+			string(openrtb_ext.BidderAdvangelists):     syncConfig,
+			string(openrtb_ext.BidderAppnexus):         syncConfig,
+			string(openrtb_ext.BidderBeachfront):       syncConfig,
+			string(openrtb_ext.BidderFacebook):         syncConfig,
+			string(openrtb_ext.BidderBrightroll):       syncConfig,
+			string(openrtb_ext.BidderConsumable):       syncConfig,
+			string(openrtb_ext.BidderConversant):       syncConfig,
+			string(openrtb_ext.BidderDatablocks):       syncConfig,
+			string(openrtb_ext.BidderEmxDigital):       syncConfig,
+			string(openrtb_ext.BidderEPlanning):        syncConfig,
+			string(openrtb_ext.BidderGrid):             syncConfig,
+			string(openrtb_ext.BidderGumGum):           syncConfig,
+			string(openrtb_ext.BidderImprovedigital):   syncConfig,
+			string(openrtb_ext.BidderIx):               syncConfig,
+			string(openrtb_ext.BidderLifestreet):       syncConfig,
+			string(openrtb_ext.BidderLockerDome):       syncConfig,
+			string(openrtb_ext.BidderMgid):             syncConfig,
+			string(openrtb_ext.BidderOpenx):            syncConfig,
+			string(openrtb_ext.BidderPubmatic):         syncConfig,
+			string(openrtb_ext.BidderPulsepoint):       syncConfig,
+			string(openrtb_ext.BidderRhythmone):        syncConfig,
+			string(openrtb_ext.BidderRTBHouse):         syncConfig,
+			string(openrtb_ext.BidderRubicon):          syncConfig,
+			string(openrtb_ext.BidderSharethrough):     syncConfig,
+			string(openrtb_ext.BidderSomoaudience):     syncConfig,
+			string(openrtb_ext.BidderSovrn):            syncConfig,
+			string(openrtb_ext.BidderSynacormedia):     syncConfig,
+			string(openrtb_ext.BidderTriplelift):       syncConfig,
+			string(openrtb_ext.BidderTripleliftNative): syncConfig,
+			string(openrtb_ext.Bidder33Across):         syncConfig,
+			string(openrtb_ext.BidderSonobi):           syncConfig,
+			string(openrtb_ext.BidderVrtcal):           syncConfig,
+			string(openrtb_ext.BidderYieldmo):          syncConfig,
+			string(openrtb_ext.BidderGamma):            syncConfig,
+			string(openrtb_ext.BidderVisx):             syncConfig,
+			string(openrtb_ext.BidderGamoshi):          syncConfig,
+			string(openrtb_ext.BidderUnruly):           syncConfig,
+			string(openrtb_ext.BidderVerizonMedia):     syncConfig,
+			string(openrtb_ext.BidderEngageBDR):        syncConfig,
 		},
 	}