Repository: prebid/prebid-server
Pull request #1408 (2020-07-17 23:31:05)

GumGum: adds support for video


Patch:
diff --git a/adapters/gumgum/gumgum.go b/adapters/gumgum/gumgum.go
index 84a008d1..49097909 100644
--- a/adapters/gumgum/gumgum.go
+++ b/adapters/gumgum/gumgum.go
@@ -8,12 +8,16 @@ import (
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"net/http"
+	"strconv"
+	"strings"
 )
 
+// GumGumAdapter implements Bidder interface.
 type GumGumAdapter struct {
 	URI string
 }
 
+// MakeRequests makes the HTTP requests which should be made to fetch bids.
 func (g *GumGumAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	var validImps []openrtb.Imp
 	var trackingId string
@@ -26,15 +30,21 @@ func (g *GumGumAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapt
 		zone, err := preprocess(&imp)
 		if err != nil {
 			errs = append(errs, err)
-		} else {
-			if request.Imp[i].Banner != nil {
-				bannerCopy := *request.Imp[i].Banner
-				if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 {
-					format := bannerCopy.Format[0]
-					bannerCopy.W = &(format.W)
-					bannerCopy.H = &(format.H)
-				}
-				request.Imp[i].Banner = &bannerCopy
+		} else if request.Imp[i].Banner != nil {
+			bannerCopy := *request.Imp[i].Banner
+			if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 {
+				format := bannerCopy.Format[0]
+				bannerCopy.W = &(format.W)
+				bannerCopy.H = &(format.H)
+			}
+			request.Imp[i].Banner = &bannerCopy
+			validImps = append(validImps, request.Imp[i])
+			trackingId = zone
+		} else if request.Imp[i].Video != nil {
+			err := validateVideoParams(request.Imp[i].Video)
+			if err != nil {
+				errs = append(errs, err)
+			} else {
 				validImps = append(validImps, request.Imp[i])
 				trackingId = zone
 			}
@@ -70,6 +80,7 @@ func (g *GumGumAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapt
 	}}, errs
 }
 
+// MakeBids unpacks the server's response into Bids.
 func (g *GumGumAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
 	if response.StatusCode == http.StatusNoContent {
 		return nil, nil
@@ -98,12 +109,19 @@ func (g *GumGumAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRe
 
 	for _, sb := range bidResp.SeatBid {
 		for i := range sb.Bid {
+			mediaType := getMediaTypeForImpID(sb.Bid[i].ImpID, internalRequest.Imp)
+			if mediaType == openrtb_ext.BidTypeVideo {
+				price := strconv.FormatFloat(sb.Bid[i].Price, 'f', -1, 64)
+				sb.Bid[i].AdM = strings.Replace(sb.Bid[i].AdM, "${AUCTION_PRICE}", price, -1)
+			}
+
 			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
 				Bid:     &sb.Bid[i],
-				BidType: openrtb_ext.BidTypeBanner,
+				BidType: mediaType,
 			})
 		}
 	}
+
 	return bidResponse, errs
 }
 
@@ -128,6 +146,25 @@ func preprocess(imp *openrtb.Imp) (string, error) {
 	return zone, nil
 }
 
+func getMediaTypeForImpID(impID string, imps []openrtb.Imp) openrtb_ext.BidType {
+	for _, imp := range imps {
+		if imp.ID == impID && imp.Banner != nil {
+			return openrtb_ext.BidTypeBanner
+		}
+	}
+	return openrtb_ext.BidTypeVideo
+}
+
+func validateVideoParams(video *openrtb.Video) (err error) {
+	if video.W == 0 || video.H == 0 || video.MinDuration == 0 || video.MaxDuration == 0 || video.Placement == 0 || video.Linearity == 0 {
+		return &errortypes.BadInput{
+			Message: "Invalid or missing video field(s)",
+		}
+	}
+	return nil
+}
+
+// NewGumGumBidder configures bidder endpoint.
 func NewGumGumBidder(endpoint string) *GumGumAdapter {
 	return &GumGumAdapter{
 		URI: endpoint,
diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go
index aae11538..79565504 100644
--- a/adapters/pubmatic/pubmatic.go
+++ b/adapters/pubmatic/pubmatic.go
@@ -21,7 +21,6 @@ import (
 )
 
 const MAX_IMPRESSIONS_PUBMATIC = 30
-const bidTypeExtKey = "BidType"
 
 type PubmaticAdapter struct {
 	http *adapters.HTTPAdapter
@@ -48,6 +47,15 @@ type pubmaticParams struct {
 	Keywords    map[string]string `json:"keywords,omitempty"`
 }
 
+type pubmaticBidExtVideo struct {
+	Duration *int `json:"duration,omitempty"`
+}
+
+type pubmaticBidExt struct {
+	BidType           *int                 `json:"BidType,omitempty"`
+	VideoCreativeInfo *pubmaticBidExtVideo `json:"video,omitempty"`
+}
+
 const (
 	INVALID_PARAMS    = "Invalid BidParam"
 	MISSING_PUBID     = "Missing PubID"
@@ -289,7 +297,11 @@ func (a *PubmaticAdapter) Call(ctx context.Context, req *pbs.PBSRequest, bidder
 				DealId:      bid.DealID,
 			}
 
-			mediaType := getBidType(bid.Ext)
+			var bidExt pubmaticBidExt
+			mediaType := openrtb_ext.BidTypeBanner
+			if err := json.Unmarshal(bid.Ext, &bidExt); err == nil {
+				mediaType = getBidType(&bidExt)
+			}
 			pbid.CreativeMediaType = string(mediaType)
 
 			bids = append(bids, &pbid)
@@ -549,9 +561,24 @@ func (a *PubmaticAdapter) MakeBids(internalRequest *openrtb.BidRequest, external
 	for _, sb := range bidResp.SeatBid {
 		for i := 0; i < len(sb.Bid); i++ {
 			bid := sb.Bid[i]
+			impVideo := &openrtb_ext.ExtBidPrebidVideo{}
+
+			if len(bid.Cat) > 1 {
+				bid.Cat = bid.Cat[0:1]
+			}
+
+			var bidExt *pubmaticBidExt
+			bidType := openrtb_ext.BidTypeBanner
+			if err := json.Unmarshal(bid.Ext, &bidExt); err == nil && bidExt != nil {
+				if bidExt.VideoCreativeInfo != nil && bidExt.VideoCreativeInfo.Duration != nil {
+					impVideo.Duration = *bidExt.VideoCreativeInfo.Duration
+				}
+				bidType = getBidType(bidExt)
+			}
 			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
-				Bid:     &bid,
-				BidType: getBidType(bid.Ext),
+				Bid:      &bid,
+				BidType:  bidType,
+				BidVideo: impVideo,
 			})
 
 		}
@@ -560,28 +587,20 @@ func (a *PubmaticAdapter) MakeBids(internalRequest *openrtb.BidRequest, external
 }
 
 // getBidType returns the bid type specified in the response bid.ext
-func getBidType(bidExt json.RawMessage) openrtb_ext.BidType {
+func getBidType(bidExt *pubmaticBidExt) openrtb_ext.BidType {
 	// setting "banner" as the default bid type
 	bidType := openrtb_ext.BidTypeBanner
-	if bidExt != nil {
-		bidExtMap := make(map[string]interface{})
-		extbyte, err := json.Marshal(bidExt)
-		if err == nil {
-			err = json.Unmarshal(extbyte, &bidExtMap)
-			if err == nil && bidExtMap[bidTypeExtKey] != nil {
-				bidTypeVal := int(bidExtMap[bidTypeExtKey].(float64))
-				switch bidTypeVal {
-				case 0:
-					bidType = openrtb_ext.BidTypeBanner
-				case 1:
-					bidType = openrtb_ext.BidTypeVideo
-				case 2:
-					bidType = openrtb_ext.BidTypeNative
-				default:
-					// default value is banner
-					bidType = openrtb_ext.BidTypeBanner
-				}
-			}
+	if bidExt != nil && bidExt.BidType != nil {
+		switch *bidExt.BidType {
+		case 0:
+			bidType = openrtb_ext.BidTypeBanner
+		case 1:
+			bidType = openrtb_ext.BidTypeVideo
+		case 2:
+			bidType = openrtb_ext.BidTypeNative
+		default:
+			// default value is banner
+			bidType = openrtb_ext.BidTypeBanner
 		}
 	}
 	return bidType
diff --git a/config/config.go b/config/config.go
index 2e7f875b..8545523d 100755
--- a/config/config.go
+++ b/config/config.go
@@ -379,6 +379,11 @@ type Metrics struct {
 type DisabledMetrics struct {
 	// True if we want to stop collecting account-to-adapter metrics
 	AccountAdapterDetails bool `mapstructure:"account_adapter_details"`
+
+	// True if we don't want to collect metrics about the connections prebid
+	// server establishes with bidder servers such as the number of connections
+	// that were created or reused.
+	AdapterConnectionMetrics bool `mapstructure:"adapter_connections_metrics"`
 }
 
 func (cfg *Metrics) validate(errs configErrors) configErrors {
@@ -688,6 +693,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("http_client_cache.idle_connection_timeout_seconds", 60)
 	// no metrics configured by default (metrics{host|database|username|password})
 	v.SetDefault("metrics.disabled_metrics.account_adapter_details", false)
+	v.SetDefault("metrics.disabled_metrics.adapter_connections_metrics", true)
 	v.SetDefault("metrics.influxdb.host", "")
 	v.SetDefault("metrics.influxdb.database", "")
 	v.SetDefault("metrics.influxdb.username", "")
@@ -890,13 +896,14 @@ func SetupViper(v *viper.Viper, filename string) {
 	/*  Loopback:   127.0.0.0/8
 	/*
 	/* IPv6
-	/*  Loopback:     ::1/128
-	/*  Unique Local: fc00::/7
-	/*  Link Local:   fe80::/10
-	/*  Multicast:    ff00::/8
+	/*  Loopback:      ::1/128
+	/*  Documentation: 2001:db8::/32
+	/*  Unique Local:  fc00::/7
+	/*  Link Local:    fe80::/10
+	/*  Multicast:     ff00::/8
 	*/
 	v.SetDefault("request_validation.ipv4_private_networks", []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "127.0.0.0/8"})
-	v.SetDefault("request_validation.ipv6_private_networks", []string{"::1/128", "fc00::/7", "fe80::/10", "ff00::/8"})
+	v.SetDefault("request_validation.ipv6_private_networks", []string{"::1/128", "fc00::/7", "fe80::/10", "ff00::/8", "2001:db8::/32"})
 
 	// Set environment variable support:
 	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go
index 53607ac5..2ecddb83 100755
--- a/exchange/adapter_map.go
+++ b/exchange/adapter_map.go
@@ -201,7 +201,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 	for name, bidder := range ortbBidders {
 		// Clean out any disabled bidders
 		if infos[string(name)].Status == adapters.StatusActive {
-			allBidders[name] = adaptBidder(adapters.EnforceBidderInfo(bidder, infos[string(name)]), client, cfg, me)
+			allBidders[name] = adaptBidder(adapters.EnforceBidderInfo(bidder, infos[string(name)]), client, cfg, me, name)
 		}
 	}
 
diff --git a/exchange/bidder.go b/exchange/bidder.go
index ee6a4942..7c39b72b 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net/http"
+	"net/http/httptrace"
 	"time"
 
 	"github.com/golang/glog"
@@ -87,20 +88,30 @@ type pbsOrtbSeatBid struct {
 //
 // The name refers to the "Adapter" architecture pattern, and should not be confused with a Prebid "Adapter"
 // (which is being phased out and replaced by Bidder for OpenRTB auctions)
-func adaptBidder(bidder adapters.Bidder, client *http.Client, cfg *config.Configuration, me pbsmetrics.MetricsEngine) adaptedBidder {
+func adaptBidder(bidder adapters.Bidder, client *http.Client, cfg *config.Configuration, me pbsmetrics.MetricsEngine, name openrtb_ext.BidderName) adaptedBidder {
 	return &bidderAdapter{
-		Bidder:      bidder,
-		Client:      client,
-		DebugConfig: cfg.Debug,
-		me:          me,
+		Bidder:     bidder,
+		BidderName: name,
+		Client:     client,
+		me:         me,
+		config: bidderAdapterConfig{
+			Debug:              cfg.Debug,
+			DisableConnMetrics: cfg.Metrics.Disabled.AdapterConnectionMetrics,
+		},
 	}
 }
 
 type bidderAdapter struct {
-	Bidder      adapters.Bidder
-	Client      *http.Client
-	DebugConfig config.Debug
-	me          pbsmetrics.MetricsEngine
+	Bidder     adapters.Bidder
+	BidderName openrtb_ext.BidderName
+	Client     *http.Client
+	me         pbsmetrics.MetricsEngine
+	config     bidderAdapterConfig
+}
+
+type bidderAdapterConfig struct {
+	Debug              config.Debug
+	DisableConnMetrics bool
 }
 
 func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo) (*pbsOrtbSeatBid, []error) {
@@ -325,6 +336,11 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re
 	}
 	httpReq.Header = req.Headers
 
+	// If adapter connection metrics are not disabled, add the client trace
+	// to get complete connection info into our metrics
+	if !bidder.config.DisableConnMetrics {
+		ctx = bidder.addClientTrace(ctx)
+	}
 	httpResp, err := ctxhttp.Do(ctx, bidder.Client, httpReq)
 	if err != nil {
 		if err == context.DeadlineExceeded {
@@ -387,7 +403,7 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou
 			httpResp, err := ctxhttp.Do(ctx, bidder.Client, httpReq)
 			success := (err == nil && httpResp.StatusCode >= 200 && httpResp.StatusCode < 300)
 			bidder.me.RecordTimeoutNotice(success)
-			if bidder.DebugConfig.TimeoutNotification.Log && !(bidder.DebugConfig.TimeoutNotification.FailOnly && success) {
+			if bidder.config.Debug.TimeoutNotification.Log && !(bidder.config.Debug.TimeoutNotification.FailOnly && success) {
 				var msg string
 				if err == nil {
 					msg = fmt.Sprintf("TimeoutNotification: status:(%d) body:%s", httpResp.StatusCode, string(toReq.Body))
@@ -395,16 +411,16 @@ 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, logger, bidder.DebugConfig.TimeoutNotification.SamplingRate)
+				util.LogRandomSample(msg, logger, bidder.config.Debug.TimeoutNotification.SamplingRate)
 			}
 		} else {
 			bidder.me.RecordTimeoutNotice(false)
-			if bidder.DebugConfig.TimeoutNotification.Log {
+			if bidder.config.Debug.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, logger, bidder.DebugConfig.TimeoutNotification.SamplingRate)
+				util.LogRandomSample(msg, logger, bidder.config.Debug.TimeoutNotification.SamplingRate)
 			}
 		}
-	} else if bidder.DebugConfig.TimeoutNotification.Log {
+	} else if bidder.config.Debug.TimeoutNotification.Log {
 		reqJSON, err := json.Marshal(req)
 		var msg string
 		if err == nil {
@@ -412,7 +428,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, logger, bidder.DebugConfig.TimeoutNotification.SamplingRate)
+		util.LogRandomSample(msg, logger, bidder.config.Debug.TimeoutNotification.SamplingRate)
 	}
 
 }
@@ -422,3 +438,34 @@ type httpCallInfo struct {
 	response *adapters.ResponseData
 	err      error
 }
+
+// This function adds an httptrace.ClientTrace object to the context so, if connection with the bidder
+// 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
+
+	trace := &httptrace.ClientTrace{
+		// GetConn is called before a connection is created or retrieved from an idle pool
+		GetConn: func(hostPort string) {
+			connStart = time.Now()
+		},
+		// GotConn is called after a successful connection is obtained
+		GotConn: func(info httptrace.GotConnInfo) {
+			connWaitTime := time.Now().Sub(connStart)
+
+			bidder.me.RecordAdapterConnections(bidder.BidderName, info.Reused, connWaitTime)
+		},
+		// DNSStart is called when a DNS lookup begins.
+		DNSStart: func(info httptrace.DNSStartInfo) {
+			dnsStart = time.Now()
+		},
+		// DNSDone is called when a DNS lookup ends.
+		DNSDone: func(info httptrace.DNSDoneInfo) {
+			dnsLookupTime := time.Now().Sub(dnsStart)
+
+			bidder.me.RecordDNSTime(dnsLookupTime)
+		},
+	}
+	return httptrace.WithClientTrace(ctx, trace)
+}
diff --git a/go.mod b/go.mod
index 72bb9b74..00cadd31 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ require (
 	github.com/influxdata/influxdb v1.6.1 // indirect
 	github.com/julienschmidt/httprouter v1.1.0
 	github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
+	github.com/kr/pretty v0.2.0 // indirect
 	github.com/lib/pq v1.0.0
 	github.com/magiconair/properties v1.8.0
 	github.com/mattn/go-colorable v0.1.2 // indirect
diff --git a/go.sum b/go.sum
index 35b2b765..5eaf37ca 100644
--- a/go.sum
+++ b/go.sum
@@ -45,6 +45,11 @@ github.com/julienschmidt/httprouter v1.1.0 h1:7wLdtIiIpzOkC9u6sXOozpBauPdskj3ru4
 github.com/julienschmidt/httprouter v1.1.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
 github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
+github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go
index 0dbe9a69..6a36f9e7 100644
--- a/pbsmetrics/config/metrics.go
+++ b/pbsmetrics/config/metrics.go
@@ -37,7 +37,7 @@ func NewMetricsEngine(cfg *config.Configuration, adapterList []openrtb_ext.Bidde
 	}
 	if cfg.Metrics.Prometheus.Port != 0 {
 		// Set up the Prometheus metrics.
-		returnEngine.PrometheusMetrics = prometheusmetrics.NewMetrics(cfg.Metrics.Prometheus)
+		returnEngine.PrometheusMetrics = prometheusmetrics.NewMetrics(cfg.Metrics.Prometheus, cfg.Metrics.Disabled)
 		engineList = append(engineList, returnEngine.PrometheusMetrics)
 	}
 
@@ -118,6 +118,21 @@ func (me *MultiMetricsEngine) RecordAdapterRequest(labels pbsmetrics.AdapterLabe
 	}
 }
 
+// Keeps track of created and reused connections to adapter bidders and the time from the
+// connection request, to the connection creation, or reuse from the pool across all engines
+func (me *MultiMetricsEngine) RecordAdapterConnections(bidderName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) {
+	for _, thisME := range *me {
+		thisME.RecordAdapterConnections(bidderName, connWasReused, connWaitTime)
+	}
+}
+
+// Times the DNS resolution process
+func (me *MultiMetricsEngine) RecordDNSTime(dnsLookupTime time.Duration) {
+	for _, thisME := range *me {
+		thisME.RecordDNSTime(dnsLookupTime)
+	}
+}
+
 // RecordAdapterBidReceived across all engines
 func (me *MultiMetricsEngine) RecordAdapterBidReceived(labels pbsmetrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
 	for _, thisME := range *me {
@@ -237,6 +252,14 @@ func (me *DummyMetricsEngine) RecordAdapterPanic(labels pbsmetrics.AdapterLabels
 func (me *DummyMetricsEngine) RecordAdapterRequest(labels pbsmetrics.AdapterLabels) {
 }
 
+// RecordAdapterConnections as a noop
+func (me *DummyMetricsEngine) RecordAdapterConnections(bidderName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) {
+}
+
+// RecordDNSTime as a noop
+func (me *DummyMetricsEngine) RecordDNSTime(dnsLookupTime 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 836434bf..26f6ce07 100644
--- a/pbsmetrics/go_metrics.go
+++ b/pbsmetrics/go_metrics.go
@@ -29,6 +29,7 @@ type Metrics struct {
 	PrebidCacheRequestTimerError   metrics.Timer
 	StoredReqCacheMeter            map[CacheResult]metrics.Meter
 	StoredImpCacheMeter            map[CacheResult]metrics.Meter
+	DNSLookupTimer                 metrics.Timer
 
 	// Metrics for OpenRTB requests specifically. So we can track what % of RequestsMeter are OpenRTB
 	// and know when legacy requests have been abandoned.
@@ -81,6 +82,9 @@ type AdapterMetrics struct {
 	BidsReceivedMeter metrics.Meter
 	PanicMeter        metrics.Meter
 	MarkupMetrics     map[openrtb_ext.BidType]*MarkupDeliveryMetrics
+	ConnCreated       metrics.Counter
+	ConnReused        metrics.Counter
+	ConnWaitTime      metrics.Timer
 }
 
 type MarkupDeliveryMetrics struct {
@@ -106,7 +110,7 @@ const unknownBidder openrtb_ext.BidderName = "unknown"
 // rather than loading legacy metrics that never get filled.
 // This will also eventually let us configure metrics, such as setting a limited set of metrics
 // for a production instance, and then expanding again when we need more debugging.
-func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, disableMetrics config.DisabledMetrics) *Metrics {
+func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, disabledMetrics config.DisabledMetrics) *Metrics {
 	blankMeter := &metrics.NilMeter{}
 	blankTimer := &metrics.NilTimer{}
 
@@ -123,6 +127,7 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 		SafariRequestMeter:             blankMeter,
 		SafariNoCookieMeter:            blankMeter,
 		RequestTimer:                   blankTimer,
+		DNSLookupTimer:                 blankTimer,
 		RequestsQueueTimer:             make(map[RequestType]map[bool]metrics.Timer),
 		PrebidCacheRequestTimerSuccess: blankTimer,
 		PrebidCacheRequestTimerError:   blankTimer,
@@ -153,13 +158,13 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 
 		AdapterMetrics:  make(map[openrtb_ext.BidderName]*AdapterMetrics, len(exchanges)),
 		accountMetrics:  make(map[string]*accountMetrics),
-		MetricsDisabled: disableMetrics,
+		MetricsDisabled: disabledMetrics,
 
 		exchanges: exchanges,
 	}
 
 	for _, a := range exchanges {
-		newMetrics.AdapterMetrics[a] = makeBlankAdapterMetrics()
+		newMetrics.AdapterMetrics[a] = makeBlankAdapterMetrics(newMetrics.MetricsDisabled)
 	}
 
 	for _, t := range RequestTypes() {
@@ -209,6 +214,7 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d
 	newMetrics.AppRequestMeter = metrics.GetOrRegisterMeter("app_requests", registry)
 	newMetrics.SafariNoCookieMeter = metrics.GetOrRegisterMeter("safari_no_cookie_requests", registry)
 	newMetrics.RequestTimer = metrics.GetOrRegisterTimer("request_time", registry)
+	newMetrics.DNSLookupTimer = metrics.GetOrRegisterTimer("dns_lookup_time", registry)
 	newMetrics.PrebidCacheRequestTimerSuccess = metrics.GetOrRegisterTimer("prebid_cache_request_time.ok", registry)
 	newMetrics.PrebidCacheRequestTimerError = metrics.GetOrRegisterTimer("prebid_cache_request_time.err", registry)
 
@@ -255,7 +261,7 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d
 }
 
 // Part of setting up blank metrics, the adapter metrics.
-func makeBlankAdapterMetrics() *AdapterMetrics {
+func makeBlankAdapterMetrics(disabledMetrics config.DisabledMetrics) *AdapterMetrics {
 	blankMeter := &metrics.NilMeter{}
 	newAdapter := &AdapterMetrics{
 		NoCookieMeter:     blankMeter,
@@ -268,6 +274,11 @@ func makeBlankAdapterMetrics() *AdapterMetrics {
 		PanicMeter:        blankMeter,
 		MarkupMetrics:     makeBlankBidMarkupMetrics(),
 	}
+	if !disabledMetrics.AdapterConnectionMetrics {
+		newAdapter.ConnCreated = metrics.NilCounter{}
+		newAdapter.ConnReused = metrics.NilCounter{}
+		newAdapter.ConnWaitTime = &metrics.NilTimer{}
+	}
 	for _, err := range AdapterErrors() {
 		newAdapter.ErrorMeters[err] = blankMeter
 	}
@@ -302,6 +313,9 @@ func registerAdapterMetrics(registry metrics.Registry, adapterOrAccount string,
 		openrtb_ext.BidTypeAudio:  makeDeliveryMetrics(registry, adapterOrAccount+"."+exchange, openrtb_ext.BidTypeAudio),
 		openrtb_ext.BidTypeNative: makeDeliveryMetrics(registry, adapterOrAccount+"."+exchange, openrtb_ext.BidTypeNative),
 	}
+	am.ConnCreated = metrics.GetOrRegisterCounter(fmt.Sprintf("%[1]s.%[2]s.connections_created", adapterOrAccount, exchange), registry)
+	am.ConnReused = metrics.GetOrRegisterCounter(fmt.Sprintf("%[1]s.%[2]s.connections_reused", adapterOrAccount, exchange), registry)
+	am.ConnWaitTime = metrics.GetOrRegisterTimer(fmt.Sprintf("%[1]s.%[2]s.connection_wait_time", adapterOrAccount, exchange), registry)
 	for err := range am.ErrorMeters {
 		am.ErrorMeters[err] = metrics.GetOrRegisterMeter(fmt.Sprintf("%s.%s.requests.%s", adapterOrAccount, exchange, err), registry)
 	}
@@ -348,7 +362,7 @@ func (me *Metrics) getAccountMetrics(id string) *accountMetrics {
 	am.adapterMetrics = make(map[openrtb_ext.BidderName]*AdapterMetrics, len(me.exchanges))
 	if !me.MetricsDisabled.AccountAdapterDetails {
 		for _, a := range me.exchanges {
-			am.adapterMetrics[a] = makeBlankAdapterMetrics()
+			am.adapterMetrics[a] = makeBlankAdapterMetrics(me.MetricsDisabled)
 			registerAdapterMetrics(me.MetricsRegistry, fmt.Sprintf("account.%s", id), string(a), am.adapterMetrics[a])
 		}
 	}
@@ -472,6 +486,34 @@ func (me *Metrics) RecordAdapterRequest(labels AdapterLabels) {
 	}
 }
 
+// Keeps track of created and reused connections to adapter bidders and the time from the
+// connection request, to the connection creation, or reuse from the pool across all engines
+func (me *Metrics) RecordAdapterConnections(adapterName openrtb_ext.BidderName,
+	connWasReused bool,
+	connWaitTime time.Duration) {
+
+	if me.MetricsDisabled.AdapterConnectionMetrics {
+		return
+	}
+
+	am, ok := me.AdapterMetrics[adapterName]
+	if !ok {
+		glog.Errorf("Trying to log adapter connection metrics for %s: adapter not found", string(adapterName))
+		return
+	}
+
+	if connWasReused {
+		am.ConnReused.Inc(1)
+	} else {
+		am.ConnCreated.Inc(1)
+	}
+	am.ConnWaitTime.Update(connWaitTime)
+}
+
+func (me *Metrics) RecordDNSTime(dnsLookupTime time.Duration) {
+	me.DNSLookupTimer.Update(dnsLookupTime)
+}
+
 // 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 514fbac1..8133bc73 100644
--- a/pbsmetrics/metrics.go
+++ b/pbsmetrics/metrics.go
@@ -301,6 +301,8 @@ type MetricsEngine interface {
 	RecordLegacyImps(labels Labels, numImps int)           // RecordImps for the legacy engine
 	RecordRequestTime(labels Labels, length time.Duration) // ignores adapter. only statusOk and statusErr fom status
 	RecordAdapterRequest(labels AdapterLabels)
+	RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration)
+	RecordDNSTime(dnsLookupTime 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 6c263f0a..42a2d1b4 100644
--- a/pbsmetrics/metrics_mock.go
+++ b/pbsmetrics/metrics_mock.go
@@ -52,6 +52,16 @@ func (me *MetricsEngineMock) RecordAdapterRequest(labels AdapterLabels) {
 	me.Called(labels)
 }
 
+// RecordAdapterConnections mock
+func (me *MetricsEngineMock) RecordAdapterConnections(bidderName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) {
+	me.Called(bidderName, connWasReused, connWaitTime)
+}
+
+// RecordDNSTime mock
+func (me *MetricsEngineMock) RecordDNSTime(dnsLookupTime time.Duration) {
+	me.Called(dnsLookupTime)
+}
+
 // RecordAdapterBidReceived mock
 func (me *MetricsEngineMock) RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
 	me.Called(labels, bidType, hasAdm)
diff --git a/pbsmetrics/prometheus/preload.go b/pbsmetrics/prometheus/preload.go
index ef1d300c..4f62a18a 100644
--- a/pbsmetrics/prometheus/preload.go
+++ b/pbsmetrics/prometheus/preload.go
@@ -85,6 +85,20 @@ func preloadLabelValues(m *Metrics) {
 		hasBidsLabel: boolValues,
 	})
 
+	if !m.metricsDisabled.AdapterConnectionMetrics {
+		preloadLabelValuesForCounter(m.adapterCreatedConnections, map[string][]string{
+			adapterLabel: adapterValues,
+		})
+
+		preloadLabelValuesForCounter(m.adapterReusedConnections, map[string][]string{
+			adapterLabel: adapterValues,
+		})
+
+		preloadLabelValuesForHistogram(m.adapterConnectionWaitTime, map[string][]string{
+			adapterLabel: adapterValues,
+		})
+	}
+
 	preloadLabelValuesForHistogram(m.adapterRequestsTimer, map[string][]string{
 		adapterLabel: adapterValues,
 	})
diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go
index d94c4d78..b42399b2 100644
--- a/pbsmetrics/prometheus/prometheus.go
+++ b/pbsmetrics/prometheus/prometheus.go
@@ -29,23 +29,29 @@ type Metrics struct {
 	storedImpressionsCacheResult *prometheus.CounterVec
 	storedRequestCacheResult     *prometheus.CounterVec
 	timeoutNotifications         *prometheus.CounterVec
+	dnsLookupTimer               prometheus.Histogram
 	privacyCCPA                  *prometheus.CounterVec
 	privacyCOPPA                 *prometheus.CounterVec
 	privacyLMT                   *prometheus.CounterVec
 	privacyTCF                   *prometheus.CounterVec
 
 	// Adapter Metrics
-	adapterBids          *prometheus.CounterVec
-	adapterCookieSync    *prometheus.CounterVec
-	adapterErrors        *prometheus.CounterVec
-	adapterPanics        *prometheus.CounterVec
-	adapterPrices        *prometheus.HistogramVec
-	adapterRequests      *prometheus.CounterVec
-	adapterRequestsTimer *prometheus.HistogramVec
-	adapterUserSync      *prometheus.CounterVec
+	adapterBids               *prometheus.CounterVec
+	adapterCookieSync         *prometheus.CounterVec
+	adapterErrors             *prometheus.CounterVec
+	adapterPanics             *prometheus.CounterVec
+	adapterPrices             *prometheus.HistogramVec
+	adapterRequests           *prometheus.CounterVec
+	adapterRequestsTimer      *prometheus.HistogramVec
+	adapterUserSync           *prometheus.CounterVec
+	adapterReusedConnections  *prometheus.CounterVec
+	adapterCreatedConnections *prometheus.CounterVec
+	adapterConnectionWaitTime *prometheus.HistogramVec
 
 	// Account Metrics
 	accountRequests *prometheus.CounterVec
+
+	metricsDisabled config.DisabledMetrics
 }
 
 const (
@@ -97,14 +103,15 @@ const (
 )
 
 // NewMetrics initializes a new Prometheus metrics instance with preloaded label values.
-func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
-	requestTimeBuckets := []float64{0.05, 0.1, 0.15, 0.20, 0.25, 0.3, 0.4, 0.5, 0.75, 1}
+func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMetrics) *Metrics {
+	standardTimeBuckets := []float64{0.05, 0.1, 0.15, 0.20, 0.25, 0.3, 0.4, 0.5, 0.75, 1}
 	cacheWriteTimeBuckets := []float64{0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1}
 	priceBuckets := []float64{250, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000}
 	queuedRequestTimeBuckets := []float64{0, 1, 5, 30, 60, 120, 180, 240, 300}
 
 	metrics := Metrics{}
 	metrics.Registry = prometheus.NewRegistry()
+	metrics.metricsDisabled = disabledMetrics
 
 	metrics.connectionsClosed = newCounterWithoutLabels(cfg, metrics.Registry,
 		"connections_closed",
@@ -132,7 +139,7 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
 		"impressions_requests_legacy",
 		"Count of requested impressions to Prebid Server using the legacy endpoint.")
 
-	metrics.prebidCacheWriteTimer = newHistogram(cfg, metrics.Registry,
+	metrics.prebidCacheWriteTimer = newHistogramVec(cfg, metrics.Registry,
 		"prebidcache_write_time_seconds",
 		"Seconds to write to Prebid Cache labeled by success or failure. Failure timing is limited by Prebid Server enforced timeouts.",
 		[]string{successLabel},
@@ -143,11 +150,11 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
 		"Count of total requests to Prebid Server labeled by type and status.",
 		[]string{requestTypeLabel, requestStatusLabel})
 
-	metrics.requestsTimer = newHistogram(cfg, metrics.Registry,
+	metrics.requestsTimer = newHistogramVec(cfg, metrics.Registry,
 		"request_time_seconds",
 		"Seconds to resolve successful Prebid Server requests labeled by type.",
 		[]string{requestTypeLabel},
-		requestTimeBuckets)
+		standardTimeBuckets)
 
 	metrics.requestsWithoutCookie = newCounter(cfg, metrics.Registry,
 		"requests_without_cookie",
@@ -169,6 +176,11 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
 		"Count of timeout notifications triggered, and if they were successfully sent.",
 		[]string{successLabel})
 
+	metrics.dnsLookupTimer = newHistogram(cfg, metrics.Registry,
+		"dns_lookup_time",
+		"Seconds to resolve DNS",
+		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 .",
@@ -209,7 +221,7 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
 		"Count of panics labeled by adapter.",
 		[]string{adapterLabel})
 
-	metrics.adapterPrices = newHistogram(cfg, metrics.Registry,
+	metrics.adapterPrices = newHistogramVec(cfg, metrics.Registry,
 		"adapter_prices",
 		"Monetary value of the bids labeled by adapter.",
 		[]string{adapterLabel},
@@ -220,11 +232,29 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
 		"Count of requests labeled by adapter, if has a cookie, and if it resulted in bids.",
 		[]string{adapterLabel, cookieLabel, hasBidsLabel})
 
-	metrics.adapterRequestsTimer = newHistogram(cfg, metrics.Registry,
+	if !metrics.metricsDisabled.AdapterConnectionMetrics {
+		metrics.adapterCreatedConnections = newCounter(cfg, metrics.Registry,
+			"adapter_connection_created",
+			"Count that keeps track of new connections when contacting adapter bidder endpoints.",
+			[]string{adapterLabel})
+
+		metrics.adapterReusedConnections = newCounter(cfg, metrics.Registry,
+			"adapter_connection_reused",
+			"Count that keeps track of reused connections when contacting adapter bidder endpoints.",
+			[]string{adapterLabel})
+
+		metrics.adapterConnectionWaitTime = newHistogramVec(cfg, metrics.Registry,
+			"adapter_connection_wait",
+			"Seconds from when the connection was requested until it is either created or reused",
+			[]string{adapterLabel},
+			standardTimeBuckets)
+	}
+
+	metrics.adapterRequestsTimer = newHistogramVec(cfg, metrics.Registry,
 		"adapter_request_time_seconds",
 		"Seconds to resolve each successful request labeled by adapter.",
 		[]string{adapterLabel},
-		requestTimeBuckets)
+		standardTimeBuckets)
 
 	metrics.adapterUserSync = newCounter(cfg, metrics.Registry,
 		"adapter_user_sync",
@@ -236,7 +266,7 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
 		"Count of total requests to Prebid Server labeled by account.",
 		[]string{accountLabel})
 
-	metrics.requestsQueueTimer = newHistogram(cfg, metrics.Registry,
+	metrics.requestsQueueTimer = newHistogramVec(cfg, metrics.Registry,
 		"request_queue_time",
 		"Seconds request was waiting in queue",
 		[]string{requestTypeLabel, requestStatusLabel},
@@ -271,7 +301,7 @@ func newCounterWithoutLabels(cfg config.PrometheusMetrics, registry *prometheus.
 	return counter
 }
 
-func newHistogram(cfg config.PrometheusMetrics, registry *prometheus.Registry, name, help string, labels []string, buckets []float64) *prometheus.HistogramVec {
+func newHistogramVec(cfg config.PrometheusMetrics, registry *prometheus.Registry, name, help string, labels []string, buckets []float64) *prometheus.HistogramVec {
 	opts := prometheus.HistogramOpts{
 		Namespace: cfg.Namespace,
 		Subsystem: cfg.Subsystem,
@@ -284,6 +314,19 @@ func newHistogram(cfg config.PrometheusMetrics, registry *prometheus.Registry, n
 	return histogram
 }
 
+func newHistogram(cfg config.PrometheusMetrics, registry *prometheus.Registry, name, help string, buckets []float64) prometheus.Histogram {
+	opts := prometheus.HistogramOpts{
+		Namespace: cfg.Namespace,
+		Subsystem: cfg.Subsystem,
+		Name:      name,
+		Help:      help,
+		Buckets:   buckets,
+	}
+	histogram := prometheus.NewHistogram(opts)
+	registry.MustRegister(histogram)
+	return histogram
+}
+
 func (m *Metrics) RecordConnectionAccept(success bool) {
 	if success {
 		m.connectionsOpened.Inc()
@@ -359,6 +402,32 @@ func (m *Metrics) RecordAdapterRequest(labels pbsmetrics.AdapterLabels) {
 	}
 }
 
+// Keeps track of created and reused connections to adapter bidders and the time from the
+// connection request, to the connection creation, or reuse from the pool across all engines
+func (m *Metrics) RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) {
+	if m.metricsDisabled.AdapterConnectionMetrics {
+		return
+	}
+
+	if connWasReused {
+		m.adapterReusedConnections.With(prometheus.Labels{
+			adapterLabel: string(adapterName),
+		}).Inc()
+	} else {
+		m.adapterCreatedConnections.With(prometheus.Labels{
+			adapterLabel: string(adapterName),
+		}).Inc()
+	}
+
+	m.adapterConnectionWaitTime.With(prometheus.Labels{
+		adapterLabel: string(adapterName),
+	}).Observe(connWaitTime.Seconds())
+}
+
+func (m *Metrics) RecordDNSTime(dnsLookupTime time.Duration) {
+	m.dnsLookupTimer.Observe(dnsLookupTime.Seconds())
+}
+
 func (m *Metrics) RecordAdapterPanic(labels pbsmetrics.AdapterLabels) {
 	m.adapterPanics.With(prometheus.Labels{
 		adapterLabel: string(labels.Adapter),
diff --git a/static/bidder-info/gumgum.yaml b/static/bidder-info/gumgum.yaml
index 0feca7cd..6ba563b4 100644
--- a/static/bidder-info/gumgum.yaml
+++ b/static/bidder-info/gumgum.yaml
@@ -4,3 +4,4 @@ capabilities:
   site:
     mediaTypes:
     - banner
+    - video
\ No newline at end of file
diff --git a/static/category-mapping/freewheel/freewheel.json b/static/category-mapping/freewheel/freewheel.json
index 1c4a4fa2..11529206 100644
--- a/static/category-mapping/freewheel/freewheel.json
+++ b/static/category-mapping/freewheel/freewheel.json
@@ -1178,5 +1178,81 @@
 	"IAB22-3": {
 		"id": "410",
 		"name": "Product"
+	},  
+	"IAB1": {
+	  "id": "392",
+	  "name": "Entertainment"
+	},
+	"IAB2": {
+	  "id": "399",
+	  "name": "Automotive"
+	},
+	"IAB3": {
+	  "id": "393",
+	  "name": "Business Services"
+	},
+	"IAB4": {
+	  "id": "405",
+	  "name": "Educational Services"
+	},
+	"IAB5": {
+	  "id": "405",
+	  "name": "Educational Services"
+	},
+	"IAB7": {
+	  "id": "406",
+	  "name": "Health Care Services"
+	},
+	"IAB8": {
+	  "id": "394",
+	  "name": "Food"
+	},
+	"IAB9": {
+	  "id": "392",
+	  "name": "Entertainment"
+	},
+	"IAB10": {
+	  "id": "434",
+	  "name": "Home Furnishings"
+	},
+	"IAB11": {
+	  "id": "398",
+	  "name": "Government/Municipal"
+	},
+	"IAB12": {
+	  "id": "438",
+	  "name": "News"
+	},
+	"IAB13": {
+	  "id": "393",
+	  "name": "Business Services"
+	},
+	"IAB16": {
+	  "id": "423",
+	  "name": "Pet Food/Supplies"
+	},
+	"IAB17": {
+	  "id": "425",
+	  "name": "Professional Sports"
+	},
+	"IAB18": {
+	  "id": "397",
+	  "name": "Apparel"
+	},
+	"IAB19": {
+	  "id": "409",
+	  "name": "Computing Product"
+	},
+	"IAB20": {
+	  "id": "395",
+	  "name": "Travel/Hotel/Airlines"
+	},
+	"IAB21": {
+	  "id": "416",
+	  "name": "Real Estate"
+	},
+	"IAB22": {
+	  "id": "403",
+	  "name": "Retail Stores/Chains"
 	}
 }
\ No newline at end of file


Test patch:
diff --git a/adapters/gumgum/gumgumtest/exemplary/video.json b/adapters/gumgum/gumgumtest/exemplary/video.json
new file mode 100644
index 00000000..ea76c733
--- /dev/null
+++ b/adapters/gumgum/gumgumtest/exemplary/video.json
@@ -0,0 +1,106 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "video": {
+                    "mimes": [
+                        "video/mp4"
+                    ],
+                    "minduration": 1,
+                    "maxduration": 2,
+                    "protocols": [
+                        1,
+                        2
+                    ],
+                    "w": 640,
+                    "h": 480,
+                    "startdelay": 1,
+                    "placement": 1,
+                    "linearity": 1
+                },
+                "ext": {
+                    "bidder": {
+                        "zone": "ggumtest"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "https://g2.gumgum.com/providers/prbds2s/bid",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "video": {
+                                "mimes": [
+                                    "video/mp4"
+                                ],
+                                "minduration": 1,
+                                "maxduration": 2,
+                                "protocols": [
+                                    1,
+                                    2
+                                ],
+                                "w": 640,
+                                "h": 480,
+                                "startdelay": 1,
+                                "placement": 1,
+                                "linearity": 1
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "zone": "ggumtest"
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "15da721e-940a-4db6-8621-a1f93140b21b",
+                                    "impid": "video1",
+                                    "price": 15,
+                                    "adid": "59082",
+                                    "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"3.0\">\n    <Ad id=\"59082\">\n        <InLine>\n            <AdSystem>GumGum Video</AdSystem>\n            <AdTitle>\n                <![CDATA[\n                Pre-Roll Preview - Olay - Linear (:15)\n                ]]>\n            </AdTitle>\n            <Impression>\n                <![CDATA[ \n                http://or-g2.gumgum.com/ad/apvideo/impression?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                ]]>\n            </Impression>\n            <Creatives>\n                <Creative>\n                    <Linear>\n                        <Duration>00:00:15</Duration>\n                        <TrackingEvents>\n                            <Tracking event=\"start\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/play?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"firstQuartile\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed25?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"midpoint\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed50?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"thirdQuartile\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed75?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"complete\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed100?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                        </TrackingEvents>\n                        <VideoClicks>\n                            <ClickThrough>\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/click/enc/_EFZ4AbNn_bHPd6fKLhwJUB_IyBmxpaj9eCAt4yjP8Lh-lInPc1v8-nOmpntWjYalCcBciU4c_QPQhN9g9uzF2lz0QBdNTDxiK5Q18wC0JZkSvT0xe4L6_C9mgmmP8BOUWcDV6bbhrCPr6OF6AHLlzcp2MaV4IJ-c6wh2MPyRbRvy5pxSHf-KbYM172krOmxW-37VSxq2yPDjQxDuUwd9XqxKUTXIYzDW_5VTwCzYCyus5vyPMNGQICKHpXkDscfR2EadcO2qRKhqC65pegIsnUg-Eq0Ey4rp7RLiPFjl0WjXO88KlRIgHMgd2-JYcHIumNnuYcy-hpr787Sk_Ejp_tMVujc6b4Cg634_YR52qWEwNqd4GFjWm0S6Hmy18v7I3SrGJPg7FzC1iGGOSMjfe1DJDplloE0qQcxKI9gI1aTJqG_G7PY6QP1Gz87Ojj7mLf40udAlRofZaq1FRdFXXgo0XIeg3J7o07-2TGPvBfdg0hIpis6TOWo-gcqbOKWOP24eiroudFCTDNH9QAi9dXKor0jUMLhKmvY6kvmNl8?du=\n                                ]]>\n                            </ClickThrough>\n                        </VideoClicks>\n                        <MediaFiles>\n                            <MediaFile bitrate=\"800\" delivery=\"progressive\" height=\"9\" type=\"video/mp4\" width=\"16\">\n                                <![CDATA[ https://c.gumgum.com/ads/com/procter_gamble/olay_daily_facials_q2_2017/pre_roll_demo/5_source_41397_100091.mp4\n                                ]]>\n                            </MediaFile>\n                        </MediaFiles>\n                    </Linear>\n                </Creative>\n            </Creatives>\n        <Impression><![CDATA[http://or-g2.gumgum.com/ad/view/enc/_EFZ4AbNn_bHPd6fKLhwJUB_IyBmxpaj9eCAt4yjP8Lh-lInPc1v8-nOmpntWjYalCcBciU4c_QPQhN9g9uzF2lz0QBdNTDxiK5Q18wC0JZkSvT0xe4L6_C9mgmmP8BOUWcDV6bbhrCPr6OF6AHLlzcp2MaV4IJ-c6wh2MPyRbRvy5pxSHf-KbYM172krOmxW-37VSxq2yPDjQxDuUwd9XqxKUTXIYzDW_5VTwCzYCyus5vyPMNGQJ8jwoXY9Y_XR2EadcO2qRKhqC65pegIsnUg-Eq0Ey4rp7RLiPFjl0WjXO88KlRIgHMgd2-JYcHIumNnuYcy-hpr787Sk_Ejp_tMVujc6b4Cg634_YR52qXaMMkNa6YwX71oVLIAHL6p6DL-8M2FwYgSPFHW1P72slMYnDS0_PfnysQjnRJ2eQXGmpiIEg0seK48nGbq14RwpQtD8WOwrIySXeuk0TqdyUQE2RMlq2dV?c=15]]></Impression><Error><![CDATA[http://or-g2.gumgum.com/video/error?code=[ERRORCODE]&adid=59082&t=ggumtest]]></Error></InLine>\n    </Ad>\n</VAST>",
+                                    "cid": "3579",
+                                    "crid": "59082"
+                                }
+                            ]
+                        }
+                    ]
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "currency": "USD",
+            "bids": [
+                {
+                    "bid": {
+                        "id": "15da721e-940a-4db6-8621-a1f93140b21b",
+                        "impid": "video1",
+                        "price": 15,
+                        "adid": "59082",
+                        "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"3.0\">\n    <Ad id=\"59082\">\n        <InLine>\n            <AdSystem>GumGum Video</AdSystem>\n            <AdTitle>\n                <![CDATA[\n                Pre-Roll Preview - Olay - Linear (:15)\n                ]]>\n            </AdTitle>\n            <Impression>\n                <![CDATA[ \n                http://or-g2.gumgum.com/ad/apvideo/impression?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                ]]>\n            </Impression>\n            <Creatives>\n                <Creative>\n                    <Linear>\n                        <Duration>00:00:15</Duration>\n                        <TrackingEvents>\n                            <Tracking event=\"start\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/play?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"firstQuartile\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed25?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"midpoint\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed50?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"thirdQuartile\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed75?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                            <Tracking event=\"complete\">\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/apvideo/completed100?vpi=1&t=ggumtest&ab=59082&pv=44df4775-15b1-4162-9970-43d06a519e34&pu=https%3A%2F%2Fwww.100daysofrealfood.com%2F&zdp=*&iimpid=16ddc020-1705-4351-9951-467474c8f385&lt=&to=&ts=1594835221365&er=0&pspc=prbds2s&jcsi=%7B%22t%22%3A1%2C%22rq%22%3A12%2C%22pbv%22%3A%220.0.0%22%7D&pbf=0.0\n                                ]]>\n                            </Tracking>\n                        </TrackingEvents>\n                        <VideoClicks>\n                            <ClickThrough>\n                                <![CDATA[\n                                http://or-g2.gumgum.com/ad/click/enc/_EFZ4AbNn_bHPd6fKLhwJUB_IyBmxpaj9eCAt4yjP8Lh-lInPc1v8-nOmpntWjYalCcBciU4c_QPQhN9g9uzF2lz0QBdNTDxiK5Q18wC0JZkSvT0xe4L6_C9mgmmP8BOUWcDV6bbhrCPr6OF6AHLlzcp2MaV4IJ-c6wh2MPyRbRvy5pxSHf-KbYM172krOmxW-37VSxq2yPDjQxDuUwd9XqxKUTXIYzDW_5VTwCzYCyus5vyPMNGQICKHpXkDscfR2EadcO2qRKhqC65pegIsnUg-Eq0Ey4rp7RLiPFjl0WjXO88KlRIgHMgd2-JYcHIumNnuYcy-hpr787Sk_Ejp_tMVujc6b4Cg634_YR52qWEwNqd4GFjWm0S6Hmy18v7I3SrGJPg7FzC1iGGOSMjfe1DJDplloE0qQcxKI9gI1aTJqG_G7PY6QP1Gz87Ojj7mLf40udAlRofZaq1FRdFXXgo0XIeg3J7o07-2TGPvBfdg0hIpis6TOWo-gcqbOKWOP24eiroudFCTDNH9QAi9dXKor0jUMLhKmvY6kvmNl8?du=\n                                ]]>\n                            </ClickThrough>\n                        </VideoClicks>\n                        <MediaFiles>\n                            <MediaFile bitrate=\"800\" delivery=\"progressive\" height=\"9\" type=\"video/mp4\" width=\"16\">\n                                <![CDATA[ https://c.gumgum.com/ads/com/procter_gamble/olay_daily_facials_q2_2017/pre_roll_demo/5_source_41397_100091.mp4\n                                ]]>\n                            </MediaFile>\n                        </MediaFiles>\n                    </Linear>\n                </Creative>\n            </Creatives>\n        <Impression><![CDATA[http://or-g2.gumgum.com/ad/view/enc/_EFZ4AbNn_bHPd6fKLhwJUB_IyBmxpaj9eCAt4yjP8Lh-lInPc1v8-nOmpntWjYalCcBciU4c_QPQhN9g9uzF2lz0QBdNTDxiK5Q18wC0JZkSvT0xe4L6_C9mgmmP8BOUWcDV6bbhrCPr6OF6AHLlzcp2MaV4IJ-c6wh2MPyRbRvy5pxSHf-KbYM172krOmxW-37VSxq2yPDjQxDuUwd9XqxKUTXIYzDW_5VTwCzYCyus5vyPMNGQJ8jwoXY9Y_XR2EadcO2qRKhqC65pegIsnUg-Eq0Ey4rp7RLiPFjl0WjXO88KlRIgHMgd2-JYcHIumNnuYcy-hpr787Sk_Ejp_tMVujc6b4Cg634_YR52qXaMMkNa6YwX71oVLIAHL6p6DL-8M2FwYgSPFHW1P72slMYnDS0_PfnysQjnRJ2eQXGmpiIEg0seK48nGbq14RwpQtD8WOwrIySXeuk0TqdyUQE2RMlq2dV?c=15]]></Impression><Error><![CDATA[http://or-g2.gumgum.com/video/error?code=[ERRORCODE]&adid=59082&t=ggumtest]]></Error></InLine>\n    </Ad>\n</VAST>",
+                        "cid": "3579",
+                        "crid": "59082"
+                    },
+                    "type": "video"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/gumgum/gumgumtest/params/race/video.json b/adapters/gumgum/gumgumtest/params/race/video.json
new file mode 100644
index 00000000..3ed28438
--- /dev/null
+++ b/adapters/gumgum/gumgumtest/params/race/video.json
@@ -0,0 +1,3 @@
+{
+    "zone": "dc9d6be1"
+}
\ No newline at end of file
diff --git a/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json b/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json
new file mode 100644
index 00000000..b2475cd7
--- /dev/null
+++ b/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json
@@ -0,0 +1,36 @@
+{
+    "mockBidRequest": {
+      "id": "test-request-id",
+      "imp": [
+        {
+            "id": "test-imp-id",
+            "video": {
+                "mimes": [
+                    "video/mp4"
+                ],
+                "protocols": [
+                    1,
+                    2
+                ],
+                "w": 640,
+                "h": 480,
+                "startdelay": 1,
+                "placement": 1,
+                "linearity": 1
+            },
+            "ext": {
+                "bidder": {
+                    "zone": "ggumtest"
+                }
+            }
+        }
+    ]
+    },
+    "expectedMakeRequestsErrors": [
+      {
+        "value": "Invalid or missing video field(s)",
+        "comparison": "literal"
+      }
+    ]
+  }
+  
\ No newline at end of file
diff --git a/adapters/pubmatic/pubmatic_test.go b/adapters/pubmatic/pubmatic_test.go
index be086f5a..5dd236d9 100644
--- a/adapters/pubmatic/pubmatic_test.go
+++ b/adapters/pubmatic/pubmatic_test.go
@@ -674,18 +674,18 @@ func TestPubmaticSampleRequest(t *testing.T) {
 }
 
 func TestGetBidTypeVideo(t *testing.T) {
-	extJSON := `{"BidType":1}`
-	extrm := json.RawMessage(extJSON)
-	actualBidTypeValue := getBidType(extrm)
+	pubmaticExt := new(pubmaticBidExt)
+	pubmaticExt.BidType = new(int)
+	*pubmaticExt.BidType = 1
+	actualBidTypeValue := getBidType(pubmaticExt)
 	if actualBidTypeValue != openrtb_ext.BidTypeVideo {
 		t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeVideo, actualBidTypeValue)
 	}
 }
 
 func TestGetBidTypeForMissingBidTypeExt(t *testing.T) {
-	extJSON := `{}`
-	extrm := json.RawMessage(extJSON)
-	actualBidTypeValue := getBidType(extrm)
+	pubmaticExt := pubmaticBidExt{}
+	actualBidTypeValue := getBidType(&pubmaticExt)
 	// banner is the default bid type when no bidType key is present in the bid.ext
 	if actualBidTypeValue != "banner" {
 		t.Errorf("Expected Bid Type value was: banner, actual value is: %v", actualBidTypeValue)
@@ -693,27 +693,30 @@ func TestGetBidTypeForMissingBidTypeExt(t *testing.T) {
 }
 
 func TestGetBidTypeBanner(t *testing.T) {
-	extJSON := `{"BidType":0}`
-	extrm := json.RawMessage(extJSON)
-	actualBidTypeValue := getBidType(extrm)
+	pubmaticExt := new(pubmaticBidExt)
+	pubmaticExt.BidType = new(int)
+	*pubmaticExt.BidType = 0
+	actualBidTypeValue := getBidType(pubmaticExt)
 	if actualBidTypeValue != openrtb_ext.BidTypeBanner {
 		t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeBanner, actualBidTypeValue)
 	}
 }
 
 func TestGetBidTypeNative(t *testing.T) {
-	extJSON := `{"BidType":2}`
-	extrm := json.RawMessage(extJSON)
-	actualBidTypeValue := getBidType(extrm)
+	pubmaticExt := new(pubmaticBidExt)
+	pubmaticExt.BidType = new(int)
+	*pubmaticExt.BidType = 2
+	actualBidTypeValue := getBidType(pubmaticExt)
 	if actualBidTypeValue != openrtb_ext.BidTypeNative {
 		t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeNative, actualBidTypeValue)
 	}
 }
 
 func TestGetBidTypeForUnsupportedCode(t *testing.T) {
-	extJSON := `{"BidType":99}`
-	extrm := json.RawMessage(extJSON)
-	actualBidTypeValue := getBidType(extrm)
+	pubmaticExt := new(pubmaticBidExt)
+	pubmaticExt.BidType = new(int)
+	*pubmaticExt.BidType = 99
+	actualBidTypeValue := getBidType(pubmaticExt)
 	if actualBidTypeValue != openrtb_ext.BidTypeBanner {
 		t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeBanner, actualBidTypeValue)
 	}
diff --git a/adapters/pubmatic/pubmatictest/exemplary/video.json b/adapters/pubmatic/pubmatictest/exemplary/video.json
index 25ed366e..4c874535 100644
--- a/adapters/pubmatic/pubmatictest/exemplary/video.json
+++ b/adapters/pubmatic/pubmatictest/exemplary/video.json
@@ -112,10 +112,14 @@
                   "h": 250,
                   "w": 300,
                   "dealid":"test deal",
+                  "cat" : ["IAB-1", "IAB-2"], 
                   "ext": {
                     "dspid": 6,
                     "deal_channel": 1,
-                    "BidType": 1
+                    "BidType": 1,
+                    "video" : {
+                      "duration" : 5  
+                    }
                   }
                 }]
               }
@@ -139,6 +143,9 @@
               "adid": "29681110",
               "adm": "some-test-ad",
               "adomain": ["pubmatic.com"],
+              "cat": [
+                "IAB-1"
+              ],
               "crid": "29681110",
               "w": 300,
               "h": 250,
@@ -146,12 +153,18 @@
               "ext": {
                 "dspid": 6,
                 "deal_channel": 1,
-                "BidType": 1
+                "BidType": 1,
+                "video" : {
+                  "duration" : 5  
+                }
               }
             },
-            "type": "video"
+            "type": "video",
+            "video" :{
+              "duration" : 5
+            }
           }
         ]
       }
     ]
-  }
\ No newline at end of file
+  }
diff --git a/config/config_test.go b/config/config_test.go
index 3456694d..4774d9d6 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -33,6 +33,7 @@ func TestDefaults(t *testing.T) {
 	cmpBools(t, "account_required", cfg.AccountRequired, false)
 	cmpInts(t, "metrics.influxdb.collection_rate_seconds", cfg.Metrics.Influxdb.MetricSendInterval, 20)
 	cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, false)
+	cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true)
 	cmpStrings(t, "certificates_file", cfg.PemCertsFile, "")
 }
 
@@ -89,6 +90,7 @@ metrics:
     metric_send_interval: 30
   disabled_metrics:
     account_adapter_details: true
+    adapter_connections_metrics: true
 datacache:
   type: postgres
   filename: /usr/db/db.db
@@ -294,6 +296,7 @@ func TestFullConfig(t *testing.T) {
 	cmpStrings(t, "adapters.rhythmone.usersync_url", cfg.Adapters[string(openrtb_ext.BidderRhythmone)].UserSyncURL, "https://sync.1rx.io/usersync2/rmphb?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=http%3A%2F%2Fprebid-server.prebid.org%2F%2Fsetuid%3Fbidder%3Drhythmone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5BRX_UUID%5D")
 	cmpBools(t, "account_required", cfg.AccountRequired, true)
 	cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, true)
+	cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true)
 	cmpStrings(t, "certificates_file", cfg.PemCertsFile, "/etc/ssl/cert.pem")
 	cmpStrings(t, "request_validation.ipv4_private_networks", cfg.RequestValidation.IPv4PrivateNetworks[0], "1.1.1.0/24")
 	cmpStrings(t, "request_validation.ipv6_private_networks", cfg.RequestValidation.IPv6PrivateNetworks[0], "1111::/16")
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index d4fc0cf7..1a27b72a 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -6,8 +6,11 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
+	"net/http/httptrace"
+	"strings"
 	"testing"
 	"time"
 
@@ -17,8 +20,11 @@ import (
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/currencies"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/pbsmetrics"
+	metricsConf "github.com/prebid/prebid-server/pbsmetrics/config"
 	metricsConfig "github.com/prebid/prebid-server/pbsmetrics/config"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
 	nativeRequests "github.com/mxmCherry/openrtb/native/request"
 	nativeResponse "github.com/mxmCherry/openrtb/native/response"
@@ -68,7 +74,7 @@ func TestSingleBidder(t *testing.T) {
 		},
 		bidResponse: mockBidderResponse,
 	}
-	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
+	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus)
 	currencyConverter := currencies.NewRateConverterDefault()
 	seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{})
 
@@ -156,7 +162,7 @@ func TestMultiBidder(t *testing.T) {
 			}},
 		bidResponse: mockBidderResponse,
 	}
-	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
+	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus)
 	currencyConverter := currencies.NewRateConverterDefault()
 	seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{})
 
@@ -194,8 +200,10 @@ func TestBidderTimeout(t *testing.T) {
 	defer server.Close()
 
 	bidder := &bidderAdapter{
-		Bidder: &mixedMultiBidder{},
-		Client: server.Client(),
+		Bidder:     &mixedMultiBidder{},
+		BidderName: openrtb_ext.BidderAppnexus,
+		Client:     server.Client(),
+		me:         &metricsConf.DummyMetricsEngine{},
 	}
 
 	callInfo := bidder.doRequest(ctx, &adapters.RequestData{
@@ -235,8 +243,10 @@ func TestConnectionClose(t *testing.T) {
 	server = httptest.NewServer(handler)
 
 	bidder := &bidderAdapter{
-		Bidder: &mixedMultiBidder{},
-		Client: server.Client(),
+		Bidder:     &mixedMultiBidder{},
+		Client:     server.Client(),
+		BidderName: openrtb_ext.BidderAppnexus,
+		me:         &metricsConf.DummyMetricsEngine{},
 	}
 
 	callInfo := bidder.doRequest(context.Background(), &adapters.RequestData{
@@ -514,7 +524,7 @@ func TestMultiCurrencies(t *testing.T) {
 		)
 
 		// Execute:
-		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
+		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus)
 		currencyConverter := currencies.NewRateConverter(
 			&http.Client{},
 			mockedHTTPServer.URL,
@@ -663,7 +673,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) {
 		}
 
 		// Execute:
-		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
+		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus)
 		currencyConverter := currencies.NewRateConverterDefault()
 		seatBid, errs := bidder.requestBid(
 			context.Background(),
@@ -829,7 +839,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) {
 		}
 
 		// Execute:
-		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
+		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus)
 		currencyConverter := currencies.NewRateConverter(
 			&http.Client{},
 			mockedHTTPServer.URL,
@@ -945,7 +955,7 @@ func TestServerCallDebugging(t *testing.T) {
 			Headers: http.Header{},
 		},
 	}
-	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
+	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus)
 	currencyConverter := currencies.NewRateConverterDefault()
 
 	bids, _ := bidder.requestBid(
@@ -1057,7 +1067,7 @@ func TestMobileNativeTypes(t *testing.T) {
 			},
 			bidResponse: tc.mockBidderResponse,
 		}
-		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
+		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus)
 		currencyConverter := currencies.NewRateConverterDefault()
 
 		seatBids, _ := bidder.requestBid(
@@ -1078,7 +1088,7 @@ func TestMobileNativeTypes(t *testing.T) {
 }
 
 func TestErrorReporting(t *testing.T) {
-	bidder := adaptBidder(&bidRejector{}, nil, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
+	bidder := adaptBidder(&bidRejector{}, nil, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus)
 	currencyConverter := currencies.NewRateConverterDefault()
 	bids, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{})
 	if bids != nil {
@@ -1233,6 +1243,82 @@ func TestSetAssetTypes(t *testing.T) {
 	}
 }
 
+func TestCallRecordAdapterConnections(t *testing.T) {
+	// Setup mock server
+	respStatus := 200
+	respBody := "{\"bid\":false}"
+	server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody))
+	defer server.Close()
+
+	// declare requestBid parameters
+	bidAdjustment := 2.0
+
+	bidderImpl := &goodSingleBidder{
+		httpRequest: &adapters.RequestData{
+			Method:  "POST",
+			Uri:     server.URL,
+			Body:    []byte("{\"key\":\"val\"}"),
+			Headers: http.Header{},
+		},
+		bidResponse: &adapters.BidderResponse{},
+	}
+
+	// setup a mock metrics engine and its expectation
+	metrics := &pbsmetrics.MetricsEngineMock{}
+	expectedAdapterName := openrtb_ext.BidderAppnexus
+	compareConnWaitTime := func(dur time.Duration) bool { return dur.Nanoseconds() > 0 }
+
+	metrics.On("RecordAdapterConnections", expectedAdapterName, false, mock.MatchedBy(compareConnWaitTime)).Once()
+
+	// Run requestBid using an http.Client with a mock handler
+	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, metrics, openrtb_ext.BidderAppnexus)
+	_, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", bidAdjustment, currencies.NewRateConverterDefault().Rates(), &adapters.ExtraRequestInfo{})
+
+	// Assert no errors
+	assert.Equal(t, 0, len(errs), "bidder.requestBid returned errors %v \n", errs)
+
+	// Assert RecordAdapterConnections() was called with the parameters we expected
+	metrics.AssertExpectations(t)
+}
+
+type DNSDoneTripper struct{}
+
+func (DNSDoneTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	//Access the httptrace.ClientTrace
+	trace := httptrace.ContextClientTrace(req.Context())
+
+	//Force DNSDone call defined in exchange/bidder.go
+	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
+}
+
+func TestCallRecordRecordDNSTime(t *testing.T) {
+	// setup a mock metrics engine and its expectation
+	metricsMock := &pbsmetrics.MetricsEngineMock{}
+	metricsMock.Mock.On("RecordDNSTime", 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: DNSDoneTripper{}},
+		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
@@ -1248,10 +1334,10 @@ func TestTimeoutNotificationOff(t *testing.T) {
 		},
 	}
 	bidder := &bidderAdapter{
-		Bidder:      bidderImpl,
-		Client:      server.Client(),
-		DebugConfig: config.Debug{},
-		me:          &metricsConfig.DummyMetricsEngine{},
+		Bidder: bidderImpl,
+		Client: server.Client(),
+		config: bidderAdapterConfig{Debug: config.Debug{}},
+		me:     &metricsConf.DummyMetricsEngine{},
 	}
 	if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); !ok {
 		t.Error("Failed to cast bidder to a TimeoutBidder")
@@ -1284,13 +1370,15 @@ func TestTimeoutNotificationOn(t *testing.T) {
 	bidderAdapter := &bidderAdapter{
 		Bidder: bidderWrappedWithInfo,
 		Client: server.Client(),
-		DebugConfig: config.Debug{
-			TimeoutNotification: config.TimeoutNotification{
-				Log:          true,
-				SamplingRate: 1.0,
+		config: bidderAdapterConfig{
+			Debug: config.Debug{
+				TimeoutNotification: config.TimeoutNotification{
+					Log:          true,
+					SamplingRate: 1.0,
+				},
 			},
 		},
-		me: &metricsConfig.DummyMetricsEngine{},
+		me: &metricsConf.DummyMetricsEngine{},
 	}
 
 	// Unwrap To Mimic exchange.go Casting Code
diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go
index 72de1d42..16955e97 100644
--- a/exchange/targeting_test.go
+++ b/exchange/targeting_test.go
@@ -134,7 +134,7 @@ func buildAdapterMap(bids map[openrtb_ext.BidderName][]*openrtb.Bid, mockServerU
 		adapterMap[bidder] = adaptBidder(&mockTargetingBidder{
 			mockServerURL: mockServerURL,
 			bids:          bids,
-		}, client, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
+		}, client, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus)
 	}
 	return adapterMap
 }
diff --git a/pbsmetrics/go_metrics_test.go b/pbsmetrics/go_metrics_test.go
index 2faa0849..f6769916 100644
--- a/pbsmetrics/go_metrics_test.go
+++ b/pbsmetrics/go_metrics_test.go
@@ -2,6 +2,7 @@ package pbsmetrics
 
 import (
 	"testing"
+	"time"
 
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
@@ -115,6 +116,10 @@ func ensureContainsAdapterMetrics(t *testing.T, registry metrics.Registry, name
 	ensureContains(t, registry, name+".request_time", adapterMetrics.RequestTimer)
 	ensureContains(t, registry, name+".prices", adapterMetrics.PriceHistogram)
 	ensureContainsBidTypeMetrics(t, registry, name, adapterMetrics.MarkupMetrics)
+
+	ensureContains(t, registry, name+".connections_created", adapterMetrics.ConnCreated)
+	ensureContains(t, registry, name+".connections_reused", adapterMetrics.ConnReused)
+	ensureContains(t, registry, name+".connection_wait_time", adapterMetrics.ConnWaitTime)
 }
 
 func TestRecordBidTypeDisabledConfig(t *testing.T) {
@@ -179,6 +184,140 @@ func TestRecordBidTypeDisabledConfig(t *testing.T) {
 	}
 }
 
+func TestRecordDNSTime(t *testing.T) {
+	testCases := []struct {
+		description         string
+		inDnsLookupDuration time.Duration
+		outExpDuration      time.Duration
+	}{
+		{
+			description:         "Five second DNS lookup time",
+			inDnsLookupDuration: time.Second * 5,
+			outExpDuration:      time.Second * 5,
+		},
+		{
+			description:         "Zero DNS lookup time",
+			inDnsLookupDuration: time.Duration(0),
+			outExpDuration:      time.Duration(0),
+		},
+	}
+	for _, test := range testCases {
+		registry := metrics.NewRegistry()
+		m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AccountAdapterDetails: true})
+
+		m.RecordDNSTime(test.inDnsLookupDuration)
+
+		assert.Equal(t, test.outExpDuration.Nanoseconds(), m.DNSLookupTimer.Sum(), test.description)
+	}
+}
+
+func TestRecordAdapterConnections(t *testing.T) {
+	var fakeBidder openrtb_ext.BidderName = "fooAdvertising"
+
+	type testIn struct {
+		adapterName         openrtb_ext.BidderName
+		connWasReused       bool
+		connWait            time.Duration
+		connMetricsDisabled bool
+	}
+
+	type testOut struct {
+		expectedConnReusedCount  int64
+		expectedConnCreatedCount int64
+		expectedConnWaitTime     time.Duration
+	}
+
+	testCases := []struct {
+		description string
+		in          testIn
+		out         testOut
+	}{
+		{
+			description: "Successful, new connection created, has connection wait",
+			in: testIn{
+				adapterName:         openrtb_ext.BidderAppnexus,
+				connWasReused:       false,
+				connWait:            time.Second * 5,
+				connMetricsDisabled: false,
+			},
+			out: testOut{
+				expectedConnReusedCount:  0,
+				expectedConnCreatedCount: 1,
+				expectedConnWaitTime:     time.Second * 5,
+			},
+		},
+		{
+			description: "Successful, new connection created, has connection wait",
+			in: testIn{
+				adapterName:         openrtb_ext.BidderAppnexus,
+				connWasReused:       false,
+				connWait:            time.Second * 4,
+				connMetricsDisabled: false,
+			},
+			out: testOut{
+				expectedConnCreatedCount: 1,
+				expectedConnWaitTime:     time.Second * 4,
+			},
+		},
+		{
+			description: "Successful, was reused, no connection wait",
+			in: testIn{
+				adapterName:         openrtb_ext.BidderAppnexus,
+				connWasReused:       true,
+				connMetricsDisabled: false,
+			},
+			out: testOut{
+				expectedConnReusedCount: 1,
+				expectedConnWaitTime:    0,
+			},
+		},
+		{
+			description: "Successful, was reused, has connection wait",
+			in: testIn{
+				adapterName:         openrtb_ext.BidderAppnexus,
+				connWasReused:       true,
+				connWait:            time.Second * 5,
+				connMetricsDisabled: false,
+			},
+			out: testOut{
+				expectedConnReusedCount: 1,
+				expectedConnWaitTime:    time.Second * 5,
+			},
+		},
+		{
+			description: "Fake bidder, nothing gets updated",
+			in: testIn{
+				adapterName:         fakeBidder,
+				connWasReused:       false,
+				connWait:            0,
+				connMetricsDisabled: false,
+			},
+			out: testOut{},
+		},
+		{
+			description: "Adapter connection metrics are disabled, nothing gets updated",
+			in: testIn{
+				adapterName:         openrtb_ext.BidderAppnexus,
+				connWasReused:       false,
+				connWait:            time.Second * 5,
+				connMetricsDisabled: true,
+			},
+			out: testOut{},
+		},
+	}
+
+	for i, test := range testCases {
+		registry := metrics.NewRegistry()
+		m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AdapterConnectionMetrics: test.in.connMetricsDisabled})
+
+		m.RecordAdapterConnections(test.in.adapterName, test.in.connWasReused, test.in.connWait)
+
+		assert.Equal(t, test.out.expectedConnReusedCount, m.AdapterMetrics[openrtb_ext.BidderAppnexus].ConnReused.Count(), "Test [%d] incorrect number of reused connections to adapter", i)
+		assert.Equal(t, test.out.expectedConnCreatedCount, m.AdapterMetrics[openrtb_ext.BidderAppnexus].ConnCreated.Count(), "Test [%d] incorrect number of new connections to adapter created", i)
+		assert.Equal(t, test.out.expectedConnWaitTime.Nanoseconds(), m.AdapterMetrics[openrtb_ext.BidderAppnexus].ConnWaitTime.Sum(), "Test [%d] incorrect wait time in connection to adapter", i)
+	}
+}
+
 func TestNewMetricsWithDisabledConfig(t *testing.T) {
 	registry := metrics.NewRegistry()
 	m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true})
diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go
index b722ab28..b6153b16 100644
--- a/pbsmetrics/prometheus/prometheus_test.go
+++ b/pbsmetrics/prometheus/prometheus_test.go
@@ -1,6 +1,7 @@
 package prometheusmetrics
 
 import (
+	"fmt"
 	"testing"
 	"time"
 
@@ -17,7 +18,7 @@ func createMetricsForTesting() *Metrics {
 		Port:      8080,
 		Namespace: "prebid",
 		Subsystem: "server",
-	})
+	}, config.DisabledMetrics{})
 }
 
 func TestMetricCountGatekeeping(t *testing.T) {
@@ -61,7 +62,7 @@ func TestMetricCountGatekeeping(t *testing.T) {
 	// Verify Per-Adapter Cardinality
 	// - This assertion provides a warning for newly added adapter metrics. Threre are 40+ adapters which makes the
 	//   cost of new per-adapter metrics rather expensive. Thought should be given when adding new per-adapter metrics.
-	assert.True(t, perAdapterCardinalityCount <= 22, "Per-Adapter Cardinality")
+	assert.True(t, perAdapterCardinalityCount <= 27, "Per-Adapter Cardinality count equals %d \n", perAdapterCardinalityCount)
 }
 
 func TestConnectionMetrics(t *testing.T) {
@@ -944,6 +945,175 @@ func TestTimeoutNotifications(t *testing.T) {
 
 }
 
+func TestRecordDNSTime(t *testing.T) {
+	type testIn struct {
+		dnsLookupDuration time.Duration
+	}
+	type testOut struct {
+		expDuration float64
+		expCount    uint64
+	}
+	testCases := []struct {
+		description string
+		in          testIn
+		out         testOut
+	}{
+		{
+			description: "Five second DNS lookup time",
+			in: testIn{
+				dnsLookupDuration: time.Second * 5,
+			},
+			out: testOut{
+				expDuration: 5,
+				expCount:    1,
+			},
+		},
+		{
+			description: "Zero DNS lookup time",
+			in:          testIn{},
+			out: testOut{
+				expDuration: 0,
+				expCount:    1,
+			},
+		},
+	}
+	for i, test := range testCases {
+		pm := createMetricsForTesting()
+		pm.RecordDNSTime(test.in.dnsLookupDuration)
+
+		m := dto.Metric{}
+		pm.dnsLookupTimer.Write(&m)
+		histogram := *m.GetHistogram()
+
+		assert.Equal(t, test.out.expCount, histogram.GetSampleCount(), "[%d] Incorrect number of histogram entries. Desc: %s\n", i, test.description)
+		assert.Equal(t, test.out.expDuration, histogram.GetSampleSum(), "[%d] Incorrect number of histogram cumulative values. Desc: %s\n", i, test.description)
+	}
+}
+
+func TestRecordAdapterConnections(t *testing.T) {
+
+	type testIn struct {
+		adapterName   openrtb_ext.BidderName
+		connWasReused bool
+		connWait      time.Duration
+	}
+
+	type testOut struct {
+		expectedConnReusedCount  int64
+		expectedConnCreatedCount int64
+		expectedConnWaitCount    uint64
+		expectedConnWaitTime     float64
+	}
+
+	testCases := []struct {
+		description string
+		in          testIn
+		out         testOut
+	}{
+		{
+			description: "[1] Successful, new connection created, was idle, has connection wait",
+			in: testIn{
+				adapterName:   openrtb_ext.BidderAppnexus,
+				connWasReused: false,
+				connWait:      time.Second * 5,
+			},
+			out: testOut{
+				expectedConnReusedCount:  0,
+				expectedConnCreatedCount: 1,
+				expectedConnWaitCount:    1,
+				expectedConnWaitTime:     5,
+			},
+		},
+		{
+			description: "[2] Successful, new connection created, not idle, has connection wait",
+			in: testIn{
+				adapterName:   openrtb_ext.BidderAppnexus,
+				connWasReused: false,
+				connWait:      time.Second * 4,
+			},
+			out: testOut{
+				expectedConnReusedCount:  0,
+				expectedConnCreatedCount: 1,
+				expectedConnWaitCount:    1,
+				expectedConnWaitTime:     4,
+			},
+		},
+		{
+			description: "[3] Successful, was reused, was idle, no connection wait",
+			in: testIn{
+				adapterName:   openrtb_ext.BidderAppnexus,
+				connWasReused: true,
+			},
+			out: testOut{
+				expectedConnReusedCount:  1,
+				expectedConnCreatedCount: 0,
+				expectedConnWaitCount:    1,
+				expectedConnWaitTime:     0,
+			},
+		},
+		{
+			description: "[4] Successful, was reused, not idle, has connection wait",
+			in: testIn{
+				adapterName:   openrtb_ext.BidderAppnexus,
+				connWasReused: true,
+				connWait:      time.Second * 5,
+			},
+			out: testOut{
+				expectedConnReusedCount:  1,
+				expectedConnCreatedCount: 0,
+				expectedConnWaitCount:    1,
+				expectedConnWaitTime:     5,
+			},
+		},
+	}
+
+	for i, test := range testCases {
+		m := createMetricsForTesting()
+		assertDesciptions := []string{
+			fmt.Sprintf("[%d] Metric: adapterReusedConnections; Desc: %s", i+1, test.description),
+			fmt.Sprintf("[%d] Metric: adapterCreatedConnections; Desc: %s", i+1, test.description),
+			fmt.Sprintf("[%d] Metric: adapterWaitConnectionCount; Desc: %s", i+1, test.description),
+			fmt.Sprintf("[%d] Metric: adapterWaitConnectionTime; Desc: %s", i+1, test.description),
+		}
+
+		m.RecordAdapterConnections(test.in.adapterName, test.in.connWasReused, test.in.connWait)
+
+		// Assert number of reused connections
+		assertCounterVecValue(t,
+			assertDesciptions[0],
+			"adapter_connection_reused",
+			m.adapterReusedConnections,
+			float64(test.out.expectedConnReusedCount),
+			prometheus.Labels{adapterLabel: string(test.in.adapterName)})
+
+		// Assert number of new created connections
+		assertCounterVecValue(t,
+			assertDesciptions[1],
+			"adapter_connection_created",
+			m.adapterCreatedConnections,
+			float64(test.out.expectedConnCreatedCount),
+			prometheus.Labels{adapterLabel: string(test.in.adapterName)})
+
+		// Assert connection wait time
+		histogram := getHistogramFromHistogramVec(m.adapterConnectionWaitTime, adapterLabel, string(test.in.adapterName))
+		assert.Equal(t, test.out.expectedConnWaitCount, histogram.GetSampleCount(), assertDesciptions[2])
+		assert.Equal(t, test.out.expectedConnWaitTime, histogram.GetSampleSum(), assertDesciptions[3])
+	}
+}
+
+func TestDisableAdapterConnections(t *testing.T) {
+	prometheusMetrics := NewMetrics(config.PrometheusMetrics{
+		Port:      8080,
+		Namespace: "prebid",
+		Subsystem: "server",
+	}, config.DisabledMetrics{AdapterConnectionMetrics: true})
+
+	// Assert counter vector was not initialized
+	assert.Nil(t, prometheusMetrics.adapterReusedConnections, "Counter Vector adapterReusedConnections should be nil")
+	assert.Nil(t, prometheusMetrics.adapterCreatedConnections, "Counter Vector adapterCreatedConnections should be nil")
+	assert.Nil(t, prometheusMetrics.adapterConnectionWaitTime, "Counter Vector adapterConnectionWaitTime should be nil")
+}
+
 func TestRecordRequestPrivacy(t *testing.T) {
 	m := createMetricsForTesting()