Repository: prebid/prebid-server
Pull request #1613 (2020-12-04 19:45:12)

Add TLS Handshake connection metrics


Patch:
diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go
index 08f492a7..91211a61 100644
--- a/adapters/rubicon/rubicon.go
+++ b/adapters/rubicon/rubicon.go
@@ -489,7 +489,18 @@ func (a *RubiconAdapter) Call(ctx context.Context, req *pbs.PBSRequest, bidder *
 		rubiReq.Device = &deviceCopy
 
 		if thisImp.Video != nil {
-			videoExt := rubiconVideoExt{Skip: params.Video.Skip, SkipDelay: params.Video.SkipDelay, RP: rubiconVideoExtRP{SizeID: params.Video.VideoSizeID}}
+
+			videoSizeId := params.Video.VideoSizeID
+			if videoSizeId == 0 {
+				resolvedSizeId, err := resolveVideoSizeId(thisImp.Video.Placement, thisImp.Instl, thisImp.ID)
+				if err == nil {
+					videoSizeId = resolvedSizeId
+				} else {
+					continue
+				}
+			}
+
+			videoExt := rubiconVideoExt{Skip: params.Video.Skip, SkipDelay: params.Video.SkipDelay, RP: rubiconVideoExtRP{SizeID: videoSizeId}}
 			thisImp.Video.Ext, err = json.Marshal(&videoExt)
 		} else {
 			primarySizeID, altSizeIDs, err := parseRubiconSizes(unit.Sizes)
@@ -601,6 +612,24 @@ func (a *RubiconAdapter) Call(ctx context.Context, req *pbs.PBSRequest, bidder *
 	return bids, nil
 }
 
+func resolveVideoSizeId(placement openrtb.VideoPlacementType, instl int8, impId string) (sizeID int, err error) {
+	if placement != 0 {
+		if placement == 1 {
+			return 201, nil
+		}
+		if placement == 3 {
+			return 203, nil
+		}
+	}
+
+	if instl == 1 {
+		return 202, nil
+	}
+	return 0, &errortypes.BadInput{
+		Message: fmt.Sprintf("video.size_id can not be resolved in impression with id : %s", impId),
+	}
+}
+
 func appendTrackerToUrl(uri string, tracker string) (res string) {
 	// Append integration method. Adapter init happens once
 	urlObject, err := url.Parse(uri)
@@ -771,11 +800,16 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adap
 
 		isVideo := isVideo(thisImp)
 		if isVideo {
-			if rubiconExt.Video.VideoSizeID == 0 {
-				errs = append(errs, &errortypes.BadInput{
-					Message: fmt.Sprintf("imp[%d].ext.bidder.rubicon.video.size_id must be defined for video impression", i),
-				})
-				continue
+			videoCopy := *thisImp.Video
+
+			videoSizeId := rubiconExt.Video.VideoSizeID
+			if videoSizeId == 0 {
+				resolvedSizeId, err := resolveVideoSizeId(thisImp.Video.Placement, thisImp.Instl, thisImp.ID)
+				if err != nil {
+					errs = append(errs, err)
+					continue
+				}
+				videoSizeId = resolvedSizeId
 			}
 
 			// if imp.ext.is_rewarded_inventory = 1, set imp.video.ext.videotype = "rewarded"
@@ -783,9 +817,7 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adap
 			if bidderExt.Prebid != nil && bidderExt.Prebid.IsRewardedInventory == 1 {
 				videoType = "rewarded"
 			}
-
-			videoCopy := *thisImp.Video
-			videoExt := rubiconVideoExt{Skip: rubiconExt.Video.Skip, SkipDelay: rubiconExt.Video.SkipDelay, VideoType: videoType, RP: rubiconVideoExtRP{SizeID: rubiconExt.Video.VideoSizeID}}
+			videoExt := rubiconVideoExt{Skip: rubiconExt.Video.Skip, SkipDelay: rubiconExt.Video.SkipDelay, VideoType: videoType, RP: rubiconVideoExtRP{SizeID: videoSizeId}}
 			videoCopy.Ext, err = json.Marshal(&videoExt)
 			thisImp.Video = &videoCopy
 			thisImp.Banner = nil
diff --git a/config/adapter.go b/config/adapter.go
new file mode 100644
index 00000000..ff262b18
--- /dev/null
+++ b/config/adapter.go
@@ -0,0 +1,136 @@
+package config
+
+import (
+	"fmt"
+	"text/template"
+
+	validator "github.com/asaskevich/govalidator"
+	"github.com/prebid/prebid-server/macros"
+)
+
+type Adapter struct {
+	Endpoint string `mapstructure:"endpoint"` // Required
+	// UserSyncURL is the URL returned by /cookie_sync for this Bidder. It is _usually_ optional.
+	// If not defined, sensible defaults will be derived based on the config.external_url.
+	// Note that some Bidders don't have sensible defaults, because their APIs require an ID that will vary
+	// from one PBS host to another.
+	//
+	// For these bidders, there will be a warning logged on startup that usersyncs will not work if you have not
+	// defined one in the app config. Check your app logs for more info.
+	//
+	// This value will be interpreted as a Golang Template. At runtime, the following Template variables will be replaced.
+	//
+	//   {{.GDPR}}      -- This will be replaced with the "gdpr" property sent to /cookie_sync
+	//   {{.Consent}}   -- This will be replaced with the "consent" property sent to /cookie_sync
+	//   {{.USPrivacy}} -- This will be replaced with the "us_privacy" property sent to /cookie_sync
+	//
+	// For more info on templates, see: https://golang.org/pkg/text/template/
+	UserSyncURL      string `mapstructure:"usersync_url"`
+	Disabled         bool   `mapstructure:"disabled"`
+	ExtraAdapterInfo string `mapstructure:"extra_info"`
+
+	// needed for Rubicon
+	XAPI AdapterXAPI `mapstructure:"xapi"`
+
+	// needed for Facebook
+	PlatformID string `mapstructure:"platform_id"`
+	AppSecret  string `mapstructure:"app_secret"`
+}
+
+type AdapterXAPI struct {
+	Username string `mapstructure:"username"`
+	Password string `mapstructure:"password"`
+	Tracker  string `mapstructure:"tracker"`
+}
+
+// validateAdapters validates adapter's endpoint and user sync URL
+func validateAdapters(adapterMap map[string]Adapter, errs []error) []error {
+	for adapterName, adapter := range adapterMap {
+		if !adapter.Disabled {
+			// Verify that every adapter has a valid endpoint associated with it
+			errs = validateAdapterEndpoint(adapter.Endpoint, adapterName, errs)
+
+			// Verify that valid user_sync URLs are specified in the config
+			errs = validateAdapterUserSyncURL(adapter.UserSyncURL, adapterName, errs)
+		}
+	}
+	return errs
+}
+
+const (
+	dummyHost        string = "dummyhost.com"
+	dummyPublisherID string = "12"
+	dummyAccountID   string = "some_account"
+	dummyGDPR        string = "0"
+	dummyGDPRConsent string = "someGDPRConsentString"
+	dummyCCPA        string = "1NYN"
+)
+
+// validateAdapterEndpoint makes sure that an adapter has a valid endpoint
+// associated with it
+func validateAdapterEndpoint(endpoint string, adapterName string, errs []error) []error {
+	if endpoint == "" {
+		return append(errs, fmt.Errorf("There's no default endpoint available for %s. Calls to this bidder/exchange will fail. "+
+			"Please set adapters.%s.endpoint in your app config", adapterName, adapterName))
+	}
+
+	// Create endpoint template
+	endpointTemplate, err := template.New("endpointTemplate").Parse(endpoint)
+	if err != nil {
+		return append(errs, fmt.Errorf("Invalid endpoint template: %s for adapter: %s. %v", endpoint, adapterName, err))
+	}
+	// Resolve macros (if any) in the endpoint URL
+	resolvedEndpoint, err := macros.ResolveMacros(*endpointTemplate, macros.EndpointTemplateParams{
+		Host:        dummyHost,
+		PublisherID: dummyPublisherID,
+		AccountID:   dummyAccountID,
+	})
+	if err != nil {
+		return append(errs, fmt.Errorf("Unable to resolve endpoint: %s for adapter: %s. %v", endpoint, adapterName, err))
+	}
+	// Validate the resolved endpoint
+	//
+	// Validating using both IsURL and IsRequestURL because IsURL allows relative paths
+	// whereas IsRequestURL requires absolute path but fails to check other valid URL
+	// format constraints.
+	//
+	// For example: IsURL will allow "abcd.com" but IsRequestURL won't
+	// IsRequestURL will allow "http://http://abcd.com" but IsURL won't
+	if !validator.IsURL(resolvedEndpoint) || !validator.IsRequestURL(resolvedEndpoint) {
+		errs = append(errs, fmt.Errorf("The endpoint: %s for %s is not a valid URL", resolvedEndpoint, adapterName))
+	}
+	return errs
+}
+
+// validateAdapterUserSyncURL validates an adapter's user sync URL if it is set
+func validateAdapterUserSyncURL(userSyncURL string, adapterName string, errs []error) []error {
+	if userSyncURL != "" {
+		// Create user_sync URL template
+		userSyncTemplate, err := template.New("userSyncTemplate").Parse(userSyncURL)
+		if err != nil {
+			return append(errs, fmt.Errorf("Invalid user sync URL template: %s for adapter: %s. %v", userSyncURL, adapterName, err))
+		}
+		// Resolve macros (if any) in the user_sync URL
+		dummyMacroValues := macros.UserSyncTemplateParams{
+			GDPR:        dummyGDPR,
+			GDPRConsent: dummyGDPRConsent,
+			USPrivacy:   dummyCCPA,
+		}
+		resolvedUserSyncURL, err := macros.ResolveMacros(*userSyncTemplate, dummyMacroValues)
+		if err != nil {
+			return append(errs, fmt.Errorf("Unable to resolve user sync URL: %s for adapter: %s. %v", userSyncURL, adapterName, err))
+		}
+		// Validate the resolved user sync URL
+		//
+		// Validating using both IsURL and IsRequestURL because IsURL allows relative paths
+		// whereas IsRequestURL requires absolute path but fails to check other valid URL
+		// format constraints.
+		//
+		// For example: IsURL will allow "abcd.com" but IsRequestURL won't
+		// IsRequestURL will allow "http://http://abcd.com" but IsURL won't
+		if !validator.IsURL(resolvedUserSyncURL) || !validator.IsRequestURL(resolvedUserSyncURL) {
+			errs = append(errs, fmt.Errorf("The user_sync URL: %s for %s is invalid", resolvedUserSyncURL, adapterName))
+		}
+	}
+	return errs
+}
diff --git a/config/config.go b/config/config.go
index 8c2bdfad..a51ddb67 100755
--- a/config/config.go
+++ b/config/config.go
@@ -7,16 +7,12 @@ import (
 	"net/url"
 	"reflect"
 	"strings"
-	"text/template"
 	"time"
 
 	"github.com/golang/glog"
 	"github.com/prebid/prebid-server/errortypes"
-	"github.com/prebid/prebid-server/macros"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/spf13/viper"
-
-	validator "github.com/asaskevich/govalidator"
 )
 
 // Configuration specifies the static application config.
@@ -201,7 +197,7 @@ type GDPR struct {
 	NonStandardPublisherMap map[string]struct{}
 	TCF1                    TCF1 `mapstructure:"tcf1"`
 	TCF2                    TCF2 `mapstructure:"tcf2"`
-	AMPException            bool `mapstructure:"amp_exception"`
+	AMPException            bool `mapstructure:"amp_exception"` // Deprecated: Use account-level GDPR settings (gdpr.integration_enabled.amp) instead
 	// EEACountries (EEA = European Economic Area) are a list of countries where we should assume GDPR applies.
 	// If the gdpr flag is unset in a request, but geo.country is set, we will assume GDPR applies if and only
 	// if the country matches one on this list. If both the GDPR flag and country are not set, we default
@@ -214,6 +210,12 @@ func (cfg *GDPR) validate(errs []error) []error {
 	if cfg.HostVendorID < 0 || cfg.HostVendorID > 0xffff {
 		errs = append(errs, fmt.Errorf("gdpr.host_vendor_id must be in the range [0, %d]. Got %d", 0xffff, cfg.HostVendorID))
 	}
+	if cfg.HostVendorID == 0 {
+		glog.Warning("gdpr.host_vendor_id was not specified. Host company GDPR checks will be skipped.")
+	}
+	if cfg.AMPException == true {
+		glog.Warning("gdpr.amp_exception is deprecated and will be removed in a future version. If you need to disable GDPR for AMP, you may do so per-account (gdpr.integration_enabled.amp) or at the host level for the default account (account_defaults.gdpr.integration_enabled.amp).")
+	}
 	return errs
 }
 
@@ -332,133 +334,6 @@ type RequestTimeoutHeaders struct {
 	RequestTimeoutInQueue string `mapstructure:"request_timeout_in_queue"`
 }
 
-const (
-	dummyHost        string = "dummyhost.com"
-	dummyPublisherID string = "12"
-	dummyAccountID   string = "some_account"
-	dummyGDPR        string = "0"
-	dummyGDPRConsent string = "someGDPRConsentString"
-	dummyCCPA        string = "1NYN"
-)
-
-type Adapter struct {
-	Endpoint string `mapstructure:"endpoint"` // Required
-	// UserSyncURL is the URL returned by /cookie_sync for this Bidder. It is _usually_ optional.
-	// If not defined, sensible defaults will be derived based on the config.external_url.
-	// Note that some Bidders don't have sensible defaults, because their APIs require an ID that will vary
-	// from one PBS host to another.
-	//
-	// For these bidders, there will be a warning logged on startup that usersyncs will not work if you have not
-	// defined one in the app config. Check your app logs for more info.
-	//
-	// This value will be interpreted as a Golang Template. At runtime, the following Template variables will be replaced.
-	//
-	//   {{.GDPR}}      -- This will be replaced with the "gdpr" property sent to /cookie_sync
-	//   {{.Consent}}   -- This will be replaced with the "consent" property sent to /cookie_sync
-	//   {{.USPrivacy}} -- This will be replaced with the "us_privacy" property sent to /cookie_sync
-	//
-	// For more info on templates, see: https://golang.org/pkg/text/template/
-	UserSyncURL      string `mapstructure:"usersync_url"`
-	Disabled         bool   `mapstructure:"disabled"`
-	ExtraAdapterInfo string `mapstructure:"extra_info"`
-
-	// needed for Rubicon
-	XAPI AdapterXAPI `mapstructure:"xapi"`
-
-	// needed for Facebook
-	PlatformID string `mapstructure:"platform_id"`
-	AppSecret  string `mapstructure:"app_secret"`
-}
-
-type AdapterXAPI struct {
-	Username string `mapstructure:"username"`
-	Password string `mapstructure:"password"`
-	Tracker  string `mapstructure:"tracker"`
-}
-
-// validateAdapterEndpoint makes sure that an adapter has a valid endpoint
-// associated with it
-func validateAdapterEndpoint(endpoint string, adapterName string, errs []error) []error {
-	if endpoint == "" {
-		return append(errs, fmt.Errorf("There's no default endpoint available for %s. Calls to this bidder/exchange will fail. "+
-			"Please set adapters.%s.endpoint in your app config", adapterName, adapterName))
-	}
-
-	// Create endpoint template
-	endpointTemplate, err := template.New("endpointTemplate").Parse(endpoint)
-	if err != nil {
-		return append(errs, fmt.Errorf("Invalid endpoint template: %s for adapter: %s. %v", endpoint, adapterName, err))
-	}
-	// Resolve macros (if any) in the endpoint URL
-	resolvedEndpoint, err := macros.ResolveMacros(*endpointTemplate, macros.EndpointTemplateParams{
-		Host:        dummyHost,
-		PublisherID: dummyPublisherID,
-		AccountID:   dummyAccountID,
-	})
-	if err != nil {
-		return append(errs, fmt.Errorf("Unable to resolve endpoint: %s for adapter: %s. %v", endpoint, adapterName, err))
-	}
-	// Validate the resolved endpoint
-	//
-	// Validating using both IsURL and IsRequestURL because IsURL allows relative paths
-	// whereas IsRequestURL requires absolute path but fails to check other valid URL
-	// format constraints.
-	//
-	// For example: IsURL will allow "abcd.com" but IsRequestURL won't
-	// IsRequestURL will allow "http://http://abcd.com" but IsURL won't
-	if !validator.IsURL(resolvedEndpoint) || !validator.IsRequestURL(resolvedEndpoint) {
-		errs = append(errs, fmt.Errorf("The endpoint: %s for %s is not a valid URL", resolvedEndpoint, adapterName))
-	}
-	return errs
-}
-
-// validateAdapterUserSyncURL validates an adapter's user sync URL if it is set
-func validateAdapterUserSyncURL(userSyncURL string, adapterName string, errs []error) []error {
-	if userSyncURL != "" {
-		// Create user_sync URL template
-		userSyncTemplate, err := template.New("userSyncTemplate").Parse(userSyncURL)
-		if err != nil {
-			return append(errs, fmt.Errorf("Invalid user sync URL template: %s for adapter: %s. %v", userSyncURL, adapterName, err))
-		}
-		// Resolve macros (if any) in the user_sync URL
-		dummyMacroValues := macros.UserSyncTemplateParams{
-			GDPR:        dummyGDPR,
-			GDPRConsent: dummyGDPRConsent,
-			USPrivacy:   dummyCCPA,
-		}
-		resolvedUserSyncURL, err := macros.ResolveMacros(*userSyncTemplate, dummyMacroValues)
-		if err != nil {
-			return append(errs, fmt.Errorf("Unable to resolve user sync URL: %s for adapter: %s. %v", userSyncURL, adapterName, err))
-		}
-		// Validate the resolved user sync URL
-		//
-		// Validating using both IsURL and IsRequestURL because IsURL allows relative paths
-		// whereas IsRequestURL requires absolute path but fails to check other valid URL
-		// format constraints.
-		//
-		// For example: IsURL will allow "abcd.com" but IsRequestURL won't
-		// IsRequestURL will allow "http://http://abcd.com" but IsURL won't
-		if !validator.IsURL(resolvedUserSyncURL) || !validator.IsRequestURL(resolvedUserSyncURL) {
-			errs = append(errs, fmt.Errorf("The user_sync URL: %s for %s is invalid", resolvedUserSyncURL, adapterName))
-		}
-	}
-	return errs
-}
-
-// validateAdapters validates adapter's endpoint and user sync URL
-func validateAdapters(adapterMap map[string]Adapter, errs []error) []error {
-	for adapterName, adapter := range adapterMap {
-		if !adapter.Disabled {
-			// Verify that every adapter has a valid endpoint associated with it
-			errs = validateAdapterEndpoint(adapter.Endpoint, adapterName, errs)
-
-			// Verify that valid user_sync URLs are specified in the config
-			errs = validateAdapterUserSyncURL(adapter.UserSyncURL, adapterName, errs)
-		}
-	}
-	return errs
-}
-
 type Metrics struct {
 	Influxdb   InfluxMetrics     `mapstructure:"influxdb"`
 	Prometheus PrometheusMetrics `mapstructure:"prometheus"`
diff --git a/exchange/bidder.go b/exchange/bidder.go
index a230041d..cba79eee 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -3,6 +3,7 @@ package exchange
 import (
 	"bytes"
 	"context"
+	"crypto/tls"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -445,7 +446,7 @@ type httpCallInfo struct {
 // endpoint is established, we can keep track of whether the connection was newly created, reused, and
 // the time from the connection request, to the connection creation.
 func (bidder *bidderAdapter) addClientTrace(ctx context.Context) context.Context {
-	var connStart, dnsStart time.Time
+	var connStart, dnsStart, tlsStart time.Time
 
 	trace := &httptrace.ClientTrace{
 		// GetConn is called before a connection is created or retrieved from an idle pool
@@ -468,6 +469,16 @@ func (bidder *bidderAdapter) addClientTrace(ctx context.Context) context.Context
 
 			bidder.me.RecordDNSTime(dnsLookupTime)
 		},
+
+		TLSHandshakeStart: func() {
+			tlsStart = time.Now()
+		},
+
+		TLSHandshakeDone: func(tls.ConnectionState, error) {
+			tlsHandshakeTime := time.Now().Sub(tlsStart)
+
+			bidder.me.RecordTLSHandshakeTime(tlsHandshakeTime)
+		},
 	}
 	return httptrace.WithClientTrace(ctx, trace)
 }
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 7a9fbc7c..c4d25662 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -95,6 +95,8 @@ func NewExchange(adapters map[openrtb_ext.BidderName]adaptedBidder, cache prebid
 	}
 }
 
+// AuctionRequest holds the bid request for the auction
+// and all other information needed to process that request
 type AuctionRequest struct {
 	BidRequest  *openrtb.BidRequest
 	Account     config.Account
@@ -107,6 +109,15 @@ type AuctionRequest struct {
 	LegacyLabels pbsmetrics.Labels
 }
 
+// BidderRequest holds the bidder specific request and all other
+// information needed to process that bidder request.
+type BidderRequest struct {
+	BidRequest     *openrtb.BidRequest
+	BidderName     openrtb_ext.BidderName
+	BidderCoreName openrtb_ext.BidderName
+	BidderLabels   pbsmetrics.AdapterLabels
+}
+
 func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *DebugLog) (*openrtb.BidResponse, error) {
 	var err error
 	requestExt, err := extractBidRequestExt(r.BidRequest)
@@ -133,13 +144,12 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 	usersyncIfAmbiguous := e.parseUsersyncIfAmbiguous(r.BidRequest)
 
 	// Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder
-	blabels := make(map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels)
-	cleanRequests, aliases, privacyLabels, errs := cleanOpenRTBRequests(ctx, r.BidRequest, requestExt, r.UserSyncs, blabels, r.LegacyLabels, e.gDPR, usersyncIfAmbiguous, e.privacyConfig, &r.Account)
+	bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(ctx, r, requestExt, e.gDPR, usersyncIfAmbiguous, e.privacyConfig)
 
 	e.me.RecordRequestPrivacy(privacyLabels)
 
 	// List of bidders we have requests for.
-	liveAdapters := listBiddersWithRequests(cleanRequests)
+	liveAdapters := listBiddersWithRequests(bidderRequests)
 
 	// 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.
@@ -149,7 +159,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 	// Get currency rates conversions for the auction
 	conversions := e.currencyConverter.Rates()
 
-	adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, cleanRequests, aliases, bidAdjustmentFactors, blabels, conversions)
+	adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions)
 
 	var auc *auction
 	var cacheErrs []error
@@ -322,37 +332,42 @@ func (e *exchange) makeAuctionContext(ctx context.Context, needsCache bool) (auc
 }
 
 // This piece sends all the requests to the bidder adapters and gathers the results.
-func (e *exchange) getAllBids(ctx context.Context, cleanRequests map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, bidAdjustments map[string]float64, blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels, conversions currencies.Conversions) (map[openrtb_ext.BidderName]*pbsOrtbSeatBid, map[openrtb_ext.BidderName]*seatResponseExtra, bool) {
+func (e *exchange) getAllBids(
+	ctx context.Context,
+	bidderRequests []BidderRequest,
+	bidAdjustments map[string]float64,
+	conversions currencies.Conversions) (
+	map[openrtb_ext.BidderName]*pbsOrtbSeatBid,
+	map[openrtb_ext.BidderName]*seatResponseExtra, bool) {
 	// Set up pointers to the bid results
-	adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid, len(cleanRequests))
-	adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, len(cleanRequests))
-	chBids := make(chan *bidResponseWrapper, len(cleanRequests))
+	adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid, len(bidderRequests))
+	adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, len(bidderRequests))
+	chBids := make(chan *bidResponseWrapper, len(bidderRequests))
 	bidsFound := false
 
-	for bidderName, req := range cleanRequests {
+	for _, bidder := range bidderRequests {
 		// Here we actually call the adapters and collect the bids.
-		coreBidder := resolveBidder(string(bidderName), aliases)
-		bidderRunner := e.recoverSafely(cleanRequests, func(aName openrtb_ext.BidderName, coreBidder openrtb_ext.BidderName, request *openrtb.BidRequest, bidlabels *pbsmetrics.AdapterLabels, conversions currencies.Conversions) {
+		bidderRunner := e.recoverSafely(bidderRequests, func(bidderRequest BidderRequest, conversions currencies.Conversions) {
 			// Passing in aName so a doesn't change out from under the go routine
-			if bidlabels.Adapter == "" {
-				glog.Errorf("Exchange: bidlables for %s (%s) missing adapter string", aName, coreBidder)
-				bidlabels.Adapter = coreBidder
+			if bidderRequest.BidderLabels.Adapter == "" {
+				glog.Errorf("Exchange: bidlables for %s (%s) missing adapter string", bidderRequest.BidderName, bidderRequest.BidderCoreName)
+				bidderRequest.BidderLabels.Adapter = bidderRequest.BidderCoreName
 			}
 			brw := new(bidResponseWrapper)
-			brw.bidder = aName
+			brw.bidder = bidderRequest.BidderName
 			// Defer basic metrics to insure we capture them after all the values have been set
 			defer func() {
-				e.me.RecordAdapterRequest(*bidlabels)
+				e.me.RecordAdapterRequest(bidderRequest.BidderLabels)
 			}()
 			start := time.Now()
 
 			adjustmentFactor := 1.0
-			if givenAdjustment, ok := bidAdjustments[string(aName)]; ok {
+			if givenAdjustment, ok := bidAdjustments[string(bidderRequest.BidderName)]; ok {
 				adjustmentFactor = givenAdjustment
 			}
 			var reqInfo adapters.ExtraRequestInfo
-			reqInfo.PbsEntryPoint = bidlabels.RType
-			bids, err := e.adapterMap[coreBidder].requestBid(ctx, request, aName, adjustmentFactor, conversions, &reqInfo)
+			reqInfo.PbsEntryPoint = bidderRequest.BidderLabels.RType
+			bids, err := e.adapterMap[bidderRequest.BidderCoreName].requestBid(ctx, bidderRequest.BidRequest, bidderRequest.BidderName, adjustmentFactor, conversions, &reqInfo)
 
 			// Add in time reporting
 			elapsed := time.Since(start)
@@ -365,26 +380,26 @@ func (e *exchange) getAllBids(ctx context.Context, cleanRequests map[openrtb_ext
 			}
 
 			// Timing statistics
-			e.me.RecordAdapterTime(*bidlabels, time.Since(start))
+			e.me.RecordAdapterTime(bidderRequest.BidderLabels, time.Since(start))
 			serr := errsToBidderErrors(err)
-			bidlabels.AdapterBids = bidsToMetric(brw.adapterBids)
-			bidlabels.AdapterErrors = errorsToMetric(err)
+			bidderRequest.BidderLabels.AdapterBids = bidsToMetric(brw.adapterBids)
+			bidderRequest.BidderLabels.AdapterErrors = errorsToMetric(err)
 			// Append any bid validation errors to the error list
 			ae.Errors = serr
 			brw.adapterExtra = ae
 			if bids != nil {
 				for _, bid := range bids.bids {
 					var cpm = float64(bid.bid.Price * 1000)
-					e.me.RecordAdapterPrice(*bidlabels, cpm)
-					e.me.RecordAdapterBidReceived(*bidlabels, bid.bidType, bid.bid.AdM != "")
+					e.me.RecordAdapterPrice(bidderRequest.BidderLabels, cpm)
+					e.me.RecordAdapterBidReceived(bidderRequest.BidderLabels, bid.bidType, bid.bid.AdM != "")
 				}
 			}
 			chBids <- brw
 		}, chBids)
-		go bidderRunner(bidderName, coreBidder, req, blabels[coreBidder], conversions)
+		go bidderRunner(bidder, conversions)
 	}
 	// Wait for the bidders to do their thing
-	for i := 0; i < len(cleanRequests); i++ {
+	for i := 0; i < len(bidderRequests); i++ {
 		brw := <-chBids
 
 		//if bidder returned no bids back - remove bidder from further processing
@@ -402,15 +417,17 @@ func (e *exchange) getAllBids(ctx context.Context, cleanRequests map[openrtb_ext
 	return adapterBids, adapterExtra, bidsFound
 }
 
-func (e *exchange) recoverSafely(cleanRequests map[openrtb_ext.BidderName]*openrtb.BidRequest, inner func(openrtb_ext.BidderName, openrtb_ext.BidderName, *openrtb.BidRequest, *pbsmetrics.AdapterLabels, currencies.Conversions), chBids chan *bidResponseWrapper) func(openrtb_ext.BidderName, openrtb_ext.BidderName, *openrtb.BidRequest, *pbsmetrics.AdapterLabels, currencies.Conversions) {
-	return func(aName openrtb_ext.BidderName, coreBidder openrtb_ext.BidderName, request *openrtb.BidRequest, bidlabels *pbsmetrics.AdapterLabels, conversions currencies.Conversions) {
+func (e *exchange) recoverSafely(bidderRequests []BidderRequest,
+	inner func(BidderRequest, currencies.Conversions),
+	chBids chan *bidResponseWrapper) func(BidderRequest, currencies.Conversions) {
+	return func(bidderRequest BidderRequest, conversions currencies.Conversions) {
 		defer func() {
 			if r := recover(); r != nil {
 
 				allBidders := ""
 				sb := strings.Builder{}
-				for k := range cleanRequests {
-					sb.WriteString(string(k))
+				for _, bidder := range bidderRequests {
+					sb.WriteString(bidder.BidderName.String())
 					sb.WriteString(",")
 				}
 				if sb.Len() > 0 {
@@ -419,15 +436,15 @@ func (e *exchange) recoverSafely(cleanRequests map[openrtb_ext.BidderName]*openr
 
 				glog.Errorf("OpenRTB auction recovered panic from Bidder %s: %v. "+
 					"Account id: %s, All Bidders: %s, Stack trace is: %v",
-					coreBidder, r, bidlabels.PubID, allBidders, string(debug.Stack()))
-				e.me.RecordAdapterPanic(*bidlabels)
+					bidderRequest.BidderCoreName, r, bidderRequest.BidderLabels.PubID, allBidders, string(debug.Stack()))
+				e.me.RecordAdapterPanic(bidderRequest.BidderLabels)
 				// Let the master request know that there is no data here
 				brw := new(bidResponseWrapper)
 				brw.adapterExtra = new(seatResponseExtra)
 				chBids <- brw
 			}
 		}()
-		inner(aName, coreBidder, request, bidlabels, conversions)
+		inner(bidderRequest, conversions)
 	}
 }
 
@@ -867,11 +884,11 @@ func buildCacheURL(cache prebid_cache_client.Client, uuid string) string {
 	return strings.TrimPrefix(cacheURL.String(), "//")
 }
 
-func listBiddersWithRequests(cleanRequests map[openrtb_ext.BidderName]*openrtb.BidRequest) []openrtb_ext.BidderName {
-	liveAdapters := make([]openrtb_ext.BidderName, len(cleanRequests))
+func listBiddersWithRequests(bidderRequests []BidderRequest) []openrtb_ext.BidderName {
+	liveAdapters := make([]openrtb_ext.BidderName, len(bidderRequests))
 	i := 0
-	for a := range cleanRequests {
-		liveAdapters[i] = a
+	for _, bidderRequest := range bidderRequests {
+		liveAdapters[i] = bidderRequest.BidderName
 		i++
 	}
 	// Randomize the list of adapters to make the auction more fair
diff --git a/exchange/utils.go b/exchange/utils.go
index 285f650c..4a1c8bb5 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -53,47 +53,43 @@ func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext
 //   2. Every BidRequest.Imp[] requested Bids from the Bidder who keys it.
 //   3. BidRequest.User.BuyerUID will be set to that Bidder's ID.
 func cleanOpenRTBRequests(ctx context.Context,
-	orig *openrtb.BidRequest,
+	req AuctionRequest,
 	requestExt *openrtb_ext.ExtRequest,
-	usersyncs IdFetcher,
-	blables map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels,
-	labels pbsmetrics.Labels,
 	gDPR gdpr.Permissions,
 	usersyncIfAmbiguous bool,
-	privacyConfig config.Privacy,
-	account *config.Account) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, privacyLabels pbsmetrics.PrivacyLabels, errs []error) {
+	privacyConfig config.Privacy) (bidderRequests []BidderRequest, privacyLabels pbsmetrics.PrivacyLabels, errs []error) {
 
-	impsByBidder, errs := splitImps(orig.Imp)
+	impsByBidder, errs := splitImps(req.BidRequest.Imp)
 	if len(errs) > 0 {
 		return
 	}
 
-	aliases, errs = parseAliases(orig)
+	aliases, errs := parseAliases(req.BidRequest)
 	if len(errs) > 0 {
 		return
 	}
 
-	requestsByBidder, errs = splitBidRequest(orig, requestExt, impsByBidder, aliases, usersyncs, blables, labels)
+	bidderRequests, errs = getAuctionBidderRequests(req, requestExt, impsByBidder, aliases)
 
-	if len(requestsByBidder) == 0 {
+	if len(bidderRequests) == 0 {
 		return
 	}
 
-	gdpr := extractGDPR(orig, usersyncIfAmbiguous)
-	consent := extractConsent(orig)
-	ampGDPRException := (labels.RType == pbsmetrics.ReqTypeAMP) && gDPR.AMPException()
+	gdpr := extractGDPR(req.BidRequest, usersyncIfAmbiguous)
+	consent := extractConsent(req.BidRequest)
+	ampGDPRException := (req.LegacyLabels.RType == pbsmetrics.ReqTypeAMP) && gDPR.AMPException()
 
-	ccpaEnforcer, err := extractCCPA(orig, privacyConfig, account, aliases, integrationTypeMap[labels.RType])
+	ccpaEnforcer, err := extractCCPA(req.BidRequest, privacyConfig, &req.Account, aliases, integrationTypeMap[req.LegacyLabels.RType])
 	if err != nil {
 		errs = append(errs, err)
 		return
 	}
 
-	lmtEnforcer := extractLMT(orig, privacyConfig)
+	lmtEnforcer := extractLMT(req.BidRequest, privacyConfig)
 
 	// request level privacy policies
 	privacyEnforcement := privacy.Enforcement{
-		COPPA: orig.Regs != nil && orig.Regs.COPPA == 1,
+		COPPA: req.BidRequest.Regs != nil && req.BidRequest.Regs.COPPA == 1,
 		LMT:   lmtEnforcer.ShouldEnforce(unknownBidder),
 	}
 
@@ -102,7 +98,7 @@ func cleanOpenRTBRequests(ctx context.Context,
 	privacyLabels.COPPAEnforced = privacyEnforcement.COPPA
 	privacyLabels.LMTEnforced = lmtEnforcer.ShouldEnforce(unknownBidder)
 
-	gdprEnabled := gdprEnabled(account, privacyConfig, integrationTypeMap[labels.RType])
+	gdprEnabled := gdprEnabled(&req.Account, privacyConfig, integrationTypeMap[req.LegacyLabels.RType])
 
 	if gdpr == 1 && gdprEnabled {
 		privacyLabels.GDPREnforced = true
@@ -114,16 +110,14 @@ func cleanOpenRTBRequests(ctx context.Context,
 	}
 
 	// bidder level privacy policies
-	for bidder, bidReq := range requestsByBidder {
+	for _, bidderRequest := range bidderRequests {
 		// CCPA
-		privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidder.String())
+		privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String())
 
 		// GDPR
 		if gdpr == 1 && gdprEnabled {
-			coreBidder := resolveBidder(bidder.String(), aliases)
-
-			var publisherID = labels.PubID
-			_, geo, id, err := gDPR.PersonalInfoAllowed(ctx, coreBidder, publisherID, consent)
+			var publisherID = req.LegacyLabels.PubID
+			_, geo, id, err := gDPR.PersonalInfoAllowed(ctx, bidderRequest.BidderCoreName, publisherID, consent)
 			privacyEnforcement.GDPRGeo = !geo && err == nil
 			privacyEnforcement.GDPRID = !id && err == nil
 		} else {
@@ -131,7 +125,7 @@ func cleanOpenRTBRequests(ctx context.Context,
 			privacyEnforcement.GDPRID = false
 		}
 
-		privacyEnforcement.Apply(bidReq, ampGDPRException)
+		privacyEnforcement.Apply(bidderRequest.BidRequest, ampGDPRException)
 	}
 
 	return
@@ -177,16 +171,14 @@ func extractLMT(orig *openrtb.BidRequest, privacyConfig config.Privacy) privacy.
 	}
 }
 
-func splitBidRequest(req *openrtb.BidRequest,
+func getAuctionBidderRequests(req AuctionRequest,
 	requestExt *openrtb_ext.ExtRequest,
 	impsByBidder map[string][]openrtb.Imp,
-	aliases map[string]string,
-	usersyncs IdFetcher,
-	blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels,
-	labels pbsmetrics.Labels) (map[openrtb_ext.BidderName]*openrtb.BidRequest, []error) {
+	aliases map[string]string) ([]BidderRequest, []error) {
+
+	bidderRequests := make([]BidderRequest, 0, len(impsByBidder))
 
-	requestsByBidder := make(map[openrtb_ext.BidderName]*openrtb.BidRequest, len(impsByBidder))
-	explicitBuyerUIDs, err := extractBuyerUIDs(req.User)
+	explicitBuyerUIDs, err := extractBuyerUIDs(req.BidRequest.User)
 	if err != nil {
 		return nil, []error{err}
 	}
@@ -198,36 +190,41 @@ func splitBidRequest(req *openrtb.BidRequest,
 		return nil, []error{err}
 	}
 
-	reqExt, err := getExtJson(req, requestExt)
+	reqExt, err := getExtJson(req.BidRequest, requestExt)
 	if err != nil {
 		return nil, []error{err}
 	}
 
 	for bidder, imps := range impsByBidder {
-		reqCopy := *req
 		coreBidder := resolveBidder(bidder, aliases)
-		newLabel := pbsmetrics.AdapterLabels{
-			Source:      labels.Source,
-			RType:       labels.RType,
-			Adapter:     coreBidder,
-			PubID:       labels.PubID,
-			CookieFlag:  labels.CookieFlag,
-			AdapterBids: pbsmetrics.AdapterBidPresent,
+
+		reqCopy := *req.BidRequest
+		reqCopy.Imp = imps
+		reqCopy.Ext = reqExt
+		prepareSource(&reqCopy, bidder, sChainsByBidder)
+
+		bidder := BidderRequest{
+			BidderName:     openrtb_ext.BidderName(bidder),
+			BidderCoreName: coreBidder,
+			BidRequest:     &reqCopy,
+			BidderLabels: pbsmetrics.AdapterLabels{
+				Source:      req.LegacyLabels.Source,
+				RType:       req.LegacyLabels.RType,
+				Adapter:     coreBidder,
+				PubID:       req.LegacyLabels.PubID,
+				CookieFlag:  req.LegacyLabels.CookieFlag,
+				AdapterBids: pbsmetrics.AdapterBidPresent,
+			},
 		}
-		blabels[coreBidder] = &newLabel
-		if hadSync := prepareUser(&reqCopy, bidder, coreBidder, explicitBuyerUIDs, usersyncs); !hadSync && req.App == nil {
-			blabels[coreBidder].CookieFlag = pbsmetrics.CookieFlagNo
+		if hadSync := prepareUser(&reqCopy, bidder.BidderName.String(), coreBidder, explicitBuyerUIDs, req.UserSyncs); !hadSync && req.BidRequest.App == nil {
+			bidder.BidderLabels.CookieFlag = pbsmetrics.CookieFlagNo
 		} else {
-			blabels[coreBidder].CookieFlag = pbsmetrics.CookieFlagYes
+			bidder.BidderLabels.CookieFlag = pbsmetrics.CookieFlagYes
 		}
-		reqCopy.Imp = imps
-
-		prepareSource(&reqCopy, bidder, sChainsByBidder)
-		reqCopy.Ext = reqExt
 
-		requestsByBidder[openrtb_ext.BidderName(bidder)] = &reqCopy
+		bidderRequests = append(bidderRequests, bidder)
 	}
-	return requestsByBidder, nil
+	return bidderRequests, nil
 }
 
 func getExtJson(req *openrtb.BidRequest, unpackedExt *openrtb_ext.ExtRequest) (json.RawMessage, error) {
diff --git a/hooks/pre-push b/hooks/pre-push
deleted file mode 100755
index 7172c194..00000000
--- a/hooks/pre-push
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/sh
-#To install this hook, run this from project root directory:
-#ln -s -f ../../hooks/pre-push .git/hooks/pre-push
-echo "running pre push hook validations"
-./validate.sh
-exit $?
diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go
index 7fd048e1..b97654a6 100644
--- a/pbsmetrics/config/metrics.go
+++ b/pbsmetrics/config/metrics.go
@@ -147,6 +147,12 @@ func (me *MultiMetricsEngine) RecordDNSTime(dnsLookupTime time.Duration) {
 	}
 }
 
+func (me *MultiMetricsEngine) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) {
+	for _, thisME := range *me {
+		thisME.RecordTLSHandshakeTime(tlsHandshakeTime)
+	}
+}
+
 // RecordAdapterBidReceived across all engines
 func (me *MultiMetricsEngine) RecordAdapterBidReceived(labels pbsmetrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
 	for _, thisME := range *me {
@@ -289,6 +295,10 @@ func (me *DummyMetricsEngine) RecordAdapterConnections(bidderName openrtb_ext.Bi
 func (me *DummyMetricsEngine) RecordDNSTime(dnsLookupTime time.Duration) {
 }
 
+// RecordTLSHandshakeTime as a noop
+func (me *DummyMetricsEngine) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) {
+}
+
 // RecordAdapterBidReceived as a noop
 func (me *DummyMetricsEngine) RecordAdapterBidReceived(labels pbsmetrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
 }
diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go
index 180b1229..4d5dd3cb 100644
--- a/pbsmetrics/go_metrics.go
+++ b/pbsmetrics/go_metrics.go
@@ -31,6 +31,7 @@ type Metrics struct {
 	StoredImpCacheMeter            map[CacheResult]metrics.Meter
 	AccountCacheMeter              map[CacheResult]metrics.Meter
 	DNSLookupTimer                 metrics.Timer
+	TLSHandshakeTimer              metrics.Timer
 
 	// Metrics for OpenRTB requests specifically. So we can track what % of RequestsMeter are OpenRTB
 	// and know when legacy requests have been abandoned.
@@ -127,6 +128,7 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 		NoCookieMeter:                  blankMeter,
 		RequestTimer:                   blankTimer,
 		DNSLookupTimer:                 blankTimer,
+		TLSHandshakeTimer:              blankTimer,
 		RequestsQueueTimer:             make(map[RequestType]map[bool]metrics.Timer),
 		PrebidCacheRequestTimerSuccess: blankTimer,
 		PrebidCacheRequestTimerError:   blankTimer,
@@ -227,6 +229,7 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d
 	newMetrics.AppRequestMeter = metrics.GetOrRegisterMeter("app_requests", registry)
 	newMetrics.RequestTimer = metrics.GetOrRegisterTimer("request_time", registry)
 	newMetrics.DNSLookupTimer = metrics.GetOrRegisterTimer("dns_lookup_time", registry)
+	newMetrics.TLSHandshakeTimer = metrics.GetOrRegisterTimer("tls_handshake_time", registry)
 	newMetrics.PrebidCacheRequestTimerSuccess = metrics.GetOrRegisterTimer("prebid_cache_request_time.ok", registry)
 	newMetrics.PrebidCacheRequestTimerError = metrics.GetOrRegisterTimer("prebid_cache_request_time.err", registry)
 
@@ -542,6 +545,10 @@ func (me *Metrics) RecordDNSTime(dnsLookupTime time.Duration) {
 	me.DNSLookupTimer.Update(dnsLookupTime)
 }
 
+func (me *Metrics) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) {
+	me.TLSHandshakeTimer.Update(tlsHandshakeTime)
+}
+
 // RecordAdapterBidReceived implements a part of the MetricsEngine interface.
 // This tracks how many bids from each Bidder use `adm` vs. `nurl.
 func (me *Metrics) RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go
index 95fa13cc..2397364b 100644
--- a/pbsmetrics/metrics.go
+++ b/pbsmetrics/metrics.go
@@ -339,6 +339,7 @@ type MetricsEngine interface {
 	RecordAdapterRequest(labels AdapterLabels)
 	RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration)
 	RecordDNSTime(dnsLookupTime time.Duration)
+	RecordTLSHandshakeTime(tlsHandshakeTime time.Duration)
 	RecordAdapterPanic(labels AdapterLabels)
 	// This records whether or not a bid of a particular type uses `adm` or `nurl`.
 	// Since the legacy endpoints don't have a bid type, it can only count bids from OpenRTB and AMP.
diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go
index 6871f92b..e8c0ee13 100644
--- a/pbsmetrics/metrics_mock.go
+++ b/pbsmetrics/metrics_mock.go
@@ -72,6 +72,10 @@ func (me *MetricsEngineMock) RecordDNSTime(dnsLookupTime time.Duration) {
 	me.Called(dnsLookupTime)
 }
 
+func (me *MetricsEngineMock) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) {
+	me.Called(tlsHandshakeTime)
+}
+
 // RecordAdapterBidReceived mock
 func (me *MetricsEngineMock) RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
 	me.Called(labels, bidType, hasAdm)
diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go
index 2086c935..a7aabd1f 100644
--- a/pbsmetrics/prometheus/prometheus.go
+++ b/pbsmetrics/prometheus/prometheus.go
@@ -41,6 +41,7 @@ type Metrics struct {
 	storedVideoErrors            *prometheus.CounterVec
 	timeoutNotifications         *prometheus.CounterVec
 	dnsLookupTimer               prometheus.Histogram
+	tlsHandhakeTimer             prometheus.Histogram
 	privacyCCPA                  *prometheus.CounterVec
 	privacyCOPPA                 *prometheus.CounterVec
 	privacyLMT                   *prometheus.CounterVec
@@ -257,6 +258,11 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet
 		"Seconds to resolve DNS",
 		standardTimeBuckets)
 
+	metrics.tlsHandhakeTimer = newHistogram(cfg, metrics.Registry,
+		"tls_handshake_time",
+		"Seconds to perform TLS Handshake",
+		standardTimeBuckets)
+
 	metrics.privacyCCPA = newCounter(cfg, metrics.Registry,
 		"privacy_ccpa",
 		"Count of total requests to Prebid Server where CCPA was provided by source and opt-out .",
@@ -554,6 +560,10 @@ func (m *Metrics) RecordDNSTime(dnsLookupTime time.Duration) {
 	m.dnsLookupTimer.Observe(dnsLookupTime.Seconds())
 }
 
+func (m *Metrics) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) {
+	m.tlsHandhakeTimer.Observe(tlsHandshakeTime.Seconds())
+}
+
 func (m *Metrics) RecordAdapterPanic(labels pbsmetrics.AdapterLabels) {
 	m.adapterPanics.With(prometheus.Labels{
 		adapterLabel: string(labels.Adapter),


Test patch:
diff --git a/adapters/rubicon/rubicon_test.go b/adapters/rubicon/rubicon_test.go
index 9d99ec4d..41e37f41 100644
--- a/adapters/rubicon/rubicon_test.go
+++ b/adapters/rubicon/rubicon_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"github.com/prebid/prebid-server/errortypes"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
@@ -278,7 +279,7 @@ func TestRubiconBasicResponse(t *testing.T) {
 
 	bids, err := an.Call(ctx, pbReq, pbReq.Bidders[0])
 	assert.Nil(t, err, "Should not have gotten an error: %v", err)
-	assert.Equal(t, 3, len(bids), "Received %d bids instead of 3", len(bids))
+	assert.Equal(t, 2, len(bids), "Received %d bids instead of 3", len(bids))
 
 	for _, bid := range bids {
 		matched := false
@@ -524,6 +525,52 @@ func TestAppendTracker(t *testing.T) {
 	}
 }
 
+func TestResolveVideoSizeId(t *testing.T) {
+	testScenarios := []struct {
+		placement   openrtb.VideoPlacementType
+		instl       int8
+		impId       string
+		expected    int
+		expectedErr error
+	}{
+		{
+			placement:   1,
+			instl:       1,
+			impId:       "impId",
+			expected:    201,
+			expectedErr: nil,
+		},
+		{
+			placement:   3,
+			instl:       1,
+			impId:       "impId",
+			expected:    203,
+			expectedErr: nil,
+		},
+		{
+			placement:   4,
+			instl:       1,
+			impId:       "impId",
+			expected:    202,
+			expectedErr: nil,
+		},
+		{
+			placement: 4,
+			instl:     3,
+			impId:     "impId",
+			expectedErr: &errortypes.BadInput{
+				Message: "video.size_id can not be resolved in impression with id : impId",
+			},
+		},
+	}
+
+	for _, scenario := range testScenarios {
+		res, err := resolveVideoSizeId(scenario.placement, scenario.instl, scenario.impId)
+		assert.Equal(t, scenario.expected, res)
+		assert.Equal(t, scenario.expectedErr, err)
+	}
+}
+
 func TestNoContentResponse(t *testing.T) {
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(http.StatusNoContent)
diff --git a/adapters/rubicon/rubicontest/exemplary/simple-video.json b/adapters/rubicon/rubicontest/exemplary/simple-video.json
new file mode 100644
index 00000000..76c64ff9
--- /dev/null
+++ b/adapters/rubicon/rubicontest/exemplary/simple-video.json
@@ -0,0 +1,159 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "device": {
+      "ip": "123.123.123.123",
+      "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+    },
+    "app": {
+      "id": "1",
+      "bundle": "com.wls.testwlsapplication"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "instl": 1,
+        "video": {
+          "placement": 3,
+          "mimes": [
+            "video/mp4"
+          ],
+          "protocols": [
+            2,
+            5
+          ],
+          "w": 1024,
+          "h": 576
+        },
+        "ext": {
+          "bidder": {
+            "video": {
+            },
+            "accountId": 1001,
+            "siteId":113932,
+            "zoneId":535510
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "uri?tk_xint=pbs-test-tracker",
+        "body": {
+          "id": "test-request-id",
+          "device": {
+            "ext": {
+              "rp": {
+                "pixelratio": 0
+              }
+            },
+            "ip": "123.123.123.123",
+            "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+          },
+          "app": {
+            "id": "1",
+            "ext": {
+              "rp": {
+                "site_id": 113932
+              }
+            },
+            "publisher": {
+              "ext": {
+                "rp": {
+                  "account_id": 1001
+                }
+              }
+            },
+            "bundle": "com.wls.testwlsapplication"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "instl": 1,
+              "video": {
+                "placement": 3,
+                "ext": {
+                  "rp": {
+                    "size_id": 203
+                  }
+                },
+                "mimes": [
+                  "video/mp4"
+                ],
+                "protocols": [
+                  2,
+                  5
+                ],
+                "w": 1024,
+                "h": 576
+              },
+              "ext": {
+                "rp": {
+                  "track":{
+                    "mint": "",
+                    "mint_version": ""
+                  },
+                  "zone_id": 535510
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "test_bid_id",
+                  "impid": "test-imp-id",
+                  "price": 0.27543,
+                  "adm": "some-test-ad",
+                  "cid": "test_cid",
+                  "crid": "test_crid",
+                  "dealid": "test_dealid",
+                  "ext": {
+                    "prebid": {
+                      "type": "video"
+                    }
+                  }
+                }
+              ],
+              "seat": "adman"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "test_bid_id",
+            "impid": "test-imp-id",
+            "price": 0.27543,
+            "adm": "some-test-ad",
+            "cid": "test_cid",
+            "crid": "test_crid",
+            "dealid": "test_dealid",
+            "ext": {
+              "prebid": {
+                "type": "video"
+              }
+            }
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/rubicon/rubicontest/supplemental/required-video-size-id.json b/adapters/rubicon/rubicontest/supplemental/required-video-size-id.json
index cf0a594e..8e7482ad 100644
--- a/adapters/rubicon/rubicontest/supplemental/required-video-size-id.json
+++ b/adapters/rubicon/rubicontest/supplemental/required-video-size-id.json
@@ -24,7 +24,7 @@
     },
     "expectedMakeRequestsErrors": [
         {
-            "value": "imp[0].ext.bidder.rubicon.video.size_id must be defined for video impression",
+            "value": "video.size_id can not be resolved in impression with id : test-imp-1",
             "comparison": "literal"
         }
     ]
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index 4f207cf5..e3b2a031 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -3,6 +3,7 @@ package exchange
 import (
 	"bytes"
 	"context"
+	"crypto/tls"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -1238,21 +1239,36 @@ func TestCallRecordAdapterConnections(t *testing.T) {
 type DNSDoneTripper struct{}
 
 func (DNSDoneTripper) RoundTrip(req *http.Request) (*http.Response, error) {
-	//Access the httptrace.ClientTrace
+	// Access the httptrace.ClientTrace
 	trace := httptrace.ContextClientTrace(req.Context())
-
-	//Force DNSDone call defined in exchange/bidder.go
+	// Call the DNSDone method on the client trace
 	trace.DNSDone(httptrace.DNSDoneInfo{})
 
 	resp := &http.Response{
 		StatusCode: 200,
-		Header:     map[string][]string{"Location": {"http://www.example.com/"}},
 		Body:       ioutil.NopCloser(strings.NewReader("postBody")),
 	}
+
+	return resp, nil
+}
+
+type TLSHandshakeTripper struct{}
+
+func (TLSHandshakeTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	// Access the httptrace.ClientTrace
+	trace := httptrace.ContextClientTrace(req.Context())
+	// Call the TLSHandshakeDone method on the client trace
+	trace.TLSHandshakeDone(tls.ConnectionState{}, nil)
+
+	resp := &http.Response{
+		StatusCode: 200,
+		Body:       ioutil.NopCloser(strings.NewReader("postBody")),
+	}
+
 	return resp, nil
 }
 
-func TestCallRecordRecordDNSTime(t *testing.T) {
+func TestCallRecordDNSTime(t *testing.T) {
 	// setup a mock metrics engine and its expectation
 	metricsMock := &pbsmetrics.MetricsEngineMock{}
 	metricsMock.Mock.On("RecordDNSTime", mock.Anything).Return()
@@ -1273,6 +1289,27 @@ func TestCallRecordRecordDNSTime(t *testing.T) {
 	metricsMock.AssertExpectations(t)
 }
 
+func TestCallRecordTLSHandshakeTime(t *testing.T) {
+	// setup a mock metrics engine and its expectation
+	metricsMock := &pbsmetrics.MetricsEngineMock{}
+	metricsMock.Mock.On("RecordTLSHandshakeTime", mock.Anything).Return()
+
+	// Instantiate the bidder that will send the request. We'll make sure to use an
+	// http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{})
+	// gets called
+	bidder := &bidderAdapter{
+		Bidder: &mixedMultiBidder{},
+		Client: &http.Client{Transport: TLSHandshakeTripper{}},
+		me:     metricsMock,
+	}
+
+	// Run test
+	bidder.doRequest(context.Background(), &adapters.RequestData{Method: "POST", Uri: "http://www.example.com/"})
+
+	// Tried one or another, none seem to work without panicking
+	metricsMock.AssertExpectations(t)
+}
+
 func TestTimeoutNotificationOff(t *testing.T) {
 	respBody := "{\"bid\":false}"
 	respStatus := 200
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index c2db8713..1d32ca3c 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -1152,18 +1152,10 @@ func TestPanicRecovery(t *testing.T) {
 	e := NewExchange(adapters, nil, cfg, &metricsConf.DummyMetricsEngine{}, gdpr.AlwaysAllow{}, currencyConverter, nilCategoryFetcher{}).(*exchange)
 
 	chBids := make(chan *bidResponseWrapper, 1)
-	panicker := func(aName openrtb_ext.BidderName, coreBidder openrtb_ext.BidderName, request *openrtb.BidRequest, bidlabels *pbsmetrics.AdapterLabels, conversions currencies.Conversions) {
+	panicker := func(bidderRequest BidderRequest, conversions currencies.Conversions) {
 		panic("panic!")
 	}
-	cleanReqs := map[openrtb_ext.BidderName]*openrtb.BidRequest{
-		"bidder1": {
-			ID: "b-1",
-		},
-		"bidder2": {
-			ID: "b-2",
-		},
-	}
-	recovered := e.recoverSafely(cleanReqs, panicker, chBids)
+
 	apnLabels := pbsmetrics.AdapterLabels{
 		Source:      pbsmetrics.DemandWeb,
 		RType:       pbsmetrics.ReqTypeORTB2Web,
@@ -1172,7 +1164,27 @@ func TestPanicRecovery(t *testing.T) {
 		CookieFlag:  pbsmetrics.CookieFlagYes,
 		AdapterBids: pbsmetrics.AdapterBidNone,
 	}
-	recovered(openrtb_ext.BidderAppnexus, openrtb_ext.BidderAppnexus, nil, &apnLabels, nil)
+
+	bidderRequests := []BidderRequest{
+		{
+			BidderName:     "bidder1",
+			BidderCoreName: "appnexus",
+			BidderLabels:   apnLabels,
+			BidRequest: &openrtb.BidRequest{
+				ID: "b-1",
+			},
+		},
+		{
+			BidderName:     "bidder2",
+			BidderCoreName: "bidder2",
+			BidRequest: &openrtb.BidRequest{
+				ID: "b-2",
+			},
+		},
+	}
+
+	recovered := e.recoverSafely(bidderRequests, panicker, chBids)
+	recovered(bidderRequests[0], nil)
 }
 
 func buildImpExt(t *testing.T, jsonFilename string) json.RawMessage {
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index e2aa72e0..2f24af2a 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -38,38 +38,48 @@ func (p *permissionsMock) AMPException() bool {
 	return false
 }
 
-func assertReq(t *testing.T, reqByBidders map[openrtb_ext.BidderName]*openrtb.BidRequest,
+func assertReq(t *testing.T, bidderRequests []BidderRequest,
 	applyCOPPA bool, consentedVendors map[string]bool) {
 	// assert individual bidder requests
-	assert.NotEqual(t, reqByBidders, 0, "cleanOpenRTBRequest should split request into individual bidder requests")
+	assert.NotEqual(t, bidderRequests, 0, "cleanOpenRTBRequest should split request into individual bidder requests")
 
 	// assert for PI data
 	// Both appnexus and brightroll should be allowed since brightroll
 	// is used as an alias for appnexus in the test request
-	for bidderName, bidder := range reqByBidders {
-		if !applyCOPPA && consentedVendors[bidderName.String()] {
-			assert.NotEqual(t, bidder.User.BuyerUID, "", "cleanOpenRTBRequest shouldn't clean PI data as per COPPA or for a consented vendor as per GDPR or per CCPA")
-			assert.NotEqual(t, bidder.Device.DIDMD5, "", "cleanOpenRTBRequest shouldn't clean PI data as per COPPA or for a consented vendor as per GDPR or per CCPA")
+	for _, req := range bidderRequests {
+		if !applyCOPPA && consentedVendors[req.BidderName.String()] {
+			assert.NotEqual(t, req.BidRequest.User.BuyerUID, "", "cleanOpenRTBRequest shouldn't clean PI data as per COPPA or for a consented vendor as per GDPR or per CCPA")
+			assert.NotEqual(t, req.BidRequest.Device.DIDMD5, "", "cleanOpenRTBRequest shouldn't clean PI data as per COPPA or for a consented vendor as per GDPR or per CCPA")
 		} else {
-			assert.Equal(t, bidder.User.BuyerUID, "", "cleanOpenRTBRequest should clean PI data as per COPPA or for a non-consented vendor as per GDPR or per CCPA", bidderName.String())
-			assert.Equal(t, bidder.Device.DIDMD5, "", "cleanOpenRTBRequest should clean PI data as per COPPA or for a non-consented vendor as per GDPR or per CCPA", bidderName.String())
+			assert.Equal(t, req.BidRequest.User.BuyerUID, "", "cleanOpenRTBRequest should clean PI data as per COPPA or for a non-consented vendor as per GDPR or per CCPA", req.BidderName.String())
+			assert.Equal(t, req.BidRequest.Device.DIDMD5, "", "cleanOpenRTBRequest should clean PI data as per COPPA or for a non-consented vendor as per GDPR or per CCPA", req.BidderName.String())
 		}
 	}
 }
 
 func TestCleanOpenRTBRequests(t *testing.T) {
 	testCases := []struct {
-		req              *openrtb.BidRequest
-		bidReqAssertions func(t *testing.T, reqByBidders map[openrtb_ext.BidderName]*openrtb.BidRequest,
+		req              AuctionRequest
+		bidReqAssertions func(t *testing.T, bidderRequests []BidderRequest,
 			applyCOPPA bool, consentedVendors map[string]bool)
 		hasError         bool
 		applyCOPPA       bool
 		consentedVendors map[string]bool
 	}{
-		{req: newRaceCheckingRequest(t), bidReqAssertions: assertReq, hasError: false,
-			applyCOPPA: true, consentedVendors: map[string]bool{"appnexus": true}},
-		{req: newAdapterAliasBidRequest(t), bidReqAssertions: assertReq, hasError: false,
-			applyCOPPA: false, consentedVendors: map[string]bool{"appnexus": true, "brightroll": true}},
+		{
+			req:              AuctionRequest{BidRequest: newRaceCheckingRequest(t), UserSyncs: &emptyUsersync{}},
+			bidReqAssertions: assertReq,
+			hasError:         false,
+			applyCOPPA:       true,
+			consentedVendors: map[string]bool{"appnexus": true},
+		},
+		{
+			req:              AuctionRequest{BidRequest: newAdapterAliasBidRequest(t), UserSyncs: &emptyUsersync{}},
+			bidReqAssertions: assertReq,
+			hasError:         false,
+			applyCOPPA:       false,
+			consentedVendors: map[string]bool{"appnexus": true, "brightroll": true},
+		},
 	}
 
 	privacyConfig := config.Privacy{
@@ -82,12 +92,12 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		reqByBidders, _, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, &config.Account{})
+		bidderRequests, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig)
 		if test.hasError {
 			assert.NotNil(t, err, "Error shouldn't be nil")
 		} else {
 			assert.Nil(t, err, "Err should be nil")
-			test.bidReqAssertions(t, reqByBidders, test.applyCOPPA, test.consentedVendors)
+			test.bidReqAssertions(t, bidderRequests, test.applyCOPPA, test.consentedVendors)
 		}
 	}
 }
@@ -227,26 +237,28 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 			},
 		}
 
-		results, _, privacyLabels, errs := cleanOpenRTBRequests(
+		auctionReq := AuctionRequest{
+			BidRequest: req,
+			UserSyncs:  &emptyUsersync{},
+			Account:    accountConfig,
+		}
+
+		bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(
 			context.Background(),
-			req,
+			auctionReq,
 			nil,
-			&emptyUsersync{},
-			map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{},
-			pbsmetrics.Labels{},
 			&permissionsMock{personalInfoAllowed: true},
 			true,
-			privacyConfig,
-			&accountConfig)
-		result := results["appnexus"]
+			privacyConfig)
+		result := bidderRequests[0]
 
 		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")
+			assert.Equal(t, result.BidRequest.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.Equal(t, result.BidRequest.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.NotEqual(t, result.BidRequest.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.NotEqual(t, result.BidRequest.Device.DIDMD5, "", test.description+":Device.DIDMD5")
 		}
 		assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels")
 	}
@@ -282,12 +294,17 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) {
 		err := json.Unmarshal(req.Ext, &reqExtStruct)
 		assert.NoError(t, err, test.description+":marshal_ext")
 
+		auctionReq := AuctionRequest{
+			BidRequest: req,
+			UserSyncs:  &emptyUsersync{},
+		}
+
 		privacyConfig := config.Privacy{
 			CCPA: config.CCPA{
 				Enforce: true,
 			},
 		}
-		_, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &reqExtStruct, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, &config.Account{})
+		_, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, &reqExtStruct, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig)
 
 		assert.ElementsMatch(t, []error{test.expectError}, errs, test.description)
 	}
@@ -322,16 +339,21 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) {
 		req := newBidRequest(t)
 		req.Regs = &openrtb.Regs{COPPA: test.coppa}
 
-		results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, config.Privacy{}, &config.Account{})
-		result := results["appnexus"]
+		auctionReq := AuctionRequest{
+			BidRequest: req,
+			UserSyncs:  &emptyUsersync{},
+		}
+
+		bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissionsMock{personalInfoAllowed: true}, true, config.Privacy{})
+		result := bidderRequests[0]
 
 		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")
+			assert.Equal(t, result.BidRequest.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.Equal(t, result.BidRequest.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.NotEqual(t, result.BidRequest.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.NotEqual(t, result.BidRequest.User.Yob, int64(0), test.description+":User.Yob")
 		}
 		assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels")
 	}
@@ -424,16 +446,20 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) {
 			extRequest = unmarshaledExt
 		}
 
-		results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, extRequest, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, config.Privacy{}, &config.Account{})
-		result := results["appnexus"]
+		auctionReq := AuctionRequest{
+			BidRequest: req,
+			UserSyncs:  &emptyUsersync{},
+		}
 
+		bidderRequests, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, &permissionsMock{}, true, config.Privacy{})
 		if test.hasError == true {
 			assert.NotNil(t, errs)
-			assert.Nil(t, result)
+			assert.Len(t, bidderRequests, 0)
 		} else {
+			result := bidderRequests[0]
 			assert.Nil(t, errs)
-			assert.Equal(t, test.outSourceExt, result.Source.Ext, test.description+":Source.Ext")
-			assert.Equal(t, test.outRequestExt, result.Ext, test.description+":Ext")
+			assert.Equal(t, test.outSourceExt, result.BidRequest.Source.Ext, test.description+":Source.Ext")
+			assert.Equal(t, test.outRequestExt, result.BidRequest.Ext, test.description+":Ext")
 		}
 	}
 }
@@ -995,22 +1021,27 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 		req := newBidRequest(t)
 		req.Device.Lmt = test.lmt
 
+		auctionReq := AuctionRequest{
+			BidRequest: req,
+			UserSyncs:  &emptyUsersync{},
+		}
+
 		privacyConfig := config.Privacy{
 			LMT: config.LMT{
 				Enforce: test.enforceLMT,
 			},
 		}
 
-		results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, &config.Account{})
-		result := results["appnexus"]
+		results, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig)
+		result := results[0]
 
 		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")
+			assert.Equal(t, result.BidRequest.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.Equal(t, result.BidRequest.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.NotEqual(t, result.BidRequest.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.NotEqual(t, result.BidRequest.Device.DIDMD5, "", test.description+":Device.DIDMD5")
 		}
 		assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels")
 	}
@@ -1148,26 +1179,28 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			},
 		}
 
-		results, _, privacyLabels, errs := cleanOpenRTBRequests(
+		auctionReq := AuctionRequest{
+			BidRequest: req,
+			UserSyncs:  &emptyUsersync{},
+			Account:    accountConfig,
+		}
+
+		results, privacyLabels, errs := cleanOpenRTBRequests(
 			context.Background(),
-			req,
+			auctionReq,
 			nil,
-			&emptyUsersync{},
-			map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{},
-			pbsmetrics.Labels{},
 			&permissionsMock{personalInfoAllowed: !test.gdprScrub},
 			true,
-			privacyConfig,
-			&accountConfig)
-		result := results["appnexus"]
+			privacyConfig)
+		result := results[0]
 
 		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")
+			assert.Equal(t, result.BidRequest.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.Equal(t, result.BidRequest.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.NotEqual(t, result.BidRequest.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.NotEqual(t, result.BidRequest.Device.DIDMD5, "", test.description+":Device.DIDMD5")
 		}
 		assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels")
 	}
diff --git a/pbsmetrics/go_metrics_test.go b/pbsmetrics/go_metrics_test.go
index f4d62c46..dd09c3ec 100644
--- a/pbsmetrics/go_metrics_test.go
+++ b/pbsmetrics/go_metrics_test.go
@@ -209,6 +209,33 @@ func TestRecordDNSTime(t *testing.T) {
 	}
 }
 
+func TestRecordTLSHandshakeTime(t *testing.T) {
+	testCases := []struct {
+		description          string
+		tLSHandshakeDuration time.Duration
+		expectedDuration     time.Duration
+	}{
+		{
+			description:          "Five second TLS handshake time",
+			tLSHandshakeDuration: time.Second * 5,
+			expectedDuration:     time.Second * 5,
+		},
+		{
+			description:          "Zero TLS handshake time",
+			tLSHandshakeDuration: time.Duration(0),
+			expectedDuration:     time.Duration(0),
+		},
+	}
+	for _, test := range testCases {
+		registry := metrics.NewRegistry()
+		m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AccountAdapterDetails: true})
+
+		m.RecordTLSHandshakeTime(test.tLSHandshakeDuration)
+
+		assert.Equal(t, test.expectedDuration.Nanoseconds(), m.TLSHandshakeTimer.Sum(), test.description)
+	}
+}
+
 func TestRecordAdapterConnections(t *testing.T) {
 	var fakeBidder openrtb_ext.BidderName = "fooAdvertising"
 
diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go
index 5d6ad391..bdd1bc7a 100644
--- a/pbsmetrics/prometheus/prometheus_test.go
+++ b/pbsmetrics/prometheus/prometheus_test.go
@@ -1197,6 +1197,39 @@ func TestRecordDNSTime(t *testing.T) {
 	}
 }
 
+func TestRecordTLSHandshakeTime(t *testing.T) {
+	testCases := []struct {
+		description          string
+		tLSHandshakeDuration time.Duration
+		expectedDuration     float64
+		expectedCount        uint64
+	}{
+		{
+			description:          "Five second DNS lookup time",
+			tLSHandshakeDuration: time.Second * 5,
+			expectedDuration:     5,
+			expectedCount:        1,
+		},
+		{
+			description:          "Zero DNS lookup time",
+			tLSHandshakeDuration: 0,
+			expectedDuration:     0,
+			expectedCount:        1,
+		},
+	}
+	for i, test := range testCases {
+		pm := createMetricsForTesting()
+		pm.RecordTLSHandshakeTime(test.tLSHandshakeDuration)
+
+		m := dto.Metric{}
+		pm.tlsHandhakeTimer.Write(&m)
+		histogram := *m.GetHistogram()
+
+		assert.Equal(t, test.expectedCount, histogram.GetSampleCount(), "[%d] Incorrect number of histogram entries. Desc: %s\n", i, test.description)
+		assert.Equal(t, test.expectedDuration, histogram.GetSampleSum(), "[%d] Incorrect number of histogram cumulative values. Desc: %s\n", i, test.description)
+	}
+}
+
 func TestRecordAdapterConnections(t *testing.T) {
 
 	type testIn struct {