Repository: prebid/prebid-server
Pull request #1400 (2020-07-15 15:14:00)

Privacy Request Metrics
Following up on #1360 to extend to CCPA, COPPA, and LMT for requests. Recreated due to sync issue with https://github.com/prebid/prebid-server/pull/1378.


Patch:
diff --git a/adapters/dmx/dmx.go b/adapters/dmx/dmx.go
index 6b4f698d..de33bd39 100644
--- a/adapters/dmx/dmx.go
+++ b/adapters/dmx/dmx.go
@@ -160,7 +160,7 @@ func (adapter *DmxAdapter) MakeRequests(request *openrtb.BidRequest, req *adapte
 	}
 
 	headers := http.Header{}
-	headers.Add("Content-Type", "Application/json;charset=utf-8")
+	headers.Add("Content-Type", "application/json;charset=utf-8")
 	reqBidder := &adapters.RequestData{
 		Method:  "POST",
 		Uri:     adapter.endpoint + addParams(sellerId), //adapter.endpoint,
diff --git a/config/config.go b/config/config.go
index 5d538f38..2e7f875b 100755
--- a/config/config.go
+++ b/config/config.go
@@ -793,7 +793,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.conversant.endpoint", "http://api.hb.ad.cpe.dotomi.com/cvx/server/hb/ortb/25")
 	v.SetDefault("adapters.cpmstar.endpoint", "https://server.cpmstar.com/openrtbbidrq.aspx")
 	v.SetDefault("adapters.datablocks.endpoint", "http://{{.Host}}/openrtb2?sid={{.SourceId}}")
-	v.SetDefault("adapters.dmx.endpoint", "https://dmx.districtm.io/b/v2")
+	v.SetDefault("adapters.dmx.endpoint", "https://dmx-direct.districtm.io/b/v2")
 	v.SetDefault("adapters.emx_digital.endpoint", "https://hb.emxdgt.com")
 	v.SetDefault("adapters.engagebdr.endpoint", "http://dsp.bnmla.com/hb")
 	v.SetDefault("adapters.eplanning.endpoint", "http://rtb.e-planning.net/pbs/1")
diff --git a/config/util/loggers.go b/config/util/loggers.go
index 88702e68..d9aad43a 100644
--- a/config/util/loggers.go
+++ b/config/util/loggers.go
@@ -4,18 +4,18 @@ import (
 	"math/rand"
 )
 
-type logMsg func(string, ...interface{})
+type LogMsg func(string, ...interface{})
 
 type randomGenerator func() float32
 
 // LogRandomSample will log a randam sample of the messages it is sent, based on the chance to log
 // chance = 1.0 => always log,
 // chance = 0.0 => never log
-func LogRandomSample(msg string, logger logMsg, chance float32) {
+func LogRandomSample(msg string, logger LogMsg, chance float32) {
 	logRandomSampleImpl(msg, logger, chance, rand.Float32)
 }
 
-func logRandomSampleImpl(msg string, logger logMsg, chance float32, randGenerator randomGenerator) {
+func logRandomSampleImpl(msg string, logger LogMsg, chance float32, randGenerator randomGenerator) {
 	if chance < 1.0 && randGenerator() > chance {
 		// this is the chance we don't log anything
 		return
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 3fd21321..86186fa8 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -39,6 +39,12 @@ import (
 
 const storedRequestTimeoutMillis = 50
 
+var (
+	dntKey      string = http.CanonicalHeaderKey("DNT")
+	dntDisabled int8   = 0
+	dntEnabled  int8   = 1
+)
+
 func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById stored_requests.Fetcher, categories stored_requests.CategoryFetcher, cfg *config.Configuration, met pbsmetrics.MetricsEngine, pbsAnalytics analytics.PBSAnalyticsModule, disabledBidders map[string]string, defReqJSON []byte, bidderMap map[string]openrtb_ext.BidderName) (httprouter.Handle, error) {
 
 	if ex == nil || validator == nil || requestsById == nil || cfg == nil || met == nil {
@@ -964,6 +970,8 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, bidReq *ope
 func setDeviceImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest, ipValidtor iputil.IPValidator) {
 	setIPImplicitly(httpReq, bidReq, ipValidtor)
 	setUAImplicitly(httpReq, bidReq)
+	setDoNotTrackImplicitly(httpReq, bidReq)
+
 }
 
 // setAuctionTypeImplicitly sets the auction type to 1 if it wasn't on the request,
@@ -1192,6 +1200,24 @@ func setUAImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) {
 	}
 }
 
+func setDoNotTrackImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) {
+	if bidReq.Device == nil || bidReq.Device.DNT == nil {
+		dnt := httpReq.Header.Get(dntKey)
+		if dnt == "0" || dnt == "1" {
+			if bidReq.Device == nil {
+				bidReq.Device = &openrtb.Device{}
+			}
+
+			switch dnt {
+			case "0":
+				bidReq.Device.DNT = &dntDisabled
+			case "1":
+				bidReq.Device.DNT = &dntEnabled
+			}
+		}
+	}
+}
+
 // parseUserID gets this user's ID for the host machine, if it exists.
 func parseUserID(cfg *config.Configuration, httpReq *http.Request) (string, bool) {
 	if hostCookie, err := httpReq.Cookie(cfg.HostCookie.CookieName); hostCookie != nil && err == nil {
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-dnt.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-dnt.json
new file mode 100644
index 00000000..b1fae20a
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-dnt.json
@@ -0,0 +1,45 @@
+{
+    "id": "some-request-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "device": {
+        "dnt": 1
+    },
+    "imp": [
+      {
+        "id": "my-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "pmp": {
+          "deals": [
+            {
+              "id": "some-deal-id"
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "targeting": {
+          "pricegranularity": "low"
+        },
+        "cache": {
+          "bids": {}
+        }
+      }
+    }
+  }
+  
\ No newline at end of file
diff --git a/exchange/bidder.go b/exchange/bidder.go
index df9f0a3b..ee6a4942 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -312,6 +312,10 @@ func makeExt(httpInfo *httpCallInfo) *openrtb_ext.ExtHttpCall {
 // doRequest makes a request, handles the response, and returns the data needed by the
 // Bidder interface.
 func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.RequestData) *httpCallInfo {
+	return bidder.doRequestImpl(ctx, req, glog.Warningf)
+}
+
+func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.RequestData, logger util.LogMsg) *httpCallInfo {
 	httpReq, err := http.NewRequest(req.Method, req.Uri, bytes.NewBuffer(req.Body))
 	if err != nil {
 		return &httpCallInfo{
@@ -325,12 +329,18 @@ func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.Reques
 	if err != nil {
 		if err == context.DeadlineExceeded {
 			err = &errortypes.Timeout{Message: err.Error()}
-			if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); ok {
+			var corebidder adapters.Bidder = bidder.Bidder
+			// The bidder adapter normally stores an info-aware bidder (a bidder wrapper)
+			// rather than the actual bidder. So we need to unpack that first.
+			if b, ok := corebidder.(*adapters.InfoAwareBidder); ok {
+				corebidder = b.Bidder
+			}
+			if tb, ok := corebidder.(adapters.TimeoutBidder); ok {
 				// Toss the timeout notification call into a go routine, as we are out of time'
 				// and cannot delay processing. We don't do anything result, as there is not much
 				// we can do about a timeout notification failure. We do not want to get stuck in
 				// a loop of trying to report timeouts to the timeout notifications.
-				go bidder.doTimeoutNotification(tb, req)
+				go bidder.doTimeoutNotification(tb, req, logger)
 			}
 
 		}
@@ -366,7 +376,7 @@ func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.Reques
 	}
 }
 
-func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.TimeoutBidder, req *adapters.RequestData) {
+func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.TimeoutBidder, req *adapters.RequestData, logger util.LogMsg) {
 	ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
 	defer cancel()
 	toReq, errL := timeoutBidder.MakeTimeoutNotification(req)
@@ -385,13 +395,13 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou
 					msg = fmt.Sprintf("TimeoutNotification: error:(%s) body:%s", err.Error(), string(toReq.Body))
 				}
 				// If logging is turned on, and logging is not disallowed via FailOnly
-				util.LogRandomSample(msg, glog.Warningf, bidder.DebugConfig.TimeoutNotification.SamplingRate)
+				util.LogRandomSample(msg, logger, bidder.DebugConfig.TimeoutNotification.SamplingRate)
 			}
 		} else {
 			bidder.me.RecordTimeoutNotice(false)
 			if bidder.DebugConfig.TimeoutNotification.Log {
 				msg := fmt.Sprintf("TimeoutNotification: Failed to make timeout request: method(%s), uri(%s), error(%s)", toReq.Method, toReq.Uri, err.Error())
-				util.LogRandomSample(msg, glog.Warningf, bidder.DebugConfig.TimeoutNotification.SamplingRate)
+				util.LogRandomSample(msg, logger, bidder.DebugConfig.TimeoutNotification.SamplingRate)
 			}
 		}
 	} else if bidder.DebugConfig.TimeoutNotification.Log {
@@ -402,7 +412,7 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou
 		} else {
 			msg = fmt.Sprintf("TimeoutNotification: Failed to generate timeout request: error(%s), bidder request marshal failed(%s)", errL[0].Error(), err.Error())
 		}
-		util.LogRandomSample(msg, glog.Warningf, bidder.DebugConfig.TimeoutNotification.SamplingRate)
+		util.LogRandomSample(msg, logger, bidder.DebugConfig.TimeoutNotification.SamplingRate)
 	}
 
 }
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 174a0b3e..3f0258dd 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -104,11 +104,10 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 
 	// Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder
 	blabels := make(map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels)
-	cleanRequests, aliases, cleanMetrics, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig)
+	cleanRequests, aliases, privacyLabels, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig)
+
+	e.me.RecordRequestPrivacy(privacyLabels)
 
-	if cleanMetrics.gdprEnforced {
-		e.me.RecordTCFReq(pbsmetrics.TCFVersionToValue(cleanMetrics.gdprTcfVersion))
-	}
 	// List of bidders we have requests for.
 	liveAdapters := listBiddersWithRequests(cleanRequests)
 
diff --git a/exchange/utils.go b/exchange/utils.go
index 4de985ec..bc1b555e 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -19,15 +19,6 @@ import (
 	"github.com/prebid/prebid-server/privacy/lmt"
 )
 
-// cleanMetrics is a struct to export any metrics data resulting from cleanOpenRTBRequests(). It starts with just
-// the TCF version, but made a struct to facilitate future expansion
-type cleanMetrics struct {
-	// A simple flag if GDPR is being enforced on this request.
-	gdprEnforced bool
-	// a zero value means a missing or invalid GDPR string
-	gdprTcfVersion int
-}
-
 func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) {
 	bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain)
 
@@ -63,7 +54,7 @@ func cleanOpenRTBRequests(ctx context.Context,
 	labels pbsmetrics.Labels,
 	gDPR gdpr.Permissions,
 	usersyncIfAmbiguous bool,
-	privacyConfig config.Privacy) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, cleanMetrics cleanMetrics, errs []error) {
+	privacyConfig config.Privacy) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, privacyLabels pbsmetrics.PrivacyLabels, errs []error) {
 
 	impsByBidder, errs := splitImps(orig.Imp)
 	if len(errs) > 0 {
@@ -98,13 +89,20 @@ func cleanOpenRTBRequests(ctx context.Context,
 		LMT:   lmtPolicy.ShouldEnforce(),
 	}
 
+	privacyLabels.CCPAProvided = ccpaPolicy.Value != ""
+	privacyLabels.CCPAEnforced = privacyEnforcement.CCPA
+	privacyLabels.COPPAEnforced = privacyEnforcement.COPPA
+	privacyLabels.LMTEnforced = privacyEnforcement.LMT
+
 	if gdpr == 1 {
-		cleanMetrics.gdprEnforced = true
+		privacyLabels.GDPREnforced = true
 		parsedConsent, err := vendorconsent.ParseString(consent)
 		if err == nil {
-			cleanMetrics.gdprTcfVersion = int(parsedConsent.Version())
+			version := int(parsedConsent.Version())
+			privacyLabels.GDPRTCFVersion = pbsmetrics.TCFVersionToValue(version)
 		}
 	}
+
 	// bidder level privacy policies
 	for bidder, bidReq := range requestsByBidder {
 
diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go
index 3d105dea..0dbe9a69 100644
--- a/pbsmetrics/config/metrics.go
+++ b/pbsmetrics/config/metrics.go
@@ -195,10 +195,10 @@ func (me *MultiMetricsEngine) RecordTimeoutNotice(success bool) {
 	}
 }
 
-// RecordTCFReq across all engines
-func (me *MultiMetricsEngine) RecordTCFReq(version pbsmetrics.TCFVersionValue) {
+// RecordRequestPrivacy across all engines
+func (me *MultiMetricsEngine) RecordRequestPrivacy(privacy pbsmetrics.PrivacyLabels) {
 	for _, thisME := range *me {
-		thisME.RecordTCFReq(version)
+		thisME.RecordRequestPrivacy(privacy)
 	}
 }
 
@@ -281,6 +281,6 @@ func (me *DummyMetricsEngine) RecordRequestQueueTime(success bool, requestType p
 func (me *DummyMetricsEngine) RecordTimeoutNotice(success bool) {
 }
 
-// RecordReq as a noop
-func (me *DummyMetricsEngine) RecordTCFReq(version pbsmetrics.TCFVersionValue) {
+// RecordRequestPrivacy as a noop
+func (me *DummyMetricsEngine) RecordRequestPrivacy(privacy pbsmetrics.PrivacyLabels) {
 }
diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go
index 73eb30a1..836434bf 100644
--- a/pbsmetrics/go_metrics.go
+++ b/pbsmetrics/go_metrics.go
@@ -53,7 +53,11 @@ type Metrics struct {
 	TimeoutNotificationFailure metrics.Meter
 
 	// TCF adaption metrics
-	TCFReqVersion map[TCFVersionValue]metrics.Meter
+	PrivacyCCPARequest       metrics.Meter
+	PrivacyCCPARequestOptOut metrics.Meter
+	PrivacyCOPPARequest      metrics.Meter
+	PrivacyLMTRequest        metrics.Meter
+	PrivacyTCFRequestVersion map[TCFVersionValue]metrics.Meter
 
 	AdapterMetrics map[openrtb_ext.BidderName]*AdapterMetrics
 	// Don't export accountMetrics because we need helper functions here to insure its properly populated dynamically
@@ -141,7 +145,11 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 		TimeoutNotificationSuccess: blankMeter,
 		TimeoutNotificationFailure: blankMeter,
 
-		TCFReqVersion: make(map[TCFVersionValue]metrics.Meter, len(TCFVersions())),
+		PrivacyCCPARequest:       blankMeter,
+		PrivacyCCPARequestOptOut: blankMeter,
+		PrivacyCOPPARequest:      blankMeter,
+		PrivacyLMTRequest:        blankMeter,
+		PrivacyTCFRequestVersion: make(map[TCFVersionValue]metrics.Meter, len(TCFVersions())),
 
 		AdapterMetrics:  make(map[openrtb_ext.BidderName]*AdapterMetrics, len(exchanges)),
 		accountMetrics:  make(map[string]*accountMetrics),
@@ -149,6 +157,7 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 
 		exchanges: exchanges,
 	}
+
 	for _, a := range exchanges {
 		newMetrics.AdapterMetrics[a] = makeBlankAdapterMetrics()
 	}
@@ -166,7 +175,7 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 	}
 
 	for _, v := range TCFVersions() {
-		newMetrics.TCFReqVersion[v] = blankMeter
+		newMetrics.PrivacyTCFRequestVersion[v] = blankMeter
 	}
 
 	//to minimize memory usage, queuedTimeout metric is now supported for video endpoint only
@@ -234,8 +243,12 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d
 	newMetrics.TimeoutNotificationSuccess = metrics.GetOrRegisterMeter("timeout_notification.ok", registry)
 	newMetrics.TimeoutNotificationFailure = metrics.GetOrRegisterMeter("timeout_notification.failed", registry)
 
+	newMetrics.PrivacyCCPARequest = metrics.GetOrRegisterMeter("privacy.request.ccpa.specified", registry)
+	newMetrics.PrivacyCCPARequestOptOut = metrics.GetOrRegisterMeter("privacy.request.ccpa.opt-out", registry)
+	newMetrics.PrivacyCOPPARequest = metrics.GetOrRegisterMeter("privacy.request.coppa", registry)
+	newMetrics.PrivacyLMTRequest = metrics.GetOrRegisterMeter("privacy.request.lmt", registry)
 	for _, version := range TCFVersions() {
-		newMetrics.TCFReqVersion[version] = metrics.GetOrRegisterMeter(fmt.Sprintf("privacy.request.tcf.%s", string(version)), registry)
+		newMetrics.PrivacyTCFRequestVersion[version] = metrics.GetOrRegisterMeter(fmt.Sprintf("privacy.request.tcf.%s", string(version)), registry)
 	}
 
 	return newMetrics
@@ -582,12 +595,28 @@ func (me *Metrics) RecordTimeoutNotice(success bool) {
 	return
 }
 
-func (me *Metrics) RecordTCFReq(version TCFVersionValue) {
-	met, ok := me.TCFReqVersion[version]
-	if ok {
-		met.Mark(1)
-	} else {
-		me.TCFReqVersion[TCFVersionErr].Mark(1)
+func (me *Metrics) RecordRequestPrivacy(privacy PrivacyLabels) {
+	if privacy.CCPAProvided {
+		me.PrivacyCCPARequest.Mark(1)
+		if privacy.CCPAEnforced {
+			me.PrivacyCCPARequestOptOut.Mark(1)
+		}
+	}
+
+	if privacy.COPPAEnforced {
+		me.PrivacyCOPPARequest.Mark(1)
+	}
+
+	if privacy.GDPREnforced {
+		if metric, ok := me.PrivacyTCFRequestVersion[privacy.GDPRTCFVersion]; ok {
+			metric.Mark(1)
+		} else {
+			me.PrivacyTCFRequestVersion[TCFVersionErr].Mark(1)
+		}
+	}
+
+	if privacy.LMTEnforced {
+		me.PrivacyLMTRequest.Mark(1)
 	}
 	return
 }
diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go
index 0e94fe71..514fbac1 100644
--- a/pbsmetrics/metrics.go
+++ b/pbsmetrics/metrics.go
@@ -41,6 +41,16 @@ type RequestLabels struct {
 	RequestStatus RequestStatus
 }
 
+// PrivacyLabels defines metrics describing the result of privacy enforcement.
+type PrivacyLabels struct {
+	CCPAEnforced   bool
+	CCPAProvided   bool
+	COPPAEnforced  bool
+	GDPREnforced   bool
+	GDPRTCFVersion TCFVersionValue
+	LMTEnforced    bool
+}
+
 // Label typecasting. Se below the type definitions for possible values
 
 // DemandSource : Demand source enumeration
@@ -257,7 +267,7 @@ const (
 	TCFVersionV2  TCFVersionValue = "v2"
 )
 
-// TCFVersions rtuens the possible values for the TCF version
+// TCFVersions returns the possible values for the TCF version
 func TCFVersions() []TCFVersionValue {
 	return []TCFVersionValue{
 		TCFVersionErr,
@@ -305,5 +315,5 @@ type MetricsEngine interface {
 	RecordPrebidCacheRequestTime(success bool, length time.Duration)
 	RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration)
 	RecordTimeoutNotice(sucess bool)
-	RecordTCFReq(version TCFVersionValue)
+	RecordRequestPrivacy(privacy PrivacyLabels)
 }
diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go
index a6d36a72..6c263f0a 100644
--- a/pbsmetrics/metrics_mock.go
+++ b/pbsmetrics/metrics_mock.go
@@ -107,7 +107,7 @@ func (me *MetricsEngineMock) RecordTimeoutNotice(success bool) {
 	me.Called(success)
 }
 
-// RecordTCFReq mock
-func (me *MetricsEngineMock) RecordTCFReq(version TCFVersionValue) {
-	me.Called(version)
+// RecordRequestPrivacy mock
+func (me *MetricsEngineMock) RecordRequestPrivacy(privacy PrivacyLabels) {
+	me.Called(privacy)
 }
diff --git a/pbsmetrics/prometheus/preload.go b/pbsmetrics/prometheus/preload.go
index 19f4f225..ef1d300c 100644
--- a/pbsmetrics/prometheus/preload.go
+++ b/pbsmetrics/prometheus/preload.go
@@ -8,15 +8,16 @@ import (
 func preloadLabelValues(m *Metrics) {
 	var (
 		actionValues          = actionsAsString()
-		adapterValues         = adaptersAsString()
 		adapterErrorValues    = adapterErrorsAsString()
+		adapterValues         = adaptersAsString()
 		bidTypeValues         = []string{markupDeliveryAdm, markupDeliveryNurl}
 		boolValues            = boolValuesAsString()
 		cacheResultValues     = cacheResultsAsString()
-		cookieValues          = cookieTypesAsString()
 		connectionErrorValues = []string{connectionAcceptError, connectionCloseError}
+		cookieValues          = cookieTypesAsString()
 		requestStatusValues   = requestStatusesAsString()
 		requestTypeValues     = requestTypesAsString()
+		sourceValues          = []string{sourceRequest}
 	)
 
 	preloadLabelValuesForCounter(m.connectionsError, map[string][]string{
@@ -100,9 +101,22 @@ func preloadLabelValues(m *Metrics) {
 		requestStatusLabel: {requestSuccessLabel, requestRejectLabel},
 	})
 
-	preloadLabelValuesForCounter(m.tcfVersion, map[string][]string{
+	preloadLabelValuesForCounter(m.privacyCCPA, map[string][]string{
+		sourceLabel: sourceValues,
+		optOutLabel: boolValues,
+	})
+
+	preloadLabelValuesForCounter(m.privacyCOPPA, map[string][]string{
+		sourceLabel: sourceValues,
+	})
+
+	preloadLabelValuesForCounter(m.privacyLMT, map[string][]string{
+		sourceLabel: sourceValues,
+	})
+
+	preloadLabelValuesForCounter(m.privacyTCF, map[string][]string{
+		sourceLabel:  sourceValues,
 		versionLabel: tcfVersionsAsString(),
-		sourceLabel:  {string(sourceRequest)},
 	})
 }
 
diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go
index bf854746..d94c4d78 100644
--- a/pbsmetrics/prometheus/prometheus.go
+++ b/pbsmetrics/prometheus/prometheus.go
@@ -29,7 +29,10 @@ type Metrics struct {
 	storedImpressionsCacheResult *prometheus.CounterVec
 	storedRequestCacheResult     *prometheus.CounterVec
 	timeoutNotifications         *prometheus.CounterVec
-	tcfVersion                   *prometheus.CounterVec
+	privacyCCPA                  *prometheus.CounterVec
+	privacyCOPPA                 *prometheus.CounterVec
+	privacyLMT                   *prometheus.CounterVec
+	privacyTCF                   *prometheus.CounterVec
 
 	// Adapter Metrics
 	adapterBids          *prometheus.CounterVec
@@ -60,6 +63,7 @@ const (
 	isNativeLabel        = "native"
 	isVideoLabel         = "video"
 	markupDeliveryLabel  = "delivery"
+	optOutLabel          = "opt_out"
 	privacyBlockedLabel  = "privacy_blocked"
 	requestStatusLabel   = "request_status"
 	requestTypeLabel     = "request_type"
@@ -165,11 +169,26 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
 		"Count of timeout notifications triggered, and if they were successfully sent.",
 		[]string{successLabel})
 
-	metrics.tcfVersion = newCounter(cfg, metrics.Registry,
+	metrics.privacyCCPA = newCounter(cfg, metrics.Registry,
+		"privacy_ccpa",
+		"Count of total requests to Prebid Server where CCPA was provided by source and opt-out .",
+		[]string{sourceLabel, optOutLabel})
+
+	metrics.privacyCOPPA = newCounter(cfg, metrics.Registry,
+		"privacy_coppa",
+		"Count of total requests to Prebid Server where the COPPA flag was set by source",
+		[]string{sourceLabel})
+
+	metrics.privacyTCF = newCounter(cfg, metrics.Registry,
 		"privacy_tcf",
-		"Count of TCF versions for requests where GDPR was enforced.",
+		"Count of TCF versions for requests where GDPR was enforced by source and version.",
 		[]string{versionLabel, sourceLabel})
 
+	metrics.privacyLMT = newCounter(cfg, metrics.Registry,
+		"privacy_lmt",
+		"Count of total requests to Prebid Server where the LMT flag was set by source",
+		[]string{sourceLabel})
+
 	metrics.adapterBids = newCounter(cfg, metrics.Registry,
 		"adapter_bids",
 		"Count of bids labeled by adapter and markup delivery type (adm or nurl).",
@@ -434,9 +453,30 @@ func (m *Metrics) RecordTimeoutNotice(success bool) {
 	}
 }
 
-func (m *Metrics) RecordTCFReq(version pbsmetrics.TCFVersionValue) {
-	m.tcfVersion.With(prometheus.Labels{
-		versionLabel: string(version),
-		sourceLabel:  sourceRequest,
-	}).Inc()
+func (m *Metrics) RecordRequestPrivacy(privacy pbsmetrics.PrivacyLabels) {
+	if privacy.CCPAProvided {
+		m.privacyCCPA.With(prometheus.Labels{
+			sourceLabel: sourceRequest,
+			optOutLabel: strconv.FormatBool(privacy.CCPAEnforced),
+		}).Inc()
+	}
+
+	if privacy.COPPAEnforced {
+		m.privacyCOPPA.With(prometheus.Labels{
+			sourceLabel: sourceRequest,
+		}).Inc()
+	}
+
+	if privacy.GDPREnforced {
+		m.privacyTCF.With(prometheus.Labels{
+			versionLabel: string(privacy.GDPRTCFVersion),
+			sourceLabel:  sourceRequest,
+		}).Inc()
+	}
+
+	if privacy.LMTEnforced {
+		m.privacyLMT.With(prometheus.Labels{
+			sourceLabel: sourceRequest,
+		}).Inc()
+	}
 }
diff --git a/static/bidder-info/adtelligent.yaml b/static/bidder-info/adtelligent.yaml
index fe791343..7a20d52b 100644
--- a/static/bidder-info/adtelligent.yaml
+++ b/static/bidder-info/adtelligent.yaml
@@ -4,6 +4,7 @@ capabilities:
   app:
     mediaTypes:
       - banner
+      - video
   site:
     mediaTypes:
       - banner


Test patch:
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index c697c206..957760c6 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -604,6 +604,189 @@ func TestImplicitIPsEndToEnd(t *testing.T) {
 	}
 }
 
+func TestImplicitDNT(t *testing.T) {
+	var (
+		disabled int8 = 0
+		enabled  int8 = 1
+	)
+	testCases := []struct {
+		description     string
+		dntHeader       string
+		request         openrtb.BidRequest
+		expectedRequest openrtb.BidRequest
+	}{
+		{
+			description:     "Device Missing - Not Set In Header",
+			dntHeader:       "",
+			request:         openrtb.BidRequest{},
+			expectedRequest: openrtb.BidRequest{},
+		},
+		{
+			description: "Device Missing - Set To 0 In Header",
+			dntHeader:   "0",
+			request:     openrtb.BidRequest{},
+			expectedRequest: openrtb.BidRequest{
+				Device: &openrtb.Device{
+					DNT: &disabled,
+				},
+			},
+		},
+		{
+			description: "Device Missing - Set To 1 In Header",
+			dntHeader:   "1",
+			request:     openrtb.BidRequest{},
+			expectedRequest: openrtb.BidRequest{
+				Device: &openrtb.Device{
+					DNT: &enabled,
+				},
+			},
+		},
+		{
+			description: "Not Set In Request - Not Set In Header",
+			dntHeader:   "",
+			request: openrtb.BidRequest{
+				Device: &openrtb.Device{},
+			},
+			expectedRequest: openrtb.BidRequest{
+				Device: &openrtb.Device{},
+			},
+		},
+		{
+			description: "Not Set In Request - Set To 0 In Header",
+			dntHeader:   "0",
+			request: openrtb.BidRequest{
+				Device: &openrtb.Device{},
+			},
+			expectedRequest: openrtb.BidRequest{
+				Device: &openrtb.Device{
+					DNT: &disabled,
+				},
+			},
+		},
+		{
+			description: "Not Set In Request - Set To 1 In Header",
+			dntHeader:   "1",
+			request: openrtb.BidRequest{
+				Device: &openrtb.Device{},
+			},
+			expectedRequest: openrtb.BidRequest{
+				Device: &openrtb.Device{
+					DNT: &enabled,
+				},
+			},
+		},
+		{
+			description: "Set In Request - Not Set In Header",
+			dntHeader:   "",
+			request: openrtb.BidRequest{
+				Device: &openrtb.Device{
+					DNT: &enabled,
+				},
+			},
+			expectedRequest: openrtb.BidRequest{
+				Device: &openrtb.Device{
+					DNT: &enabled,
+				},
+			},
+		},
+		{
+			description: "Set In Request - Set To 0 In Header",
+			dntHeader:   "0",
+			request: openrtb.BidRequest{
+				Device: &openrtb.Device{
+					DNT: &enabled,
+				},
+			},
+			expectedRequest: openrtb.BidRequest{
+				Device: &openrtb.Device{
+					DNT: &enabled,
+				},
+			},
+		},
+		{
+			description: "Set In Request - Set To 1 In Header",
+			dntHeader:   "1",
+			request: openrtb.BidRequest{
+				Device: &openrtb.Device{
+					DNT: &enabled,
+				},
+			},
+			expectedRequest: openrtb.BidRequest{
+				Device: &openrtb.Device{
+					DNT: &enabled,
+				},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		httpReq := httptest.NewRequest("POST", "/openrtb2/auction", nil)
+		httpReq.Header.Set("DNT", test.dntHeader)
+		setDoNotTrackImplicitly(httpReq, &test.request)
+		assert.Equal(t, test.expectedRequest, test.request)
+	}
+}
+
+func TestImplicitDNTEndToEnd(t *testing.T) {
+	var (
+		disabled int8 = 0
+		enabled  int8 = 1
+	)
+	testCases := []struct {
+		description string
+		reqJSONFile string
+		dntHeader   string
+		expectedDNT *int8
+	}{
+		{
+			description: "Not Set In Request - Not Set In Header",
+			reqJSONFile: "site.json",
+			dntHeader:   "",
+			expectedDNT: nil,
+		},
+		{
+			description: "Not Set In Request - Set To 0 In Header",
+			reqJSONFile: "site.json",
+			dntHeader:   "0",
+			expectedDNT: &disabled,
+		},
+		{
+			description: "Not Set In Request - Set To 1 In Header",
+			reqJSONFile: "site.json",
+			dntHeader:   "1",
+			expectedDNT: &enabled,
+		},
+		{
+			description: "Set In Request - Not Set In Header",
+			reqJSONFile: "site-has-dnt.json",
+			dntHeader:   "",
+			expectedDNT: &enabled, // Hardcoded value in test file.
+		},
+		{
+			description: "Set In Request - Not Overwritten By Header",
+			reqJSONFile: "site-has-dnt.json",
+			dntHeader:   "0",
+			expectedDNT: &enabled, // Hardcoded value in test file.
+		},
+	}
+
+	metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
+	for _, test := range testCases {
+		exchange := &nobidExchange{}
+		endpoint, _ := NewEndpoint(exchange, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap)
+
+		httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, test.reqJSONFile)))
+		httpReq.Header.Set("DNT", test.dntHeader)
+
+		endpoint(httptest.NewRecorder(), httpReq, nil)
+
+		result := exchange.gotRequest
+		if !assert.NotEmpty(t, result, test.description+"Request received by the exchange.") {
+			t.FailNow()
+		}
+		assert.Equal(t, test.expectedDNT, result.Device.DNT, test.description+":dnt")
+	}
+}
 func TestImplicitSecure(t *testing.T) {
 	httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json")))
 	httpReq.Header.Set(http.CanonicalHeaderKey("X-Forwarded-Proto"), "https")
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index b776715a..d4fc0cf7 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -1,6 +1,7 @@
 package exchange
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"errors"
@@ -10,6 +11,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/golang/glog"
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
@@ -1237,8 +1239,8 @@ func TestTimeoutNotificationOff(t *testing.T) {
 	server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody))
 	defer server.Close()
 
-	bidderImpl := &notifingBidder{
-		notiRequest: adapters.RequestData{
+	bidderImpl := &notifyingBidder{
+		notifyRequest: adapters.RequestData{
 			Method:  "GET",
 			Uri:     server.URL + "/notify/me",
 			Body:    nil,
@@ -1254,39 +1256,83 @@ func TestTimeoutNotificationOff(t *testing.T) {
 	if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); !ok {
 		t.Error("Failed to cast bidder to a TimeoutBidder")
 	} else {
-		bidder.doTimeoutNotification(tb, &adapters.RequestData{})
+		bidder.doTimeoutNotification(tb, &adapters.RequestData{}, glog.Warningf)
 	}
 }
 
 func TestTimeoutNotificationOn(t *testing.T) {
-	respBody := "{\"bid\":false}"
-	respStatus := 200
-	server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody))
+	// Expire context immediately to force timeout handler.
+	ctx, cancelFunc := context.WithDeadline(context.Background(), time.Now())
+	cancelFunc()
+
+	// Notification logic is hardcoded for 200ms. We need to wait for a little longer than that.
+	server := httptest.NewServer(mockSlowHandler(205*time.Millisecond, 200, `{"bid":false}`))
 	defer server.Close()
 
-	bidderImpl := &notifingBidder{
-		notiRequest: adapters.RequestData{
+	bidder := &notifyingBidder{
+		notifyRequest: adapters.RequestData{
 			Method:  "GET",
 			Uri:     server.URL + "/notify/me",
 			Body:    nil,
 			Headers: http.Header{},
 		},
 	}
-	bidder := &bidderAdapter{
-		Bidder: bidderImpl,
+
+	// Wrap with BidderInfo to mimic exchange.go flow.
+	bidderWrappedWithInfo := wrapWithBidderInfo(bidder)
+
+	bidderAdapter := &bidderAdapter{
+		Bidder: bidderWrappedWithInfo,
 		Client: server.Client(),
 		DebugConfig: config.Debug{
 			TimeoutNotification: config.TimeoutNotification{
-				Log: true,
+				Log:          true,
+				SamplingRate: 1.0,
 			},
 		},
 		me: &metricsConfig.DummyMetricsEngine{},
 	}
-	if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); !ok {
-		t.Error("Failed to cast bidder to a TimeoutBidder")
-	} else {
-		bidder.doTimeoutNotification(tb, &adapters.RequestData{})
+
+	// Unwrap To Mimic exchange.go Casting Code
+	var coreBidder adapters.Bidder = bidderAdapter.Bidder
+	if b, ok := coreBidder.(*adapters.InfoAwareBidder); ok {
+		coreBidder = b.Bidder
+	}
+	if _, ok := coreBidder.(adapters.TimeoutBidder); !ok {
+		t.Fatal("Failed to cast bidder to a TimeoutBidder")
+	}
+
+	bidRequest := adapters.RequestData{
+		Method: "POST",
+		Uri:    server.URL,
+		Body:   []byte(`{"id":"this-id","app":{"publisher":{"id":"pub-id"}}}`),
+	}
+
+	var loggerBuffer bytes.Buffer
+	logger := func(msg string, args ...interface{}) {
+		loggerBuffer.WriteString(fmt.Sprintf(fmt.Sprintln(msg), args...))
+	}
+
+	bidderAdapter.doRequestImpl(ctx, &bidRequest, logger)
+
+	// Wait a little longer than the 205ms mock server sleep.
+	time.Sleep(210 * time.Millisecond)
+
+	logExpected := "TimeoutNotification: error:(context deadline exceeded) body:\n"
+	logActual := loggerBuffer.String()
+	assert.EqualValues(t, logExpected, logActual)
+}
+
+func wrapWithBidderInfo(bidder adapters.Bidder) adapters.Bidder {
+	bidderInfo := adapters.BidderInfo{
+		Status: adapters.StatusActive,
+		Capabilities: &adapters.CapabilitiesInfo{
+			App: &adapters.PlatformInfo{
+				MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner},
+			},
+		},
 	}
+	return adapters.EnforceBidderInfo(bidder, bidderInfo)
 }
 
 type goodSingleBidder struct {
@@ -1363,18 +1409,19 @@ func (bidder *bidRejector) MakeBids(internalRequest *openrtb.BidRequest, externa
 	return nil, []error{errors.New("Can't make a response.")}
 }
 
-type notifingBidder struct {
-	notiRequest adapters.RequestData
+type notifyingBidder struct {
+	requests      []*adapters.RequestData
+	notifyRequest adapters.RequestData
 }
 
-func (bidder *notifingBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
-	return nil, nil
+func (bidder *notifyingBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	return bidder.requests, nil
 }
 
-func (bidder *notifingBidder) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+func (bidder *notifyingBidder) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
 	return nil, nil
 }
 
-func (bidder *notifingBidder) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error) {
-	return &bidder.notiRequest, nil
+func (bidder *notifyingBidder) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error) {
+	return &bidder.notifyRequest, nil
 }
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 161b24fd..96f740de 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -1830,6 +1830,15 @@ func mockHandler(statusCode int, getBody string, postBody string) http.Handler {
 	})
 }
 
+func mockSlowHandler(delay time.Duration, statusCode int, body string) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		time.Sleep(delay)
+
+		w.WriteHeader(statusCode)
+		w.Write([]byte(body))
+	})
+}
+
 type wellBehavedCache struct{}
 
 func (c *wellBehavedCache) GetExtCacheData() (string, string) {
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 6d66e816..608e6a17 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -15,7 +15,9 @@ import (
 // permissionsMock mocks the Permissions interface for tests
 //
 // It only allows appnexus for GDPR consent
-type permissionsMock struct{}
+type permissionsMock struct {
+	personalInfoAllowed bool
+}
 
 func (p *permissionsMock) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) {
 	return true, nil
@@ -26,10 +28,7 @@ func (p *permissionsMock) BidderSyncAllowed(ctx context.Context, bidder openrtb_
 }
 
 func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) {
-	if bidder == "appnexus" {
-		return true, true, nil
-	}
-	return false, false, nil
+	return p.personalInfoAllowed, p.personalInfoAllowed, nil
 }
 
 func (p *permissionsMock) AMPException() bool {
@@ -80,7 +79,7 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		reqByBidders, _, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
+		reqByBidders, _, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig)
 		if test.hasError {
 			assert.NotNil(t, err, "Error shouldn't be nil")
 		} else {
@@ -92,26 +91,48 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 
 func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 	testCases := []struct {
-		description     string
-		enforceCCPA     bool
-		expectDataScrub bool
+		description         string
+		ccpaConsent         string
+		enforceCCPA         bool
+		expectDataScrub     bool
+		expectPrivacyLabels pbsmetrics.PrivacyLabels
 	}{
 		{
-			description:     "Feature Flag Enabled",
+			description:     "Feature Flag Enabled - Opt Out",
+			ccpaConsent:     "1-Y-",
 			enforceCCPA:     true,
 			expectDataScrub: true,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				CCPAProvided: true,
+				CCPAEnforced: true,
+			},
+		},
+		{
+			description:     "Feature Flag Enabled - Opt In",
+			ccpaConsent:     "1-N-",
+			enforceCCPA:     true,
+			expectDataScrub: false,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				CCPAProvided: true,
+				CCPAEnforced: false,
+			},
 		},
 		{
 			description:     "Feature Flag Disabled",
+			ccpaConsent:     "1-Y-",
 			enforceCCPA:     false,
 			expectDataScrub: false,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				CCPAProvided: false,
+				CCPAEnforced: false,
+			},
 		},
 	}
 
 	for _, test := range testCases {
 		req := newBidRequest(t)
 		req.Regs = &openrtb.Regs{
-			Ext: json.RawMessage(`{"us_privacy":"1-Y-"}`),
+			Ext: json.RawMessage(`{"us_privacy":"` + test.ccpaConsent + `"}`),
 		}
 
 		privacyConfig := config.Privacy{
@@ -120,11 +141,10 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 			},
 		}
 
-		results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
+		results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig)
 		result := results["appnexus"]
 
 		assert.Nil(t, errs)
-
 		if test.expectDataScrub {
 			assert.Equal(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
 			assert.Equal(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5")
@@ -132,6 +152,51 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 			assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
 			assert.NotEqual(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5")
 		}
+		assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels")
+	}
+}
+
+func TestCleanOpenRTBRequestsCOPPA(t *testing.T) {
+	testCases := []struct {
+		description         string
+		coppa               int8
+		expectDataScrub     bool
+		expectPrivacyLabels pbsmetrics.PrivacyLabels
+	}{
+		{
+			description:     "Enabled",
+			coppa:           1,
+			expectDataScrub: true,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				COPPAEnforced: true,
+			},
+		},
+		{
+			description:     "Disabled",
+			coppa:           0,
+			expectDataScrub: false,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				COPPAEnforced: false,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		req := newBidRequest(t)
+		req.Regs = &openrtb.Regs{COPPA: test.coppa}
+
+		results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, config.Privacy{})
+		result := results["appnexus"]
+
+		assert.Nil(t, errs)
+		if test.expectDataScrub {
+			assert.Equal(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.Equal(t, result.User.Yob, int64(0), test.description+":User.Yob")
+		} else {
+			assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.NotEqual(t, result.User.Yob, int64(0), test.description+":User.Yob")
+		}
+		assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels")
 	}
 }
 
@@ -227,34 +292,47 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 		disabled int8 = 0
 	)
 	testCases := []struct {
-		description     string
-		lmt             *int8
-		enforceLMT      bool
-		expectDataScrub bool
+		description         string
+		lmt                 *int8
+		enforceLMT          bool
+		expectDataScrub     bool
+		expectPrivacyLabels pbsmetrics.PrivacyLabels
 	}{
 		{
 			description:     "Feature Flag Enabled - OpenTRB Enabled",
 			lmt:             &enabled,
 			enforceLMT:      true,
 			expectDataScrub: true,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				LMTEnforced: true,
+			},
 		},
 		{
 			description:     "Feature Flag Disabled - OpenTRB Enabled",
 			lmt:             &enabled,
 			enforceLMT:      false,
 			expectDataScrub: false,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				LMTEnforced: false,
+			},
 		},
 		{
 			description:     "Feature Flag Enabled - OpenTRB Disabled",
 			lmt:             &disabled,
 			enforceLMT:      true,
 			expectDataScrub: false,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				LMTEnforced: false,
+			},
 		},
 		{
 			description:     "Feature Flag Disabled - OpenTRB Disabled",
 			lmt:             &disabled,
 			enforceLMT:      false,
 			expectDataScrub: false,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				LMTEnforced: false,
+			},
 		},
 	}
 
@@ -268,11 +346,10 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 			},
 		}
 
-		results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
+		results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig)
 		result := results["appnexus"]
 
 		assert.Nil(t, errs)
-
 		if test.expectDataScrub {
 			assert.Equal(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
 			assert.Equal(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5")
@@ -280,6 +357,88 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 			assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
 			assert.NotEqual(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5")
 		}
+		assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels")
+	}
+}
+
+func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
+	testCases := []struct {
+		description         string
+		gdpr                string
+		gdprConsent         string
+		gdprScrub           bool
+		enforceGDPR         bool
+		expectPrivacyLabels pbsmetrics.PrivacyLabels
+	}{
+		{
+			description: "Enforce - TCF Invalid",
+			gdpr:        "1",
+			gdprConsent: "malformed",
+			gdprScrub:   false,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				GDPREnforced:   true,
+				GDPRTCFVersion: "",
+			},
+		},
+		{
+			description: "Enforce - TCF 1",
+			gdpr:        "1",
+			gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprScrub:   true,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				GDPREnforced:   true,
+				GDPRTCFVersion: pbsmetrics.TCFVersionV1,
+			},
+		},
+		{
+			description: "Enforce - TCF 2",
+			gdpr:        "1",
+			gdprConsent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA",
+			gdprScrub:   true,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				GDPREnforced:   true,
+				GDPRTCFVersion: pbsmetrics.TCFVersionV2,
+			},
+		},
+		{
+			description: "Not Enforce - TCF 1",
+			gdpr:        "0",
+			gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprScrub:   false,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				GDPREnforced:   false,
+				GDPRTCFVersion: "",
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		req := newBidRequest(t)
+		req.User.Ext = json.RawMessage(`{"consent":"` + test.gdprConsent + `"}`)
+		req.Regs = &openrtb.Regs{
+			Ext: json.RawMessage(`{"gdpr":` + test.gdpr + `}`),
+		}
+
+		privacyConfig := config.Privacy{
+			GDPR: config.GDPR{
+				TCF2: config.TCF2{
+					Enabled: true,
+				},
+			},
+		}
+
+		results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: !test.gdprScrub}, true, privacyConfig)
+		result := results["appnexus"]
+
+		assert.Nil(t, errs)
+		if test.gdprScrub {
+			assert.Equal(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.Equal(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5")
+		} else {
+			assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.NotEqual(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5")
+		}
+		assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels")
 	}
 }
 
@@ -352,6 +511,7 @@ func newBidRequest(t *testing.T) *openrtb.BidRequest {
 		User: &openrtb.User{
 			ID:       "our-id",
 			BuyerUID: "their-id",
+			Yob:      1982,
 			Ext:      json.RawMessage(`{"digitrust":{"id":"digi-id","keyv":1,"pref":1}}`),
 		},
 		Imp: []openrtb.Imp{{
diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go
index f05f25e8..05b2fb6d 100644
--- a/gdpr/impl_test.go
+++ b/gdpr/impl_test.go
@@ -276,7 +276,7 @@ func TestAllowPersonalInfoTCF2(t *testing.T) {
 		},
 	}
 
-	// COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consensts to purposes and vendors 2, 6, 8
+	// COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consents to purposes and vendors 2, 6, 8
 	// PI needs all purposes to succeed
 	testDefs := []tcf2TestDef{
 		{
diff --git a/pbsmetrics/go_metrics_test.go b/pbsmetrics/go_metrics_test.go
index 6d9eaf9f..2faa0849 100644
--- a/pbsmetrics/go_metrics_test.go
+++ b/pbsmetrics/go_metrics_test.go
@@ -56,10 +56,14 @@ func TestNewMetrics(t *testing.T) {
 
 	ensureContains(t, registry, "timeout_notification.ok", m.TimeoutNotificationSuccess)
 	ensureContains(t, registry, "timeout_notification.failed", m.TimeoutNotificationFailure)
-	ensureContains(t, registry, "privacy.request.tcf.v1", m.TCFReqVersion[TCFVersionV1])
-	ensureContains(t, registry, "privacy.request.tcf.v2", m.TCFReqVersion[TCFVersionV2])
-	ensureContains(t, registry, "privacy.request.tcf.err", m.TCFReqVersion[TCFVersionErr])
 
+	ensureContains(t, registry, "privacy.request.ccpa.specified", m.PrivacyCCPARequest)
+	ensureContains(t, registry, "privacy.request.ccpa.opt-out", m.PrivacyCCPARequestOptOut)
+	ensureContains(t, registry, "privacy.request.coppa", m.PrivacyCOPPARequest)
+	ensureContains(t, registry, "privacy.request.lmt", m.PrivacyLMTRequest)
+	ensureContains(t, registry, "privacy.request.tcf.v1", m.PrivacyTCFRequestVersion[TCFVersionV1])
+	ensureContains(t, registry, "privacy.request.tcf.v2", m.PrivacyTCFRequestVersion[TCFVersionV2])
+	ensureContains(t, registry, "privacy.request.tcf.err", m.PrivacyTCFRequestVersion[TCFVersionErr])
 }
 
 func TestRecordBidType(t *testing.T) {
@@ -202,6 +206,61 @@ func TestRecordPrebidCacheRequestTimeWithNotSuccess(t *testing.T) {
 	assert.Equal(t, m.PrebidCacheRequestTimerError.Count(), int64(1))
 }
 
+func TestRecordRequestPrivacy(t *testing.T) {
+	registry := metrics.NewRegistry()
+	m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true})
+
+	// CCPA
+	m.RecordRequestPrivacy(PrivacyLabels{
+		CCPAEnforced: true,
+		CCPAProvided: true,
+	})
+	m.RecordRequestPrivacy(PrivacyLabels{
+		CCPAEnforced: true,
+		CCPAProvided: false,
+	})
+	m.RecordRequestPrivacy(PrivacyLabels{
+		CCPAEnforced: false,
+		CCPAProvided: true,
+	})
+
+	// COPPA
+	m.RecordRequestPrivacy(PrivacyLabels{
+		COPPAEnforced: true,
+	})
+
+	// LMT
+	m.RecordRequestPrivacy(PrivacyLabels{
+		LMTEnforced: true,
+	})
+
+	// GDPR
+	m.RecordRequestPrivacy(PrivacyLabels{
+		GDPREnforced:   true,
+		GDPRTCFVersion: TCFVersionErr,
+	})
+	m.RecordRequestPrivacy(PrivacyLabels{
+		GDPREnforced:   true,
+		GDPRTCFVersion: TCFVersionV1,
+	})
+	m.RecordRequestPrivacy(PrivacyLabels{
+		GDPREnforced:   true,
+		GDPRTCFVersion: TCFVersionV2,
+	})
+	m.RecordRequestPrivacy(PrivacyLabels{
+		GDPREnforced:   true,
+		GDPRTCFVersion: TCFVersionV1,
+	})
+
+	assert.Equal(t, m.PrivacyCCPARequest.Count(), int64(2), "CCPA")
+	assert.Equal(t, m.PrivacyCCPARequestOptOut.Count(), int64(1), "CCPA Opt Out")
+	assert.Equal(t, m.PrivacyCOPPARequest.Count(), int64(1), "COPPA")
+	assert.Equal(t, m.PrivacyLMTRequest.Count(), int64(1), "LMT")
+	assert.Equal(t, m.PrivacyTCFRequestVersion[TCFVersionErr].Count(), int64(1), "TCF Err")
+	assert.Equal(t, m.PrivacyTCFRequestVersion[TCFVersionV1].Count(), int64(2), "TCF V1")
+	assert.Equal(t, m.PrivacyTCFRequestVersion[TCFVersionV2].Count(), int64(1), "TCF V2")
+}
+
 func ensureContainsBidTypeMetrics(t *testing.T, registry metrics.Registry, prefix string, mdm map[openrtb_ext.BidType]*MarkupDeliveryMetrics) {
 	ensureContains(t, registry, prefix+".banner.adm_bids_received", mdm[openrtb_ext.BidTypeBanner].AdmMeter)
 	ensureContains(t, registry, prefix+".banner.nurl_bids_received", mdm[openrtb_ext.BidTypeBanner].NurlMeter)
diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go
index 03daff0d..b722ab28 100644
--- a/pbsmetrics/prometheus/prometheus_test.go
+++ b/pbsmetrics/prometheus/prometheus_test.go
@@ -944,33 +944,96 @@ func TestTimeoutNotifications(t *testing.T) {
 
 }
 
-func TestTCFMetrics(t *testing.T) {
+func TestRecordRequestPrivacy(t *testing.T) {
 	m := createMetricsForTesting()
 
-	m.RecordTCFReq(pbsmetrics.TCFVersionToValue(0))
-	m.RecordTCFReq(pbsmetrics.TCFVersionToValue(1))
-	m.RecordTCFReq(pbsmetrics.TCFVersionToValue(2))
-	m.RecordTCFReq(pbsmetrics.TCFVersionToValue(1))
+	// CCPA
+	m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{
+		CCPAEnforced: true,
+		CCPAProvided: true,
+	})
+	m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{
+		CCPAEnforced: true,
+		CCPAProvided: false,
+	})
+	m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{
+		CCPAEnforced: false,
+		CCPAProvided: true,
+	})
+
+	// COPPA
+	m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{
+		COPPAEnforced: true,
+	})
 
-	assertCounterVecValue(t, "", "privacy_tcf:err", m.tcfVersion,
+	// LMT
+	m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{
+		LMTEnforced: true,
+	})
+
+	// GDPR
+	m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{
+		GDPREnforced:   true,
+		GDPRTCFVersion: pbsmetrics.TCFVersionErr,
+	})
+	m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{
+		GDPREnforced:   true,
+		GDPRTCFVersion: pbsmetrics.TCFVersionV1,
+	})
+	m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{
+		GDPREnforced:   true,
+		GDPRTCFVersion: pbsmetrics.TCFVersionV2,
+	})
+	m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{
+		GDPREnforced:   true,
+		GDPRTCFVersion: pbsmetrics.TCFVersionV1,
+	})
+
+	assertCounterVecValue(t, "", "privacy_ccpa", m.privacyCCPA,
+		float64(1),
+		prometheus.Labels{
+			sourceLabel: sourceRequest,
+			optOutLabel: "true",
+		})
+
+	assertCounterVecValue(t, "", "privacy_ccpa", m.privacyCCPA,
+		float64(1),
+		prometheus.Labels{
+			sourceLabel: sourceRequest,
+			optOutLabel: "false",
+		})
+
+	assertCounterVecValue(t, "", "privacy_coppa", m.privacyCOPPA,
+		float64(1),
+		prometheus.Labels{
+			sourceLabel: sourceRequest,
+		})
+
+	assertCounterVecValue(t, "", "privacy_lmt", m.privacyLMT,
+		float64(1),
+		prometheus.Labels{
+			sourceLabel: sourceRequest,
+		})
+
+	assertCounterVecValue(t, "", "privacy_tcf:err", m.privacyTCF,
 		float64(1),
 		prometheus.Labels{
-			versionLabel: "err",
 			sourceLabel:  sourceRequest,
+			versionLabel: "err",
 		})
 
-	assertCounterVecValue(t, "", "privacy_tcf:v1", m.tcfVersion,
+	assertCounterVecValue(t, "", "privacy_tcf:v1", m.privacyTCF,
 		float64(2),
 		prometheus.Labels{
-			versionLabel: "v1",
 			sourceLabel:  sourceRequest,
+			versionLabel: "v1",
 		})
 
-	assertCounterVecValue(t, "", "privacy_tcf:v2", m.tcfVersion,
+	assertCounterVecValue(t, "", "privacy_tcf:v2", m.privacyTCF,
 		float64(1),
 		prometheus.Labels{
-			versionLabel: "v2",
 			sourceLabel:  sourceRequest,
+			versionLabel: "v2",
 		})
 }