Repository: prebid/prebid-server
Pull request #1319 (2020-05-28 13:20:45)

Add Adtarget server adapter

SyntaxNode: @GeneGenie Friendly ping. :)
GeneGenie: @SyntaxNode hi there :) 
will submit changes asap

SyntaxNode: > @SyntaxNode hi there :)
> will submit changes asap

Thank you. There was no need to address it immediately. Just wanted to give you a friendly reminder about the pending feedback. Looks good to me now. You just have a merge conflict to resolve.

Patch:
diff --git a/adapters/adtarget/adtarget.go b/adapters/adtarget/adtarget.go
new file mode 100644
index 00000000..77622d45
--- /dev/null
+++ b/adapters/adtarget/adtarget.go
@@ -0,0 +1,189 @@
+package adtarget
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type AdtargetAdapter struct {
+	endpoint string
+}
+
+type adtargetImpExt struct {
+	Adtarget openrtb_ext.ExtImpAdtarget `json:"adtarget"`
+}
+
+func (a *AdtargetAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+
+	totalImps := len(request.Imp)
+	errors := make([]error, 0, totalImps)
+	imp2source := make(map[int][]int)
+
+	for i := 0; i < totalImps; i++ {
+
+		sourceId, err := validateImpressionAndSetExt(&request.Imp[i])
+
+		if err != nil {
+			errors = append(errors, err)
+			continue
+		}
+
+		if _, ok := imp2source[sourceId]; !ok {
+			imp2source[sourceId] = make([]int, 0, totalImps-i)
+		}
+
+		imp2source[sourceId] = append(imp2source[sourceId], i)
+
+	}
+
+	totalReqs := len(imp2source)
+	if 0 == totalReqs {
+		return nil, errors
+	}
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+
+	reqs := make([]*adapters.RequestData, 0, totalReqs)
+
+	imps := request.Imp
+	request.Imp = make([]openrtb.Imp, 0, len(imps))
+	for sourceId, impIndexes := range imp2source {
+		request.Imp = request.Imp[:0]
+
+		for i := 0; i < len(impIndexes); i++ {
+			request.Imp = append(request.Imp, imps[impIndexes[i]])
+		}
+
+		body, err := json.Marshal(request)
+		if err != nil {
+			errors = append(errors, fmt.Errorf("error while encoding bidRequest, err: %s", err))
+			return nil, errors
+		}
+
+		reqs = append(reqs, &adapters.RequestData{
+			Method:  "POST",
+			Uri:     a.endpoint + fmt.Sprintf("?aid=%d", sourceId),
+			Body:    body,
+			Headers: headers,
+		})
+	}
+
+	return reqs, errors
+}
+
+func (a *AdtargetAdapter) MakeBids(bidReq *openrtb.BidRequest, unused *adapters.RequestData, httpRes *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+
+	if httpRes.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+	if httpRes.StatusCode == http.StatusBadRequest {
+		return nil, []error{&errortypes.BadInput{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", httpRes.StatusCode),
+		}}
+	}
+	var bidResp openrtb.BidResponse
+	if err := json.Unmarshal(httpRes.Body, &bidResp); err != nil {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: fmt.Sprintf("error while decoding response, err: %s", err),
+		}}
+	}
+
+	bidResponse := adapters.NewBidderResponse()
+	var errors []error
+
+	var impOK bool
+	for _, sb := range bidResp.SeatBid {
+		for i := 0; i < len(sb.Bid); i++ {
+
+			bid := sb.Bid[i]
+
+			impOK = false
+			mediaType := openrtb_ext.BidTypeBanner
+			for _, imp := range bidReq.Imp {
+				if imp.ID == bid.ImpID {
+
+					impOK = true
+
+					if imp.Video != nil {
+						mediaType = openrtb_ext.BidTypeVideo
+						break
+					}
+				}
+			}
+
+			if !impOK {
+				errors = append(errors, &errortypes.BadServerResponse{
+					Message: fmt.Sprintf("ignoring bid id=%s, request doesn't contain any impression with id=%s", bid.ID, bid.ImpID),
+				})
+				continue
+			}
+
+			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+				Bid:     &bid,
+				BidType: mediaType,
+			})
+		}
+	}
+
+	return bidResponse, errors
+}
+
+func validateImpressionAndSetExt(imp *openrtb.Imp) (int, error) {
+
+	if imp.Banner == nil && imp.Video == nil {
+		return 0, &errortypes.BadInput{
+			Message: fmt.Sprintf("ignoring imp id=%s, Adtarget supports only Video and Banner", imp.ID),
+		}
+	}
+
+	if 0 == len(imp.Ext) {
+		return 0, &errortypes.BadInput{
+			Message: fmt.Sprintf("ignoring imp id=%s, extImpBidder is empty", imp.ID),
+		}
+	}
+
+	var bidderExt adapters.ExtImpBidder
+
+	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
+		return 0, &errortypes.BadInput{
+			Message: fmt.Sprintf("ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err),
+		}
+	}
+
+	impExt := openrtb_ext.ExtImpAdtarget{}
+	err := json.Unmarshal(bidderExt.Bidder, &impExt)
+	if err != nil {
+		return 0, &errortypes.BadInput{
+			Message: fmt.Sprintf("ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err),
+		}
+	}
+
+	// common extension for all impressions
+	var impExtBuffer []byte
+
+	impExtBuffer, err = json.Marshal(&adtargetImpExt{
+		Adtarget: impExt,
+	})
+
+	if impExt.BidFloor > 0 {
+		imp.BidFloor = impExt.BidFloor
+	}
+
+	imp.Ext = impExtBuffer
+
+	return impExt.SourceId, nil
+}
+
+func NewAdtargetBidder(endpoint string) *AdtargetAdapter {
+	return &AdtargetAdapter{
+		endpoint: endpoint,
+	}
+}
diff --git a/adapters/adtarget/usersync.go b/adapters/adtarget/usersync.go
new file mode 100644
index 00000000..20bced25
--- /dev/null
+++ b/adapters/adtarget/usersync.go
@@ -0,0 +1,12 @@
+package adtarget
+
+import (
+	"text/template"
+
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/usersync"
+)
+
+func NewAdtargetSyncer(temp *template.Template) usersync.Usersyncer {
+	return adapters.NewSyncer("adtarget", 0, temp, adapters.SyncTypeRedirect)
+}
diff --git a/config/config.go b/config/config.go
index 4e54bc71..07384f9d 100755
--- a/config/config.go
+++ b/config/config.go
@@ -66,6 +66,8 @@ type Configuration struct {
 	PemCertsFile string `mapstructure:"certificates_file"`
 	// Custom headers to handle request timeouts from queueing infrastructure
 	RequestTimeoutHeaders RequestTimeoutHeaders `mapstructure:"request_timeout_headers"`
+	// Debug/logging flags go here
+	Debug Debug `mapstructure:"debug"`
 }
 
 const MIN_COOKIE_SIZE_BYTES = 500
@@ -104,6 +106,7 @@ func (cfg *Configuration) validate() configErrors {
 	errs = cfg.GDPR.validate(errs)
 	errs = cfg.CurrencyConverter.validate(errs)
 	errs = validateAdapters(cfg.Adapters, errs)
+	errs = cfg.Debug.validate(errs)
 	return errs
 }
 
@@ -450,6 +453,30 @@ type DefReqFiles struct {
 	FileName string `mapstructure:"name"`
 }
 
+type Debug struct {
+	TimeoutNotification TimeoutNotification `mapstructure:"timeout_notification"`
+}
+
+func (cfg *Debug) validate(errs configErrors) configErrors {
+	return cfg.TimeoutNotification.validate(errs)
+}
+
+type TimeoutNotification struct {
+	// Log timeout notifications in the application log
+	Log bool `mapstructure:"log"`
+	// Fraction of notifications to log
+	SamplingRate float32 `mapstructure:"sampling_rate"`
+	// Only log failures
+	FailOnly bool `mapstructure:"fail_only"`
+}
+
+func (cfg *TimeoutNotification) validate(errs configErrors) configErrors {
+	if cfg.SamplingRate < 0.0 || cfg.SamplingRate > 1.0 {
+		errs = append(errs, fmt.Errorf("debug.timeout_notification.sampling_rate must be positive and not greater than 1.0. Got %f", cfg.SamplingRate))
+	}
+	return errs
+}
+
 // New uses viper to get our server configurations.
 func New(v *viper.Viper) (*Configuration, error) {
 	var c Configuration
@@ -521,6 +548,7 @@ func (cfg *Configuration) setDerivedDefaults() {
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdkernel, "https://sync.adkernel.com/user-sync?t=image&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadkernel%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7BUID%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdkernelAdn, "https://tag.adkernel.com/syncr?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3DadkernelAdn%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7BUID%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdpone, "https://usersync.adpone.com/csync?redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadpone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdtarget, "https://sync.console.adtarget.com.tr/csync?t=p&ep=0&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadtarget%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdtelligent, "https://sync.adtelligent.com/csync?t=p&ep=0&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadtelligent%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdmixer, "https://inv-nets.admixer.net/adxcm.aspx?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=1&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadmixer%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%24visitor_cookie%24%24")
 	// openrtb_ext.BidderAdOcean doesn't have a good default.
@@ -725,6 +753,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.adocean.endpoint", "https://{{.Host}}")
 	v.SetDefault("adapters.adoppler.endpoint", "http://app.trustedmarketplace.io/ads")
 	v.SetDefault("adapters.adpone.endpoint", "http://rtb.adpone.com/bid-request?src=prebid_server")
+	v.SetDefault("adapters.adtarget.endpoint", "http://ghb.console.adtarget.com.tr/pbs/ortb")
 	v.SetDefault("adapters.adtelligent.endpoint", "http://ghb.adtelligent.com/pbs/ortb")
 	v.SetDefault("adapters.advangelists.endpoint", "http://nep.advangelists.com/xp/get?pubid={{.PublisherID}}")
 	v.SetDefault("adapters.aja.endpoint", "https://ad.as.amanad.adtdp.com/v1/bid/4")
@@ -820,6 +849,10 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("request_timeout_headers.request_time_in_queue", "")
 	v.SetDefault("request_timeout_headers.request_timeout_in_queue", "")
 
+	v.SetDefault("debug.timeout_notification.log", false)
+	v.SetDefault("debug.timeout_notification.sampling_rate", 0.0)
+	v.SetDefault("debug.timeout_notification.fail_only", false)
+
 	// Set environment variable support:
 	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
 	v.SetEnvPrefix("PBS")
diff --git a/config/util/loggers.go b/config/util/loggers.go
new file mode 100644
index 00000000..88702e68
--- /dev/null
+++ b/config/util/loggers.go
@@ -0,0 +1,24 @@
+package util
+
+import (
+	"math/rand"
+)
+
+type logMsg func(string, ...interface{})
+
+type randomGenerator func() float32
+
+// LogRandomSample will log a randam sample of the messages it is sent, based on the chance to log
+// chance = 1.0 => always log,
+// chance = 0.0 => never log
+func LogRandomSample(msg string, logger logMsg, chance float32) {
+	logRandomSampleImpl(msg, logger, chance, rand.Float32)
+}
+
+func logRandomSampleImpl(msg string, logger logMsg, chance float32, randGenerator randomGenerator) {
+	if chance < 1.0 && randGenerator() > chance {
+		// this is the chance we don't log anything
+		return
+	}
+	logger(msg)
+}
diff --git a/docs/bidders/adtarget.md b/docs/bidders/adtarget.md
new file mode 100644
index 00000000..b658a728
--- /dev/null
+++ b/docs/bidders/adtarget.md
@@ -0,0 +1,5 @@
+# Adtarget bidder
+
+To use the Adtarget bidder you will need an aid from an exchange account on [https://console.adtarget.com.tr](adtarget.com.tr).
+
+For further information, please contact kamil@adtarget.com.tr
\ No newline at end of file
diff --git a/docs/developers/add-new-bidder.md b/docs/developers/add-new-bidder.md
index e68185fd..d76a1fd2 100644
--- a/docs/developers/add-new-bidder.md
+++ b/docs/developers/add-new-bidder.md
@@ -46,6 +46,16 @@ If bidder is going to support long form video make sure bidder has:
 Note: `bid.bidVideo.PrimaryCategory` or `TypedBid.bid.Cat` should be specified.
 To learn more about IAB categories, please refer to this convenience link (not the final official definition): [IAB categories](https://adtagmacros.com/list-of-iab-categories-for-advertisement/)
 
+### Timeout notification support
+This is an optional feature. If you wish to get timeout notifications when a bid request from PBS times out, you can implement the
+`MakeTimeoutNotification` method in your adapter. If you do not wish timeout notification, do not implement the method.
+
+`func (a *Adapter) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error)`
+
+Here the `RequestData` supplied as an argument is the request returned from `MakeRequests` that timed out. If an adapter generates
+multiple requests, and more than one of them times out, then there will be a call to `MakeTimeoutNotification` for each failed
+request. The function should then return a `RequestData` object that will be the timeout notification to be sent to the bidder, or a list of errors encountered trying to create the timeout notification request. Timeout notifications will not generate subsequent timeout notifications if they timeout or fail.
+
 ## Test Your Bidder
 
 ### Automated Tests
diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go
index 39001611..2ea8f7fb 100755
--- a/exchange/adapter_map.go
+++ b/exchange/adapter_map.go
@@ -5,6 +5,8 @@ import (
 	"net/http"
 	"strings"
 
+	"github.com/prebid/prebid-server/pbsmetrics"
+
 	"github.com/prebid/prebid-server/adapters"
 	ttx "github.com/prebid/prebid-server/adapters/33across"
 	"github.com/prebid/prebid-server/adapters/adform"
@@ -16,6 +18,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/adocean"
 	"github.com/prebid/prebid-server/adapters/adoppler"
 	"github.com/prebid/prebid-server/adapters/adpone"
+	"github.com/prebid/prebid-server/adapters/adtarget"
 	"github.com/prebid/prebid-server/adapters/adtelligent"
 	"github.com/prebid/prebid-server/adapters/advangelists"
 	"github.com/prebid/prebid-server/adapters/aja"
@@ -85,7 +88,7 @@ import (
 // The newAdapterMap function is segregated to its own file to make it a simple and clean location for each Adapter
 // to register itself. No wading through Exchange code to find it.
 
-func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapters.BidderInfos) map[openrtb_ext.BidderName]adaptedBidder {
+func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapters.BidderInfos, me pbsmetrics.MetricsEngine) map[openrtb_ext.BidderName]adaptedBidder {
 	ortbBidders := map[openrtb_ext.BidderName]adapters.Bidder{
 		openrtb_ext.Bidder33Across:     ttx.New33AcrossBidder(cfg.Adapters[string(openrtb_ext.Bidder33Across)].Endpoint),
 		openrtb_ext.BidderAdform:       adform.NewAdformBidder(client, cfg.Adapters[string(openrtb_ext.BidderAdform)].Endpoint),
@@ -97,6 +100,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 		openrtb_ext.BidderAdOcean:      adocean.NewAdOceanBidder(client, cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdOcean))].Endpoint),
 		openrtb_ext.BidderAdoppler:     adoppler.NewAdopplerBidder(cfg.Adapters[string(openrtb_ext.BidderAdoppler)].Endpoint),
 		openrtb_ext.BidderAdpone:       adpone.NewAdponeBidder(cfg.Adapters[string(openrtb_ext.BidderAdpone)].Endpoint),
+		openrtb_ext.BidderAdtarget:     adtarget.NewAdtargetBidder(cfg.Adapters[string(openrtb_ext.BidderAdtarget)].Endpoint),
 		openrtb_ext.BidderAdtelligent:  adtelligent.NewAdtelligentBidder(cfg.Adapters[string(openrtb_ext.BidderAdtelligent)].Endpoint),
 		openrtb_ext.BidderAdvangelists: advangelists.NewAdvangelistsBidder(cfg.Adapters[string(openrtb_ext.BidderAdvangelists)].Endpoint),
 		openrtb_ext.BidderAJA:          aja.NewAJABidder(cfg.Adapters[string(openrtb_ext.BidderAJA)].Endpoint),
@@ -190,7 +194,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)
+			allBidders[name] = adaptBidder(adapters.EnforceBidderInfo(bidder, infos[string(name)]), client, cfg, me)
 		}
 	}
 
diff --git a/exchange/bidder.go b/exchange/bidder.go
index 7a53db5e..f9b4a522 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -10,13 +10,18 @@ import (
 	"net/http"
 	"time"
 
+	"github.com/golang/glog"
+	"github.com/prebid/prebid-server/config/util"
+
 	"github.com/mxmCherry/openrtb"
 	nativeRequests "github.com/mxmCherry/openrtb/native/request"
 	nativeResponse "github.com/mxmCherry/openrtb/native/response"
 	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/currencies"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/pbsmetrics"
 	"golang.org/x/net/context/ctxhttp"
 )
 
@@ -82,16 +87,20 @@ 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) adaptedBidder {
+func adaptBidder(bidder adapters.Bidder, client *http.Client, cfg *config.Configuration, me pbsmetrics.MetricsEngine) adaptedBidder {
 	return &bidderAdapter{
-		Bidder: bidder,
-		Client: client,
+		Bidder:      bidder,
+		Client:      client,
+		DebugConfig: cfg.Debug,
+		me:          me,
 	}
 }
 
 type bidderAdapter struct {
-	Bidder adapters.Bidder
-	Client *http.Client
+	Bidder      adapters.Bidder
+	Client      *http.Client
+	DebugConfig config.Debug
+	me          pbsmetrics.MetricsEngine
 }
 
 func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo) (*pbsOrtbSeatBid, []error) {
@@ -365,8 +374,21 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou
 		httpReq, err := http.NewRequest(toReq.Method, toReq.Uri, bytes.NewBuffer(toReq.Body))
 		if err == nil {
 			httpReq.Header = req.Headers
-			ctxhttp.Do(ctx, bidder.Client, httpReq)
-			// No validation yet on sending notifications
+			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) {
+				var msg string
+				if err == nil {
+					msg = fmt.Sprintf("TimeoutNotification: status:(%d) body:%s", httpResp.StatusCode, string(toReq.Body))
+				} else {
+					msg = fmt.Sprintf("TimeoutNotification: error:(%s) body:%s", err.Error(), string(toReq.Body))
+				}
+				// If logging is turned on, and logging is not disallowed via FailOnly
+				util.LogRandomSample(msg, glog.Warningf, bidder.DebugConfig.TimeoutNotification.SamplingRate)
+			}
+		} else {
+			bidder.me.RecordTimeoutNotice(false)
 		}
 	}
 
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 6d51b87d..660beb64 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -69,7 +69,7 @@ type bidResponseWrapper struct {
 func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *config.Configuration, metricsEngine pbsmetrics.MetricsEngine, infos adapters.BidderInfos, gDPR gdpr.Permissions, currencyConverter *currencies.RateConverter) Exchange {
 	e := new(exchange)
 
-	e.adapterMap = newAdapterMap(client, cfg, infos)
+	e.adapterMap = newAdapterMap(client, cfg, infos, metricsEngine)
 	e.cache = cache
 	e.cacheTime = time.Duration(cfg.CacheURL.ExpectedTimeMillis) * time.Millisecond
 	e.me = metricsEngine
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index b3ecddb0..659c6616 100755
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -33,6 +33,7 @@ const (
 	BidderAdpone           BidderName = "adpone"
 	BidderAdmixer          BidderName = "admixer"
 	BidderAdOcean          BidderName = "adocean"
+	BidderAdtarget         BidderName = "adtarget"
 	BidderAdtelligent      BidderName = "adtelligent"
 	BidderAdvangelists     BidderName = "advangelists"
 	BidderAJA              BidderName = "aja"
@@ -110,6 +111,7 @@ var BidderMap = map[string]BidderName{
 	"admixer":           BidderAdmixer,
 	"adocean":           BidderAdOcean,
 	"adpone":            BidderAdpone,
+	"adtarget":          BidderAdtarget,
 	"adtelligent":       BidderAdtelligent,
 	"advangelists":      BidderAdvangelists,
 	"aja":               BidderAJA,
diff --git a/openrtb_ext/imp_adtarget.go b/openrtb_ext/imp_adtarget.go
new file mode 100644
index 00000000..a8ac70a1
--- /dev/null
+++ b/openrtb_ext/imp_adtarget.go
@@ -0,0 +1,9 @@
+package openrtb_ext
+
+// ExtImpAdtarget defines the contract for bidrequest.imp[i].ext.adtarget
+type ExtImpAdtarget struct {
+	SourceId    int     `json:"aid"`
+	PlacementId int     `json:"placementId,omitempty"`
+	SiteId      int     `json:"siteId,omitempty"`
+	BidFloor    float64 `json:"bidFloor,omitempty"`
+}
diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go
index e1cdaceb..4e249785 100644
--- a/pbsmetrics/config/metrics.go
+++ b/pbsmetrics/config/metrics.go
@@ -188,6 +188,13 @@ func (me *MultiMetricsEngine) RecordRequestQueueTime(success bool, requestType p
 	}
 }
 
+// RecordTimeoutNotice across all engines
+func (me *MultiMetricsEngine) RecordTimeoutNotice(success bool) {
+	for _, thisME := range *me {
+		thisME.RecordTimeoutNotice(success)
+	}
+}
+
 // DummyMetricsEngine is a Noop metrics engine in case no metrics are configured. (may also be useful for tests)
 type DummyMetricsEngine struct{}
 
@@ -262,3 +269,7 @@ func (me *DummyMetricsEngine) RecordPrebidCacheRequestTime(success bool, length
 // RecordRequestQueueTime as a noop
 func (me *DummyMetricsEngine) RecordRequestQueueTime(success bool, requestType pbsmetrics.RequestType, length time.Duration) {
 }
+
+// RecordTimeoutNotice as a noop
+func (me *DummyMetricsEngine) RecordTimeoutNotice(success bool) {
+}
diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go
index ff3d9681..1ced4d57 100644
--- a/pbsmetrics/go_metrics.go
+++ b/pbsmetrics/go_metrics.go
@@ -48,6 +48,9 @@ type Metrics struct {
 	ImpsTypeAudio  metrics.Meter
 	ImpsTypeNative metrics.Meter
 
+	TimeoutNotificationSuccess metrics.Meter
+	TimeoutNotificationFailure metrics.Meter
+
 	AdapterMetrics map[openrtb_ext.BidderName]*AdapterMetrics
 	// Don't export accountMetrics because we need helper functions here to insure its properly populated dynamically
 	accountMetrics        map[string]*accountMetrics
@@ -131,6 +134,9 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 		ImpsTypeAudio:  blankMeter,
 		ImpsTypeNative: blankMeter,
 
+		TimeoutNotificationSuccess: blankMeter,
+		TimeoutNotificationFailure: blankMeter,
+
 		AdapterMetrics:  make(map[openrtb_ext.BidderName]*AdapterMetrics, len(exchanges)),
 		accountMetrics:  make(map[string]*accountMetrics),
 		MetricsDisabled: disableMetrics,
@@ -209,6 +215,9 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d
 
 	newMetrics.userSyncSet[unknownBidder] = metrics.GetOrRegisterMeter("usersync.unknown.sets", registry)
 	newMetrics.userSyncGDPRPrevent[unknownBidder] = metrics.GetOrRegisterMeter("usersync.unknown.gdpr_prevent", registry)
+
+	newMetrics.TimeoutNotificationSuccess = metrics.GetOrRegisterMeter("timeout_notification.ok", registry)
+	newMetrics.TimeoutNotificationFailure = metrics.GetOrRegisterMeter("timeout_notification.failed", registry)
 	return newMetrics
 }
 
@@ -544,6 +553,15 @@ func (me *Metrics) RecordRequestQueueTime(success bool, requestType RequestType,
 
 }
 
+func (me *Metrics) RecordTimeoutNotice(success bool) {
+	if success {
+		me.TimeoutNotificationSuccess.Mark(1)
+	} else {
+		me.TimeoutNotificationFailure.Mark(1)
+	}
+	return
+}
+
 func doMark(bidder openrtb_ext.BidderName, meters map[openrtb_ext.BidderName]metrics.Meter) {
 	met, ok := meters[bidder]
 	if ok {
diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go
index 611692c9..e65ba313 100644
--- a/pbsmetrics/metrics.go
+++ b/pbsmetrics/metrics.go
@@ -275,4 +275,5 @@ type MetricsEngine interface {
 	RecordStoredImpCacheResult(cacheResult CacheResult, inc int)
 	RecordPrebidCacheRequestTime(success bool, length time.Duration)
 	RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration)
+	RecordTimeoutNotice(sucess bool)
 }
diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go
index 1f5b84b1..482cbf24 100644
--- a/pbsmetrics/metrics_mock.go
+++ b/pbsmetrics/metrics_mock.go
@@ -101,3 +101,8 @@ func (me *MetricsEngineMock) RecordPrebidCacheRequestTime(success bool, length t
 func (me *MetricsEngineMock) RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration) {
 	me.Called(success, requestType, length)
 }
+
+// RecordTimeoutNotice mock
+func (me *MetricsEngineMock) RecordTimeoutNotice(success bool) {
+	me.Called(success)
+}
diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go
index d66defea..e385b044 100644
--- a/pbsmetrics/prometheus/prometheus.go
+++ b/pbsmetrics/prometheus/prometheus.go
@@ -28,6 +28,7 @@ type Metrics struct {
 	requestsWithoutCookie        *prometheus.CounterVec
 	storedImpressionsCacheResult *prometheus.CounterVec
 	storedRequestCacheResult     *prometheus.CounterVec
+	timeout_notifications        *prometheus.CounterVec
 
 	// Adapter Metrics
 	adapterBids          *prometheus.CounterVec
@@ -79,6 +80,11 @@ const (
 	requestRejectLabel  = "requestRejectedLabel"
 )
 
+const (
+	requestSuccessful = "ok"
+	requestFailed     = "failed"
+)
+
 // 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}
@@ -147,6 +153,11 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
 		"Count of stored request cache requests attempts by hits or miss.",
 		[]string{cacheResultLabel})
 
+	metrics.timeout_notifications = newCounter(cfg, metrics.Registry,
+		"timeout_notification",
+		"Count of timeout notifications triggered, and if they were successfully sent.",
+		[]string{successLabel})
+
 	metrics.adapterBids = newCounter(cfg, metrics.Registry,
 		"adapter_bids",
 		"Count of bids labeled by adapter and markup delivery type (adm or nurl).",
@@ -398,3 +409,15 @@ func (m *Metrics) RecordRequestQueueTime(success bool, requestType pbsmetrics.Re
 		requestStatusLabel: successLabelFormatted,
 	}).Observe(length.Seconds())
 }
+
+func (m *Metrics) RecordTimeoutNotice(success bool) {
+	if success {
+		m.timeout_notifications.With(prometheus.Labels{
+			successLabel: requestSuccessful,
+		}).Inc()
+	} else {
+		m.timeout_notifications.With(prometheus.Labels{
+			successLabel: requestFailed,
+		}).Inc()
+	}
+}
diff --git a/static/bidder-info/adtarget.yaml b/static/bidder-info/adtarget.yaml
new file mode 100644
index 00000000..d52f18ac
--- /dev/null
+++ b/static/bidder-info/adtarget.yaml
@@ -0,0 +1,11 @@
+maintainer:
+  email: "kamil@adtarget.com.tr"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+  site:
+    mediaTypes:
+      - banner
+      - video
diff --git a/static/bidder-params/adtarget.json b/static/bidder-params/adtarget.json
new file mode 100644
index 00000000..195bf2dd
--- /dev/null
+++ b/static/bidder-params/adtarget.json
@@ -0,0 +1,26 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Adtarget Adapter Params",
+  "description": "A schema which validates params accepted by the Adtarget adapter",
+
+  "type": "object",
+  "properties": {
+    "placementId": {
+      "type": "integer",
+      "description": "An ID which identifies this placement of the impression"
+    },
+    "siteId": {
+      "type": "integer",
+      "description": "An ID which identifies the site selling the impression"
+    },
+    "aid": {
+      "type": "integer",
+      "description": "An ID which identifies the channel"
+    },
+    "bidFloor": {
+      "type": "number",
+      "description": "BidFloor, US Dollars"
+    }
+  },
+  "required": ["aid"]
+}
diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go
index 5dccf855..751d2aab 100755
--- a/usersync/usersyncers/syncer.go
+++ b/usersync/usersyncers/syncer.go
@@ -12,6 +12,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/admixer"
 	"github.com/prebid/prebid-server/adapters/adocean"
 	"github.com/prebid/prebid-server/adapters/adpone"
+	"github.com/prebid/prebid-server/adapters/adtarget"
 	"github.com/prebid/prebid-server/adapters/adtelligent"
 	"github.com/prebid/prebid-server/adapters/advangelists"
 	"github.com/prebid/prebid-server/adapters/aja"
@@ -84,6 +85,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdmixer, admixer.NewAdmixerSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdOcean, adocean.NewAdOceanSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdpone, adpone.NewadponeSyncer)
+	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdtarget, adtarget.NewAdtargetSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdtelligent, adtelligent.NewAdtelligentSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdvangelists, advangelists.NewAdvangelistsSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAJA, aja.NewAJASyncer)


Test patch:
diff --git a/adapters/adtarget/adtarget_test.go b/adapters/adtarget/adtarget_test.go
new file mode 100644
index 00000000..93732988
--- /dev/null
+++ b/adapters/adtarget/adtarget_test.go
@@ -0,0 +1,11 @@
+package adtarget
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+)
+
+func TestJsonSamples(t *testing.T) {
+	adapterstest.RunJSONBidderTest(t, "adtargettest", NewAdtargetBidder("http://ghb.console.adtarget.com.tr/pbs/ortb"))
+}
diff --git a/adapters/adtarget/adtargettest/exemplary/media-type-mapping.json b/adapters/adtarget/adtargettest/exemplary/media-type-mapping.json
new file mode 100644
index 00000000..518268d4
--- /dev/null
+++ b/adapters/adtarget/adtargettest/exemplary/media-type-mapping.json
@@ -0,0 +1,88 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "aid": 1000
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "adtarget": {
+                  "aid": 1000
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "test-bid-id",
+                  "impid": "test-imp-id",
+                  "price": 3.5,
+                  "w": 900,
+                  "h": 250
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "test-bid-id",
+            "impid": "test-imp-id",
+            "price": 3.5,
+            "w": 900,
+            "h": 250
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/adtarget/adtargettest/exemplary/simple-banner.json b/adapters/adtarget/adtargettest/exemplary/simple-banner.json
new file mode 100644
index 00000000..b63739bd
--- /dev/null
+++ b/adapters/adtarget/adtargettest/exemplary/simple-banner.json
@@ -0,0 +1,62 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "aid": 1000,
+            "siteId": 1234,
+            "bidFloor": 20
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "banner": {
+                "format": [
+                  {"w":300,"h":250},
+                  {"w":300,"h":600}
+                ]
+              },
+              "bidfloor": 20,
+              "ext": {
+                "adtarget": {
+                  "aid": 1000,
+                  "siteId": 1234,
+                  "bidFloor": 20
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 204
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/adtarget/adtargettest/exemplary/simple-video.json b/adapters/adtarget/adtargettest/exemplary/simple-video.json
new file mode 100644
index 00000000..4dc4547d
--- /dev/null
+++ b/adapters/adtarget/adtargettest/exemplary/simple-video.json
@@ -0,0 +1,55 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "aid": 1000
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "adtarget": {
+                  "aid": 1000
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 204
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/adtarget/adtargettest/params/race/banner.json b/adapters/adtarget/adtargettest/params/race/banner.json
new file mode 100644
index 00000000..1d6658c7
--- /dev/null
+++ b/adapters/adtarget/adtargettest/params/race/banner.json
@@ -0,0 +1,3 @@
+{
+  "aid": 350975
+}
diff --git a/adapters/adtarget/adtargettest/params/race/video.json b/adapters/adtarget/adtargettest/params/race/video.json
new file mode 100644
index 00000000..fe4207ef
--- /dev/null
+++ b/adapters/adtarget/adtargettest/params/race/video.json
@@ -0,0 +1,3 @@
+{
+  "aid": 331133
+}
diff --git a/adapters/adtarget/adtargettest/supplemental/audio.json b/adapters/adtarget/adtargettest/supplemental/audio.json
new file mode 100644
index 00000000..e2148e9d
--- /dev/null
+++ b/adapters/adtarget/adtargettest/supplemental/audio.json
@@ -0,0 +1,25 @@
+{
+  "mockBidRequest": {
+    "id": "unsupported-audio-request",
+    "imp": [
+      {
+        "id": "unsupported-audio-imp",
+        "audio": {
+          "mimes": ["video/mp4"]
+        },
+        "ext": {
+          "bidder": {
+            "placementId": 1
+          }
+        }
+      }
+    ]
+  },
+
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "ignoring imp id=unsupported-audio-imp, Adtarget supports only Video and Banner",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/adtarget/adtargettest/supplemental/explicit-dimensions.json b/adapters/adtarget/adtargettest/supplemental/explicit-dimensions.json
new file mode 100644
index 00000000..a4e48746
--- /dev/null
+++ b/adapters/adtarget/adtargettest/supplemental/explicit-dimensions.json
@@ -0,0 +1,58 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ],
+          "w": 100,
+          "h": 400
+        },
+        "ext": {
+          "bidder": {
+            "aid": 1000
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ],
+                "w": 100,
+                "h": 400
+              },
+              "ext": {
+                "adtarget": {
+                 "aid": 1000
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 204
+      }
+    }
+  ]
+}
diff --git a/adapters/adtarget/adtargettest/supplemental/native.json b/adapters/adtarget/adtargettest/supplemental/native.json
new file mode 100644
index 00000000..3d9aa663
--- /dev/null
+++ b/adapters/adtarget/adtargettest/supplemental/native.json
@@ -0,0 +1,25 @@
+{
+  "mockBidRequest": {
+    "id": "unsupported-native-request",
+    "imp": [
+      {
+        "id": "unsupported-native-imp",
+        "native": {
+          "ver": "1.1"
+        },
+        "ext": {
+          "bidder": {
+            "placementId": 1
+          }
+        }
+      }
+    ]
+  },
+
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "ignoring imp id=unsupported-native-imp, Adtarget supports only Video and Banner",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/adtarget/adtargettest/supplemental/wrong-impression-ext.json b/adapters/adtarget/adtargettest/supplemental/wrong-impression-ext.json
new file mode 100644
index 00000000..1986dfaf
--- /dev/null
+++ b/adapters/adtarget/adtargettest/supplemental/wrong-impression-ext.json
@@ -0,0 +1,26 @@
+{
+  "mockBidRequest": {
+    "id": "unsupported-native-request",
+    "imp": [
+      {
+        "id": "unsupported-native-imp",
+        "video": {
+         "w": 100,
+          "h": 200
+        },
+        "ext": {
+          "bidder": {
+            "aid": "some string instead of int"
+          }
+        }
+      }
+    ]
+  },
+
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "ignoring imp id=unsupported-native-imp, error while decoding impExt, err: json: cannot unmarshal string into Go struct field ExtImpAdtarget.aid of type int",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json b/adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json
new file mode 100644
index 00000000..0dffdb2b
--- /dev/null
+++ b/adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json
@@ -0,0 +1,77 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "aid": 1000
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "adtarget": {
+                  "aid": 1000
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "test-bid-id",
+                  "impid": "SOME-WRONG-IMP-ID",
+                  "price": 3.5,
+                  "w": 900,
+                  "h": 250
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "ignoring bid id=test-bid-id, request doesn't contain any impression with id=SOME-WRONG-IMP-ID",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/adtarget/params_test.go b/adapters/adtarget/params_test.go
new file mode 100644
index 00000000..b128d11c
--- /dev/null
+++ b/adapters/adtarget/params_test.go
@@ -0,0 +1,60 @@
+package adtarget
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// This file actually intends to test static/bidder-params/adtarget.json
+// These also validate the format of the external API: request.imp[i].ext.adtarget
+// TestValidParams makes sure that the adtarget schema accepts all imp.ext fields which we intend to support.
+
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, validParam := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderAdtarget, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected adtarget params: %s", validParam)
+		}
+	}
+}
+
+// TestInvalidParams makes sure that the adtarget schema rejects all the imp.ext fields we don't support.
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, invalidParam := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderAdtarget, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"aid":123}`,
+	`{"aid":123,"placementId":1234}`,
+	`{"aid":123,"siteId":4321}`,
+	`{"aid":123,"siteId":0,"bidFloor":0}`,
+}
+
+var invalidParams = []string{
+	``,
+	`null`,
+	`true`,
+	`5`,
+	`4.2`,
+	`[]`,
+	`{}`,
+	`{"aid":"123"}`,
+	`{"aid":"0"}`,
+	`{"aid":"123","placementId":"123"}`,
+	`{"aid":123, "placementId":"123", "siteId":"321"}`,
+}
diff --git a/adapters/adtarget/usersync_test.go b/adapters/adtarget/usersync_test.go
new file mode 100644
index 00000000..3ab2ed5b
--- /dev/null
+++ b/adapters/adtarget/usersync_test.go
@@ -0,0 +1,37 @@
+package adtarget
+
+import (
+	"fmt"
+	"github.com/prebid/prebid-server/privacy/ccpa"
+	"testing"
+	"text/template"
+
+	"github.com/prebid/prebid-server/privacy"
+	"github.com/prebid/prebid-server/privacy/gdpr"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAdtargetSyncer(t *testing.T) {
+	syncURL := "//sync.console.adtarget.com.tr/csync?t=p&ep=0&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=localhost%2Fsetuid%3Fbidder%3Dadtarget%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D"
+	fmt.Println("adtarget sync")
+	syncURLTemplate := template.Must(
+		template.New("sync-template").Parse(syncURL),
+	)
+
+	syncer := NewAdtargetSyncer(syncURLTemplate)
+	syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{
+		GDPR: gdpr.Policy{
+			Signal:  "0",
+			Consent: "123",
+		},
+		CCPA: ccpa.Policy{
+			Value: "1-YY",
+		},
+	})
+
+	assert.NoError(t, err)
+	assert.Equal(t, "//sync.console.adtarget.com.tr/csync?t=p&ep=0&gdpr=0&gdpr_consent=123&us_privacy=1-YY&redir=localhost%2Fsetuid%3Fbidder%3Dadtarget%26gdpr%3D0%26gdpr_consent%3D123%26uid%3D%7Buid%7D", syncInfo.URL)
+	assert.Equal(t, "redirect", syncInfo.Type)
+	assert.EqualValues(t, 0, syncer.GDPRVendorID())
+	assert.Equal(t, false, syncInfo.SupportCORS)
+}
diff --git a/config/config_test.go b/config/config_test.go
index 92794d79..ee8e68e7 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -426,6 +426,14 @@ func TestCookieSizeError(t *testing.T) {
 	}
 }
 
+func TestValidateDebug(t *testing.T) {
+	cfg := newDefaultConfig(t)
+	cfg.Debug.TimeoutNotification.SamplingRate = 1.1
+
+	err := cfg.validate()
+	assert.NotNil(t, err, "cfg.debug.timeout_notification.sampling_rate should not be allowed to be greater than 1.0, but it was allowed")
+}
+
 func newDefaultConfig(t *testing.T) *Configuration {
 	v := viper.New()
 	SetupViper(v, "")
diff --git a/config/util/loggers_test.go b/config/util/loggers_test.go
new file mode 100644
index 00000000..4bfab967
--- /dev/null
+++ b/config/util/loggers_test.go
@@ -0,0 +1,32 @@
+package util
+
+import (
+	"bytes"
+	"fmt"
+	"math/rand"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLogRandomSample(t *testing.T) {
+
+	const expected string = `This is test line 2
+This is test line 3
+`
+
+	myRand := rand.New(rand.NewSource(1337))
+	var buf bytes.Buffer
+
+	mylogger := func(msg string, args ...interface{}) {
+		buf.WriteString(fmt.Sprintf(fmt.Sprintln(msg), args...))
+	}
+
+	logRandomSampleImpl("This is test line 1", mylogger, 0.5, myRand.Float32)
+	logRandomSampleImpl("This is test line 2", mylogger, 0.5, myRand.Float32)
+	logRandomSampleImpl("This is test line 3", mylogger, 0.5, myRand.Float32)
+	logRandomSampleImpl("This is test line 4", mylogger, 0.5, myRand.Float32)
+	logRandomSampleImpl("This is test line 5", mylogger, 0.5, myRand.Float32)
+
+	assert.EqualValues(t, expected, buf.String())
+}
diff --git a/exchange/adapter_map_test.go b/exchange/adapter_map_test.go
index a732f357..f472ab1d 100644
--- a/exchange/adapter_map_test.go
+++ b/exchange/adapter_map_test.go
@@ -7,11 +7,12 @@ import (
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	metricsConfig "github.com/prebid/prebid-server/pbsmetrics/config"
 )
 
 func TestNewAdapterMap(t *testing.T) {
 	cfg := &config.Configuration{Adapters: blankAdapterConfig(openrtb_ext.BidderList())}
-	adapterMap := newAdapterMap(nil, cfg, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()))
+	adapterMap := newAdapterMap(nil, cfg, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), &metricsConfig.DummyMetricsEngine{})
 	for _, bidderName := range openrtb_ext.BidderMap {
 		if bidder, ok := adapterMap[bidderName]; bidder == nil || !ok {
 			t.Errorf("adapterMap missing expected Bidder: %s", string(bidderName))
@@ -38,7 +39,7 @@ func TestNewAdapterMapDisabledAdapters(t *testing.T) {
 			}
 		}
 	}
-	adapterMap := newAdapterMap(nil, &config.Configuration{Adapters: cfgAdapters}, adapters.ParseBidderInfos(cfgAdapters, "../static/bidder-info", bidderList))
+	adapterMap := newAdapterMap(nil, &config.Configuration{Adapters: cfgAdapters}, adapters.ParseBidderInfos(cfgAdapters, "../static/bidder-info", bidderList), &metricsConfig.DummyMetricsEngine{})
 	for _, bidderName := range openrtb_ext.BidderMap {
 		if bidder, ok := adapterMap[bidderName]; bidder == nil || !ok {
 			if inList(bidderList, bidderName) {
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index f20b431c..fa04e6a4 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -12,8 +12,10 @@ import (
 
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/currencies"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	metricsConfig "github.com/prebid/prebid-server/pbsmetrics/config"
 	"github.com/stretchr/testify/assert"
 
 	nativeRequests "github.com/mxmCherry/openrtb/native/request"
@@ -64,7 +66,7 @@ func TestSingleBidder(t *testing.T) {
 		},
 		bidResponse: mockBidderResponse,
 	}
-	bidder := adaptBidder(bidderImpl, server.Client())
+	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 	currencyConverter := currencies.NewRateConverterDefault()
 	seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{})
 
@@ -152,7 +154,7 @@ func TestMultiBidder(t *testing.T) {
 			}},
 		bidResponse: mockBidderResponse,
 	}
-	bidder := adaptBidder(bidderImpl, server.Client())
+	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 	currencyConverter := currencies.NewRateConverterDefault()
 	seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{})
 
@@ -510,7 +512,7 @@ func TestMultiCurrencies(t *testing.T) {
 		)
 
 		// Execute:
-		bidder := adaptBidder(bidderImpl, server.Client())
+		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 		currencyConverter := currencies.NewRateConverter(
 			&http.Client{},
 			mockedHTTPServer.URL,
@@ -658,7 +660,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) {
 		}
 
 		// Execute:
-		bidder := adaptBidder(bidderImpl, server.Client())
+		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 		currencyConverter := currencies.NewRateConverterDefault()
 		seatBid, errs := bidder.requestBid(
 			context.Background(),
@@ -824,7 +826,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) {
 		}
 
 		// Execute:
-		bidder := adaptBidder(bidderImpl, server.Client())
+		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 		currencyConverter := currencies.NewRateConverter(
 			&http.Client{},
 			mockedHTTPServer.URL,
@@ -939,7 +941,7 @@ func TestServerCallDebugging(t *testing.T) {
 			Headers: http.Header{},
 		},
 	}
-	bidder := adaptBidder(bidderImpl, server.Client())
+	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 	currencyConverter := currencies.NewRateConverterDefault()
 
 	bids, _ := bidder.requestBid(
@@ -1051,7 +1053,7 @@ func TestMobileNativeTypes(t *testing.T) {
 			},
 			bidResponse: tc.mockBidderResponse,
 		}
-		bidder := adaptBidder(bidderImpl, server.Client())
+		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 		currencyConverter := currencies.NewRateConverterDefault()
 
 		seatBids, _ := bidder.requestBid(
@@ -1072,7 +1074,7 @@ func TestMobileNativeTypes(t *testing.T) {
 }
 
 func TestErrorReporting(t *testing.T) {
-	bidder := adaptBidder(&bidRejector{}, nil)
+	bidder := adaptBidder(&bidRejector{}, nil, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 	currencyConverter := currencies.NewRateConverterDefault()
 	bids, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{})
 	if bids != nil {
diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go
index f8630968..72de1d42 100644
--- a/exchange/targeting_test.go
+++ b/exchange/targeting_test.go
@@ -8,12 +8,14 @@ import (
 	"testing"
 	"time"
 
+	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/currencies"
 
 	"github.com/prebid/prebid-server/gdpr"
 
 	"github.com/prebid/prebid-server/pbsmetrics"
 	metricsConf "github.com/prebid/prebid-server/pbsmetrics/config"
+	metricsConfig "github.com/prebid/prebid-server/pbsmetrics/config"
 
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/adapters"
@@ -132,7 +134,7 @@ func buildAdapterMap(bids map[openrtb_ext.BidderName][]*openrtb.Bid, mockServerU
 		adapterMap[bidder] = adaptBidder(&mockTargetingBidder{
 			mockServerURL: mockServerURL,
 			bids:          bids,
-		}, client)
+		}, client, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 	}
 	return adapterMap
 }
diff --git a/pbsmetrics/go_metrics_test.go b/pbsmetrics/go_metrics_test.go
index 253ff69e..25f75e77 100644
--- a/pbsmetrics/go_metrics_test.go
+++ b/pbsmetrics/go_metrics_test.go
@@ -53,6 +53,9 @@ func TestNewMetrics(t *testing.T) {
 
 	ensureContains(t, registry, "queued_requests.video.rejected", m.RequestsQueueTimer[ReqTypeVideo][false])
 	ensureContains(t, registry, "queued_requests.video.accepted", m.RequestsQueueTimer[ReqTypeVideo][true])
+
+	ensureContains(t, registry, "timeout_notification.ok", m.TimeoutNotificationSuccess)
+	ensureContains(t, registry, "timeout_notification.failed", m.TimeoutNotificationFailure)
 }
 
 func TestRecordBidType(t *testing.T) {
diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go
index e4d6a4f7..24c50492 100644
--- a/pbsmetrics/prometheus/prometheus_test.go
+++ b/pbsmetrics/prometheus/prometheus_test.go
@@ -923,6 +923,27 @@ func TestRecordRequestQueueTimeMetric(t *testing.T) {
 	}
 }
 
+func TestTimeoutNotifications(t *testing.T) {
+	m := createMetricsForTesting()
+
+	m.RecordTimeoutNotice(true)
+	m.RecordTimeoutNotice(true)
+	m.RecordTimeoutNotice(false)
+
+	assertCounterVecValue(t, "", "timeout_notifications:ok", m.timeout_notifications,
+		float64(2),
+		prometheus.Labels{
+			successLabel: requestSuccessful,
+		})
+
+	assertCounterVecValue(t, "", "timeout_notifications:fail", m.timeout_notifications,
+		float64(1),
+		prometheus.Labels{
+			successLabel: requestFailed,
+		})
+
+}
+
 func assertCounterValue(t *testing.T, description, name string, counter prometheus.Counter, expected float64) {
 	m := dto.Metric{}
 	counter.Write(&m)
diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go
index ddd067e8..c9ef382f 100755
--- a/usersync/usersyncers/syncer_test.go
+++ b/usersync/usersyncers/syncer_test.go
@@ -21,6 +21,7 @@ func TestNewSyncerMap(t *testing.T) {
 			string(openrtb_ext.BidderAdmixer):          syncConfig,
 			string(openrtb_ext.BidderAdOcean):          syncConfig,
 			string(openrtb_ext.BidderAdpone):           syncConfig,
+			string(openrtb_ext.BidderAdtarget):         syncConfig,
 			string(openrtb_ext.BidderAdtelligent):      syncConfig,
 			string(openrtb_ext.BidderAdvangelists):     syncConfig,
 			string(openrtb_ext.BidderAJA):              syncConfig,