Repository: prebid/prebid-server
Pull request #2193 (2022-03-21 13:55:34)

YahooSSP: Added video support.


Patch:
diff --git a/adapters/adkernelAdn/adkernelAdn.go b/adapters/adkernelAdn/adkernelAdn.go
index 9ece01db..2c29e69a 100644
--- a/adapters/adkernelAdn/adkernelAdn.go
+++ b/adapters/adkernelAdn/adkernelAdn.go
@@ -15,8 +15,6 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
-const defaultDomain string = "tag.adkernel.com"
-
 type adkernelAdnAdapter struct {
 	EndpointTemplate *template.Template
 }
@@ -103,7 +101,6 @@ func dispatchImpressions(imps []openrtb2.Imp, impsExt []openrtb_ext.ExtImpAdkern
 			res[impExt] = make([]openrtb2.Imp, 0)
 		}
 		res[impExt] = append(res[impExt], imp)
-
 	}
 	return res, errors
 }
@@ -212,12 +209,8 @@ func createBidRequest(prebidBidRequest *openrtb2.BidRequest, params *openrtb_ext
 
 // Builds enpoint url based on adapter-specific pub settings from imp.ext
 func (adapter *adkernelAdnAdapter) buildEndpointURL(params *openrtb_ext.ExtImpAdkernelAdn) (string, error) {
-	reqHost := defaultDomain
-	if params.Host != "" {
-		reqHost = params.Host
-	}
 	pubIDStr := strconv.Itoa(params.PublisherID)
-	endpointParams := macros.EndpointTemplateParams{Host: reqHost, PublisherID: pubIDStr}
+	endpointParams := macros.EndpointTemplateParams{PublisherID: pubIDStr}
 	return macros.ResolveMacros(adapter.EndpointTemplate, endpointParams)
 }
 
diff --git a/adapters/adyoulike/adyoulike.go b/adapters/adyoulike/adyoulike.go
index 17c42e4a..8537c54d 100644
--- a/adapters/adyoulike/adyoulike.go
+++ b/adapters/adyoulike/adyoulike.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/buger/jsonparser"
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
@@ -36,6 +37,23 @@ func (a *adapter) MakeRequests(
 	reqCopy := *openRTBRequest
 	reqCopy.Imp = []openrtb2.Imp{}
 	for ind, imp := range openRTBRequest.Imp {
+
+		// Check if imp comes with bid floor amount defined in a foreign currency
+		if imp.BidFloor > 0 && imp.BidFloorCur != "" && strings.ToUpper(imp.BidFloorCur) != "USD" {
+			// Convert to US dollars
+			convertedValue, err := reqInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, "USD")
+			if err != nil {
+				return nil, []error{err}
+			}
+			// Update after conversion. All imp elements inside request.Imp are shallow copies
+			// therefore, their non-pointer values are not shared memory and are safe to modify.
+			imp.BidFloorCur = "USD"
+			imp.BidFloor = convertedValue
+		}
+
+		// Set the CUR of bid to USD after converting all floors
+		reqCopy.Cur = []string{"USD"}
+
 		reqCopy.Imp = append(reqCopy.Imp, imp)
 
 		tagID, err = jsonparser.GetString(reqCopy.Imp[ind].Ext, "bidder", "placement")
@@ -106,7 +124,8 @@ func (a *adapter) MakeBids(
 	}
 
 	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(openRTBRequest.Imp))
-	bidResponse.Currency = openRTBBidderResponse.Cur
+	bidResponse.Currency = "USD"
+
 	for _, seatBid := range openRTBBidderResponse.SeatBid {
 		for idx := range seatBid.Bid {
 			b := &adapters.TypedBid{
diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go
index f855f917..79f07200 100644
--- a/adapters/pubmatic/pubmatic.go
+++ b/adapters/pubmatic/pubmatic.go
@@ -271,6 +271,14 @@ func parseImpressionObject(imp *openrtb2.Imp, extractWrapperExtFromImp, extractP
 		imp.Banner = bannerCopy
 	}
 
+	if pubmaticExt.Kadfloor != "" {
+		bidfloor, err := strconv.ParseFloat(strings.TrimSpace(pubmaticExt.Kadfloor), 64)
+		if err == nil {
+			//do not overwrite existing value if kadfloor is invalid
+			imp.BidFloor = bidfloor
+		}
+	}
+
 	extMap := make(map[string]interface{}, 0)
 	if pubmaticExt.Keywords != nil && len(pubmaticExt.Keywords) != 0 {
 		addKeywordsToExt(pubmaticExt.Keywords, extMap)
diff --git a/adapters/yahoossp/yahoossp.go b/adapters/yahoossp/yahoossp.go
index 652d57f9..f0cc23bf 100644
--- a/adapters/yahoossp/yahoossp.go
+++ b/adapters/yahoossp/yahoossp.go
@@ -117,14 +117,15 @@ func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest
 				}}
 			}
 
-			if openrtb_ext.BidTypeBanner != mediaTypeId {
-				//only banner is supported, anything else is ignored
+			if openrtb_ext.BidTypeBanner != mediaTypeId &&
+				openrtb_ext.BidTypeVideo != mediaTypeId {
+				//only banner and video are mediaTypeId, anything else is ignored
 				continue
 			}
 
 			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
 				Bid:     &bid,
-				BidType: openrtb_ext.BidTypeBanner,
+				BidType: mediaTypeId,
 			})
 		}
 	}
@@ -140,6 +141,8 @@ func getImpInfo(impId string, imps []openrtb2.Imp) (bool, openrtb_ext.BidType) {
 			exists = true
 			if imp.Banner != nil {
 				mediaType = openrtb_ext.BidTypeBanner
+			} else if imp.Video != nil {
+				mediaType = openrtb_ext.BidTypeVideo
 			}
 			break
 		}
@@ -156,13 +159,20 @@ func changeRequestForBidService(request *openrtb2.BidRequest, extension *openrtb
 		request.App.ID = extension.Dcn
 	}
 
-	if request.Imp[0].Banner == nil {
-		return nil
+	if request.Imp[0].Banner != nil {
+		banner := *request.Imp[0].Banner
+		request.Imp[0].Banner = &banner
+
+		err := validateBanner(&banner)
+		if err != nil {
+			return err
+		}
 	}
 
-	banner := *request.Imp[0].Banner
-	request.Imp[0].Banner = &banner
+	return nil
+}
 
+func validateBanner(banner *openrtb2.Banner) error {
 	if banner.W != nil && banner.H != nil {
 		if *banner.W == 0 || *banner.H == 0 {
 			return fmt.Errorf("Invalid sizes provided for Banner %dx%d", *banner.W, *banner.H)
diff --git a/amp/parse.go b/amp/parse.go
index 05e43aca..1ce19a76 100644
--- a/amp/parse.go
+++ b/amp/parse.go
@@ -9,7 +9,7 @@ import (
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
 )
 
-// Params defines the paramters of an AMP request.
+// Params defines the parameters of an AMP request.
 type Params struct {
 	Account         string
 	CanonicalURL    string
@@ -31,7 +31,7 @@ type Size struct {
 	Width          int64
 }
 
-// ParseParams parses the AMP paramters from a HTTP request.
+// ParseParams parses the AMP parameters from a HTTP request.
 func ParseParams(httpRequest *http.Request) (Params, error) {
 	query := httpRequest.URL.Query()
 
@@ -104,7 +104,7 @@ func chooseConsent(consent, gdprConsent string) string {
 		return consent
 	}
 
-	// Fallback to 'gdpr_consent' for compatability until it's no longer used. This was our original
+	// Fallback to 'gdpr_consent' for compatibility until it's no longer used. This was our original
 	// implementation before the same AMP macro was reused for CCPA.
 	return gdprConsent
 }
diff --git a/analytics/event.go b/analytics/event.go
index 56b75769..0b0e65f0 100644
--- a/analytics/event.go
+++ b/analytics/event.go
@@ -5,8 +5,9 @@ type EventType string
 
 // Possible values of events Prebid Server can receive for an ad.
 const (
-	Win EventType = "win"
-	Imp EventType = "imp"
+	Win  EventType = "win"
+	Imp  EventType = "imp"
+	Vast EventType = "vast"
 )
 
 // ResponseFormat enumerates the values of a Prebid Server event.
@@ -19,6 +20,18 @@ const (
 	Image ResponseFormat = "i"
 )
 
+// VastType enumerates the values of vast type events Prebid Server can receive
+type VastType string
+
+// Possible value of VastType event prebid server can receive.
+const (
+	Start         VastType = "start"
+	FirstQuartile VastType = "firstQuartile"
+	MidPoint      VastType = "midPoint"
+	ThirdQuartile VastType = "thirdQuartile"
+	Complete      VastType = "complete"
+)
+
 // Analytics indicates if the notification event should be handled or not
 type Analytics string
 
@@ -36,4 +49,5 @@ type EventRequest struct {
 	Bidder      string         `json:"bidder,omitempty"`
 	Timestamp   int64          `json:"timestamp,omitempty"`
 	Integration string         `json:"integration,omitempty"`
+	VType       VastType       `json:"vtype,omitempty"`
 }
diff --git a/config/accounts.go b/config/accounts.go
index 0e6c4114..ae9d656e 100644
--- a/config/accounts.go
+++ b/config/accounts.go
@@ -13,15 +13,24 @@ const (
 
 // Account represents a publisher account configuration
 type Account struct {
-	ID                 string      `mapstructure:"id" json:"id"`
-	Disabled           bool        `mapstructure:"disabled" json:"disabled"`
-	CacheTTL           DefaultTTLs `mapstructure:"cache_ttl" json:"cache_ttl"`
-	EventsEnabled      bool        `mapstructure:"events_enabled" json:"events_enabled"`
-	CCPA               AccountCCPA `mapstructure:"ccpa" json:"ccpa"`
-	GDPR               AccountGDPR `mapstructure:"gdpr" json:"gdpr"`
-	DebugAllow         bool        `mapstructure:"debug_allow" json:"debug_allow"`
-	DefaultIntegration string      `mapstructure:"default_integration" json:"default_integration"`
-	Events             Events      `mapstructure:"events" json:"events"` // Don't enable this feature. It is still under developmment - https://github.com/prebid/prebid-server/issues/1725
+	ID                      string      `mapstructure:"id" json:"id"`
+	Disabled                bool        `mapstructure:"disabled" json:"disabled"`
+	CacheTTL                DefaultTTLs `mapstructure:"cache_ttl" json:"cache_ttl"`
+	EventsEnabled           bool        `mapstructure:"events_enabled" json:"events_enabled"`
+	CCPA                    AccountCCPA `mapstructure:"ccpa" json:"ccpa"`
+	GDPR                    AccountGDPR `mapstructure:"gdpr" json:"gdpr"`
+	DebugAllow              bool        `mapstructure:"debug_allow" json:"debug_allow"`
+	DefaultIntegration      string      `mapstructure:"default_integration" json:"default_integration"`
+	CookieSync              CookieSync  `mapstructure:"cookie_sync" json:"cookie_sync"`
+	Events                  Events      `mapstructure:"events" json:"events"` // Don't enable this feature. It is still under developmment - https://github.com/prebid/prebid-server/issues/1725
+	TruncateTargetAttribute *int        `mapstructure:"truncate_target_attr" json:"truncate_target_attr"`
+}
+
+// CookieSync represents the account-level defaults for the cookie sync endpoint.
+type CookieSync struct {
+	DefaultLimit    int  `mapstructure:"default_limit" json:"default_limit"`
+	MaxLimit        int  `mapstructure:"max_limit" json:"max_limit"`
+	DefaultCoopSync bool `mapstructure:"default_coop_sync" json:"default_coop_sync"`
 }
 
 // AccountCCPA represents account-specific CCPA configuration
diff --git a/config/config.go b/config/config.go
index 57c9b5a6..9324acce 100644
--- a/config/config.go
+++ b/config/config.go
@@ -819,7 +819,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.adgeneration.endpoint", "https://d.socdm.com/adsv/v1")
 	v.SetDefault("adapters.adhese.endpoint", "https://ads-{{.AccountID}}.adhese.com/json")
 	v.SetDefault("adapters.adkernel.endpoint", "https://pbs.adksrv.com/hb?zone={{.ZoneID}}")
-	v.SetDefault("adapters.adkerneladn.endpoint", "http://{{.Host}}/rtbpub?account={{.PublisherID}}")
+	v.SetDefault("adapters.adkerneladn.endpoint", "https://pbs2.adksrv.com/rtbpub?account={{.PublisherID}}")
 	v.SetDefault("adapters.adman.endpoint", "http://pub.admanmedia.com/?c=o&m=ortb")
 	v.SetDefault("adapters.admixer.endpoint", "http://inv-nets.admixer.net/pbs.aspx")
 	v.SetDefault("adapters.adocean.endpoint", "https://{{.Host}}")
diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index 079348be..12adc8ad 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -8,17 +8,21 @@ import (
 	"io/ioutil"
 	"net/http"
 	"strconv"
+	"strings"
 
 	"github.com/golang/glog"
 	"github.com/julienschmidt/httprouter"
+	accountService "github.com/prebid/prebid-server/account"
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/privacy"
 	"github.com/prebid/prebid-server/privacy/ccpa"
 	gdprPrivacy "github.com/prebid/prebid-server/privacy/gdpr"
+	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/usersync"
 )
 
@@ -28,6 +32,8 @@ var (
 	errCookieSyncGDPRConsentMissing                = errors.New("gdpr_consent is required if gdpr=1")
 	errCookieSyncGDPRConsentMissingSignalAmbiguous = errors.New("gdpr_consent is required. gdpr is not specified and is assumed to be 1 by the server. set gdpr=0 to exempt this request")
 	errCookieSyncInvalidBiddersType                = errors.New("invalid bidders type. must either be a string '*' or a string array of bidders")
+	errCookieSyncAccountBlocked                    = errors.New("account is disabled, please reach out to the prebid server host")
+	errCookieSyncAccountInvalid                    = errors.New("account must be valid if provided, please reach out to the prebid server host")
 )
 
 var cookieSyncBidderFilterAllowAll = usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude)
@@ -38,6 +44,7 @@ func NewCookieSyncEndpoint(
 	gdprPermissions gdpr.Permissions,
 	metrics metrics.MetricsEngine,
 	pbsAnalytics analytics.PBSAnalyticsModule,
+	accountsFetcher stored_requests.AccountFetcher,
 	bidders map[string]openrtb_ext.BidderName) HTTPRouterHandler {
 
 	bidderHashSet := make(map[string]struct{}, len(bidders))
@@ -46,38 +53,38 @@ func NewCookieSyncEndpoint(
 	}
 
 	return &cookieSyncEndpoint{
-		chooser:          usersync.NewChooser(syncersByBidder),
-		config:           config.UserSync,
-		hostCookieConfig: &config.HostCookie,
+		chooser: usersync.NewChooser(syncersByBidder),
+		config:  config,
 		privacyConfig: usersyncPrivacyConfig{
 			gdprConfig:      config.GDPR,
 			gdprPermissions: gdprPermissions,
 			ccpaEnforce:     config.CCPA.Enforce,
 			bidderHashSet:   bidderHashSet,
 		},
-		metrics:      metrics,
-		pbsAnalytics: pbsAnalytics,
+		metrics:         metrics,
+		pbsAnalytics:    pbsAnalytics,
+		accountsFetcher: accountsFetcher,
 	}
 }
 
 type cookieSyncEndpoint struct {
-	chooser          usersync.Chooser
-	config           config.UserSync
-	hostCookieConfig *config.HostCookie
-	privacyConfig    usersyncPrivacyConfig
-	metrics          metrics.MetricsEngine
-	pbsAnalytics     analytics.PBSAnalyticsModule
+	chooser         usersync.Chooser
+	config          *config.Configuration
+	privacyConfig   usersyncPrivacyConfig
+	metrics         metrics.MetricsEngine
+	pbsAnalytics    analytics.PBSAnalyticsModule
+	accountsFetcher stored_requests.AccountFetcher
 }
 
 func (c *cookieSyncEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 	request, privacyPolicies, err := c.parseRequest(r)
 	if err != nil {
-		c.metrics.RecordCookieSync(metrics.CookieSyncBadRequest)
+		c.writeParseRequestErrorMetrics(err)
 		c.handleError(w, err, http.StatusBadRequest)
 		return
 	}
 
-	cookie := usersync.ParseCookieFromRequest(r, c.hostCookieConfig)
+	cookie := usersync.ParseCookieFromRequest(r, &c.config.HostCookie)
 
 	result := c.chooser.Choose(request, cookie)
 	switch result.Status {
@@ -125,6 +132,11 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
 		}
 	}
 
+	request, err = c.setLimit(request)
+	if err != nil {
+		return usersync.Request{}, privacy.Policies{}, err
+	}
+
 	privacyPolicies := privacy.Policies{
 		GDPR: gdprPrivacy.Policy{
 			Signal:  gdprString,
@@ -154,8 +166,8 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
 	rx := usersync.Request{
 		Bidders: request.Bidders,
 		Cooperative: usersync.Cooperative{
-			Enabled:        (request.CooperativeSync != nil && *request.CooperativeSync) || (request.CooperativeSync == nil && c.config.Cooperative.EnabledByDefault),
-			PriorityGroups: c.config.Cooperative.PriorityGroups,
+			Enabled:        (request.CooperativeSync != nil && *request.CooperativeSync) || (request.CooperativeSync == nil && c.config.UserSync.Cooperative.EnabledByDefault),
+			PriorityGroups: c.config.UserSync.Cooperative.PriorityGroups,
 		},
 		Limit: request.Limit,
 		Privacy: usersyncPrivacy{
@@ -169,6 +181,39 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
 	return rx, privacyPolicies, nil
 }
 
+func (c *cookieSyncEndpoint) writeParseRequestErrorMetrics(err error) {
+	switch err {
+	case errCookieSyncAccountBlocked:
+		c.metrics.RecordCookieSync(metrics.CookieSyncAccountBlocked)
+	case errCookieSyncAccountInvalid:
+		c.metrics.RecordCookieSync(metrics.CookieSyncAccountInvalid)
+	default:
+		c.metrics.RecordCookieSync(metrics.CookieSyncBadRequest)
+	}
+}
+
+func (c *cookieSyncEndpoint) setLimit(request cookieSyncRequest) (cookieSyncRequest, error) {
+	if request.Account != "" {
+		accountInfo, errs := accountService.GetAccount(context.Background(), c.config, c.accountsFetcher, request.Account)
+		if len(errs) > 0 {
+			return request, combineErrors(errs)
+		}
+		if request.Limit <= 0 {
+			request.Limit = accountInfo.CookieSync.DefaultLimit
+		}
+		if request.Limit <= 0 || request.Limit > accountInfo.CookieSync.MaxLimit {
+			request.Limit = accountInfo.CookieSync.MaxLimit
+		}
+		if request.Limit < 0 {
+			request.Limit = 0
+		}
+		if request.CooperativeSync == nil {
+			request.CooperativeSync = &accountInfo.CookieSync.DefaultCoopSync
+		}
+	}
+	return request, nil
+}
+
 func parseTypeFilter(request *cookieSyncRequestFilterSettings) (usersync.SyncTypeFilter, error) {
 	syncTypeFilter := usersync.SyncTypeFilter{
 		IFrame:   cookieSyncBidderFilterAllowAll,
@@ -237,6 +282,23 @@ func (c *cookieSyncEndpoint) handleError(w http.ResponseWriter, err error, httpS
 	})
 }
 
+func combineErrors(errs []error) error {
+	var errorStrings []string
+	for _, err := range errs {
+		// preserve knowledge of special account errors
+		switch errortypes.ReadCode(err) {
+		case errortypes.BlacklistedAcctErrorCode:
+			return errCookieSyncAccountBlocked
+		case errortypes.AcctRequiredErrorCode:
+			return errCookieSyncAccountInvalid
+		}
+
+		errorStrings = append(errorStrings, err.Error())
+	}
+	combinedErrors := strings.Join(errorStrings, " ")
+	return errors.New(combinedErrors)
+}
+
 func (c *cookieSyncEndpoint) writeBidderMetrics(biddersEvaluated []usersync.BidderEvaluation) {
 	for _, bidder := range biddersEvaluated {
 		switch bidder.Status {
@@ -319,6 +381,7 @@ type cookieSyncRequest struct {
 	Limit           int                              `json:"limit"`
 	CooperativeSync *bool                            `json:"coopSync"`
 	FilterSettings  *cookieSyncRequestFilterSettings `json:"filterSettings"`
+	Account         string                           `json:"account"`
 }
 
 type cookieSyncRequestFilterSettings struct {
diff --git a/endpoints/events/event.go b/endpoints/events/event.go
index fd47683a..8c9c252a 100644
--- a/endpoints/events/event.go
+++ b/endpoints/events/event.go
@@ -23,6 +23,7 @@ const (
 	// Required
 	TemplateUrl        = "%v/event?t=%v&b=%v&a=%v"
 	TypeParameter      = "t"
+	VTypeParameter     = "vtype"
 	BidIdParameter     = "b"
 	AccountIdParameter = "a"
 
@@ -144,6 +145,16 @@ func ParseEventRequest(r *http.Request) (*analytics.EventRequest, []error) {
 		errs = append(errs, err)
 	}
 
+	if event.Type == analytics.Vast {
+		if err := readVType(event, r); err != nil {
+			errs = append(errs, err)
+		}
+	} else {
+		if t := r.URL.Query().Get(VTypeParameter); t != "" {
+			errs = append(errs, &errortypes.BadInput{Message: "parameter 'vtype' is only required for t=vast"})
+		}
+	}
+
 	// validate bidid
 	if bidid, err := checkRequiredParameter(r, BidIdParameter); err != nil {
 		errs = append(errs, err)
@@ -261,11 +272,40 @@ func readType(er *analytics.EventRequest, httpRequest *http.Request) error {
 	case string(analytics.Win):
 		er.Type = analytics.Win
 		return nil
+	case string(analytics.Vast):
+		er.Type = analytics.Vast
+		return nil
 	default:
 		return &errortypes.BadInput{Message: fmt.Sprintf("unknown type: '%s'", t)}
 	}
 }
 
+// readVType validates analytics.EventRequest vtype
+func readVType(er *analytics.EventRequest, httpRequest *http.Request) error {
+	vtype, err := checkRequiredParameter(httpRequest, VTypeParameter)
+
+	if err != nil {
+		return err
+	}
+
+	switch vtype {
+	case string(analytics.Start):
+		er.VType = analytics.Start
+	case string(analytics.FirstQuartile):
+		er.VType = analytics.FirstQuartile
+	case string(analytics.MidPoint):
+		er.VType = analytics.MidPoint
+	case string(analytics.ThirdQuartile):
+		er.VType = analytics.ThirdQuartile
+	case string(analytics.Complete):
+		er.VType = analytics.Complete
+	default:
+		return &errortypes.BadInput{Message: fmt.Sprintf("unknown vtype: '%s'", vtype)}
+	}
+
+	return nil
+}
+
 // readFormat validates analytics.EventRequest format attribute
 func readFormat(er *analytics.EventRequest, httpRequest *http.Request) error {
 	f := httpRequest.URL.Query().Get(FormatParameter)
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 074f6739..42caa3a7 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -940,7 +940,7 @@ func validateNativeContextTypes(cType native1.ContextType, cSubtype native1.Cont
 
 func validateNativePlacementType(pt native1.PlacementType, impIndex int) error {
 	if pt == 0 {
-		// Placement Type is only reccomended, not required.
+		// Placement Type is only recommended, not required.
 		return nil
 	}
 	if pt < native1.PlacementTypeFeed || (pt > native1.PlacementTypeRecommendationWidget && pt < openrtb_ext.NativeExchangeSpecificLowerBound) {
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 08fd3117..80820ac1 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -158,7 +158,7 @@ type AuctionRequest struct {
 	GlobalPrivacyControlHeader string
 	ImpExtInfoMap              map[string]ImpExtInfo
 
-	// LegacyLabels is included here for temporary compatability with cleanOpenRTBRequests
+	// LegacyLabels is included here for temporary compatibility with cleanOpenRTBRequests
 	// in HoldAuction until we get to factoring it away. Do not use for anything new.
 	LegacyLabels   metrics.Labels
 	FirstPartyData map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData
@@ -287,7 +287,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 				errs = append(errs, cacheErrs...)
 			}
 
-			targData.setTargeting(auc, r.BidRequest.App != nil, bidCategory)
+			targData.setTargeting(auc, r.BidRequest.App != nil, bidCategory, r.Account.TruncateTargetAttribute)
 
 		}
 		bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, r, responseDebugAllow, errs)
diff --git a/exchange/targeting.go b/exchange/targeting.go
index f00e43d2..00008d29 100644
--- a/exchange/targeting.go
+++ b/exchange/targeting.go
@@ -35,7 +35,7 @@ type targetData struct {
 // The one exception is the `hb_cache_id` key. Since our APIs explicitly document cache keys to be on a "best effort" basis,
 // it's ok if those stay in the auction. For now, this method implements a very naive cache strategy.
 // In the future, we should implement a more clever retry & backoff strategy to balance the success rate & performance.
-func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMapping map[string]string) {
+func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMapping map[string]string, truncateTargetAttr *int) {
 	for impId, topBidsPerImp := range auc.winningBidsByBidder {
 		overallWinner := auc.winningBids[impId]
 		for bidderName, topBidPerBidder := range topBidsPerImp {
@@ -43,38 +43,38 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMappi
 
 			targets := make(map[string]string, 10)
 			if cpm, ok := auc.roundedPrices[topBidPerBidder]; ok {
-				targData.addKeys(targets, openrtb_ext.HbpbConstantKey, cpm, bidderName, isOverallWinner)
+				targData.addKeys(targets, openrtb_ext.HbpbConstantKey, cpm, bidderName, isOverallWinner, truncateTargetAttr)
 			}
-			targData.addKeys(targets, openrtb_ext.HbBidderConstantKey, string(bidderName), bidderName, isOverallWinner)
+			targData.addKeys(targets, openrtb_ext.HbBidderConstantKey, string(bidderName), bidderName, isOverallWinner, truncateTargetAttr)
 			if hbSize := makeHbSize(topBidPerBidder.bid); hbSize != "" {
-				targData.addKeys(targets, openrtb_ext.HbSizeConstantKey, hbSize, bidderName, isOverallWinner)
+				targData.addKeys(targets, openrtb_ext.HbSizeConstantKey, hbSize, bidderName, isOverallWinner, truncateTargetAttr)
 			}
 			if cacheID, ok := auc.cacheIds[topBidPerBidder.bid]; ok {
-				targData.addKeys(targets, openrtb_ext.HbCacheKey, cacheID, bidderName, isOverallWinner)
+				targData.addKeys(targets, openrtb_ext.HbCacheKey, cacheID, bidderName, isOverallWinner, truncateTargetAttr)
 			}
 			if vastID, ok := auc.vastCacheIds[topBidPerBidder.bid]; ok {
-				targData.addKeys(targets, openrtb_ext.HbVastCacheKey, vastID, bidderName, isOverallWinner)
+				targData.addKeys(targets, openrtb_ext.HbVastCacheKey, vastID, bidderName, isOverallWinner, truncateTargetAttr)
 			}
 			if targData.includeFormat {
-				targData.addKeys(targets, openrtb_ext.HbFormatKey, string(topBidPerBidder.bidType), bidderName, isOverallWinner)
+				targData.addKeys(targets, openrtb_ext.HbFormatKey, string(topBidPerBidder.bidType), bidderName, isOverallWinner, truncateTargetAttr)
 			}
 
 			if targData.cacheHost != "" {
-				targData.addKeys(targets, openrtb_ext.HbConstantCacheHostKey, targData.cacheHost, bidderName, isOverallWinner)
+				targData.addKeys(targets, openrtb_ext.HbConstantCacheHostKey, targData.cacheHost, bidderName, isOverallWinner, truncateTargetAttr)
 			}
 			if targData.cachePath != "" {
-				targData.addKeys(targets, openrtb_ext.HbConstantCachePathKey, targData.cachePath, bidderName, isOverallWinner)
+				targData.addKeys(targets, openrtb_ext.HbConstantCachePathKey, targData.cachePath, bidderName, isOverallWinner, truncateTargetAttr)
 			}
 
 			if deal := topBidPerBidder.bid.DealID; len(deal) > 0 {
-				targData.addKeys(targets, openrtb_ext.HbDealIDConstantKey, deal, bidderName, isOverallWinner)
+				targData.addKeys(targets, openrtb_ext.HbDealIDConstantKey, deal, bidderName, isOverallWinner, truncateTargetAttr)
 			}
 
 			if isApp {
-				targData.addKeys(targets, openrtb_ext.HbEnvKey, openrtb_ext.HbEnvKeyApp, bidderName, isOverallWinner)
+				targData.addKeys(targets, openrtb_ext.HbEnvKey, openrtb_ext.HbEnvKeyApp, bidderName, isOverallWinner, truncateTargetAttr)
 			}
 			if len(categoryMapping) > 0 {
-				targData.addKeys(targets, openrtb_ext.HbCategoryDurationKey, categoryMapping[topBidPerBidder.bid.ID], bidderName, isOverallWinner)
+				targData.addKeys(targets, openrtb_ext.HbCategoryDurationKey, categoryMapping[topBidPerBidder.bid.ID], bidderName, isOverallWinner, truncateTargetAttr)
 			}
 
 			topBidPerBidder.bidTargets = targets
@@ -82,9 +82,18 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMappi
 	}
 }
 
-func (targData *targetData) addKeys(keys map[string]string, key openrtb_ext.TargetingKey, value string, bidderName openrtb_ext.BidderName, overallWinner bool) {
+func (targData *targetData) addKeys(keys map[string]string, key openrtb_ext.TargetingKey, value string, bidderName openrtb_ext.BidderName, overallWinner bool, truncateTargetAttr *int) {
+	var maxLength int
+	if truncateTargetAttr != nil {
+		maxLength = *truncateTargetAttr
+		if maxLength < 0 {
+			maxLength = MaxKeyLength
+		}
+	} else {
+		maxLength = MaxKeyLength
+	}
 	if targData.includeBidderKeys {
-		keys[key.BidderKey(bidderName, MaxKeyLength)] = value
+		keys[key.BidderKey(bidderName, maxLength)] = value
 	}
 	if targData.includeWinners && overallWinner {
 		keys[string(key)] = value
diff --git a/firstpartydata/first_party_data.go b/firstpartydata/first_party_data.go
index d1940448..78ab5076 100644
--- a/firstpartydata/first_party_data.go
+++ b/firstpartydata/first_party_data.go
@@ -221,20 +221,26 @@ func unmarshalJSONToInt64(input json.RawMessage) (int64, error) {
 	if err != nil {
 		return -1, err
 	}
-	resNum, err := num.Int64()
-	return resNum, err
+	result, err := num.Int64()
+	return result, err
 }
 
 func unmarshalJSONToString(input json.RawMessage) (string, error) {
-	var inputString string
-	err := json.Unmarshal(input, &inputString)
-	return inputString, err
+	var result string
+	err := json.Unmarshal(input, &result)
+	return result, err
+}
+
+func unmarshalJSONToData(input json.RawMessage) ([]openrtb2.Data, error) {
+	var result []openrtb2.Data
+	err := json.Unmarshal(input, &result)
+	return result, err
 }
 
 func unmarshalJSONToStringArray(input json.RawMessage) ([]string, error) {
-	var inputString []string
-	err := json.Unmarshal(input, &inputString)
-	return inputString, err
+	var result []string
+	err := json.Unmarshal(input, &result)
+	return result, err
 }
 
 //resolveExtension inserts remaining {site/app/user} attributes back to {site/app/user}.ext.data
@@ -294,6 +300,15 @@ func mergeUsers(original *openrtb2.User, fpdConfigUser map[string]json.RawMessag
 		delete(fpdConfigUser, yobKey)
 	}
 
+	if userData, present := fpdConfigUser[dataKey]; present {
+		newUserData, err := unmarshalJSONToData(userData)
+		if err != nil {
+			return newUser, err
+		}
+		newUser.Data = append(newUser.Data, newUserData...)
+		delete(fpdConfigUser, dataKey)
+	}
+
 	if len(fpdConfigUser) > 0 {
 		newUser.Ext, err = resolveExtension(fpdConfigUser, original.Ext)
 	}
diff --git a/metrics/metrics.go b/metrics/metrics.go
index 9d16143f..b70907ea 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -300,6 +300,8 @@ const (
 	CookieSyncBadRequest            CookieSyncStatus = "bad_request"
 	CookieSyncOptOut                CookieSyncStatus = "opt_out"
 	CookieSyncGDPRHostCookieBlocked CookieSyncStatus = "gdpr_blocked_host_cookie"
+	CookieSyncAccountBlocked        CookieSyncStatus = "acct_blocked"
+	CookieSyncAccountInvalid        CookieSyncStatus = "acct_invalid"
 )
 
 // CookieSyncStatuses returns possible cookie sync statuses.
@@ -404,7 +406,7 @@ type MetricsEngine interface {
 	RecordStoredDataError(labels StoredDataLabels)
 	RecordPrebidCacheRequestTime(success bool, length time.Duration)
 	RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration)
-	RecordTimeoutNotice(sucess bool)
+	RecordTimeoutNotice(success bool)
 	RecordRequestPrivacy(privacy PrivacyLabels)
 	RecordAdapterGDPRRequestBlocked(adapterName openrtb_ext.BidderName)
 }
diff --git a/openrtb_ext/imp_adkernelAdn.go b/openrtb_ext/imp_adkernelAdn.go
index 803f3fa4..fee22318 100644
--- a/openrtb_ext/imp_adkernelAdn.go
+++ b/openrtb_ext/imp_adkernelAdn.go
@@ -2,6 +2,5 @@ package openrtb_ext
 
 // ExtImpAdkernelAdn defines the contract for bidrequest.imp[i].ext.adkernelAdn
 type ExtImpAdkernelAdn struct {
-	PublisherID int    `json:"pubId"`
-	Host        string `json:"host,omitempty"`
+	PublisherID int `json:"pubId"`
 }
diff --git a/openrtb_ext/imp_pubmatic.go b/openrtb_ext/imp_pubmatic.go
index cf5093a2..db96ca51 100644
--- a/openrtb_ext/imp_pubmatic.go
+++ b/openrtb_ext/imp_pubmatic.go
@@ -15,6 +15,7 @@ type ExtImpPubmatic struct {
 	PmZoneID    string                  `json:"pmzoneid"`
 	WrapExt     json.RawMessage         `json:"wrapper,omitempty"`
 	Keywords    []*ExtImpPubmaticKeyVal `json:"keywords,omitempty"`
+	Kadfloor    string                  `json:"kadfloor,omitempty"`
 }
 
 // ExtImpPubmaticKeyVal defines the contract for bidrequest.imp[i].ext.pubmatic.keywords[i]
diff --git a/router/router.go b/router/router.go
index e49f88b1..eb887c02 100644
--- a/router/router.go
+++ b/router/router.go
@@ -235,7 +235,7 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
 	r.GET("/info/bidders", infoEndpoints.NewBiddersEndpoint(bidderInfos, defaultAliases))
 	r.GET("/info/bidders/:bidderName", infoEndpoints.NewBiddersDetailEndpoint(bidderInfos, cfg.Adapters, defaultAliases))
 	r.GET("/bidders/params", NewJsonDirectoryServer(schemaDirectory, paramsValidator, defaultAliases))
-	r.POST("/cookie_sync", endpoints.NewCookieSyncEndpoint(syncersByBidder, cfg, gdprPerms, r.MetricsEngine, pbsAnalytics, activeBidders).Handle)
+	r.POST("/cookie_sync", endpoints.NewCookieSyncEndpoint(syncersByBidder, cfg, gdprPerms, r.MetricsEngine, pbsAnalytics, accounts, activeBidders).Handle)
 	r.GET("/status", endpoints.NewStatusEndpoint(cfg.StatusResponse))
 	r.GET("/", serveIndex)
 	r.Handler("GET", "/version", endpoints.NewVersionEndpoint(version.Ver, version.Rev))
diff --git a/static/bidder-info/yahoossp.yaml b/static/bidder-info/yahoossp.yaml
index 06d0cb18..cefa603d 100644
--- a/static/bidder-info/yahoossp.yaml
+++ b/static/bidder-info/yahoossp.yaml
@@ -5,9 +5,11 @@ capabilities:
   app:
     mediaTypes:
       - banner
+      - video
   site:
     mediaTypes:
       - banner
+      - video
 userSync:
   # yahoossp supports user syncing, but requires configuration by the host. contact this
   # bidder directly at the email address in this file to ask about enabling user sync.
diff --git a/static/bidder-params/adkernelAdn.json b/static/bidder-params/adkernelAdn.json
index 6c1afe11..84448245 100644
--- a/static/bidder-params/adkernelAdn.json
+++ b/static/bidder-params/adkernelAdn.json
@@ -9,10 +9,6 @@
         "type": "integer",
         "minimum": 1,
         "description": "Publisher Id to use."
-      },
-      "host": {
-        "type": "string",
-        "description": "Network host to send request"
       }
   },
   "required": ["pubId"]
diff --git a/static/bidder-params/pubmatic.json b/static/bidder-params/pubmatic.json
index 5d41b9fc..a13b5906 100644
--- a/static/bidder-params/pubmatic.json
+++ b/static/bidder-params/pubmatic.json
@@ -16,6 +16,10 @@
 			"type": "string",
 			"description": "Comma separated zone id. Used im deal targeting & site section targeting. e.g drama,sport"
 		},
+		"kadfloor": {
+			"type": "string",
+			"description": "bid floor value set to imp.bidfloor"
+		},
 		"dctr": {
 			"type": "string",
 			"description": "Deals Custom Targeting, pipe separated key-value pairs e.g key1=V1,V2,V3|key2=v1|key3=v3,v5"


Test patch:
diff --git a/adapters/adkernelAdn/adkernelAdn_test.go b/adapters/adkernelAdn/adkernelAdn_test.go
index 687f5a94..945a415f 100644
--- a/adapters/adkernelAdn/adkernelAdn_test.go
+++ b/adapters/adkernelAdn/adkernelAdn_test.go
@@ -11,7 +11,7 @@ import (
 
 func TestJsonSamples(t *testing.T) {
 	bidder, buildErr := Builder(openrtb_ext.BidderAdkernelAdn, config.Adapter{
-		Endpoint: "http://{{.Host}}/rtbpub?account={{.PublisherID}}"})
+		Endpoint: "https://pbs2.adksrv.com/rtbpub?account={{.PublisherID}}"})
 
 	if buildErr != nil {
 		t.Fatalf("Builder returned unexpected error %v", buildErr)
diff --git a/adapters/adkernelAdn/adkerneladntest/exemplary/multiformat-impression.json b/adapters/adkernelAdn/adkerneladntest/exemplary/multiformat-impression.json
index 71df95b7..2a6db14d 100644
--- a/adapters/adkernelAdn/adkerneladntest/exemplary/multiformat-impression.json
+++ b/adapters/adkernelAdn/adkerneladntest/exemplary/multiformat-impression.json
@@ -36,7 +36,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://tag.test.com/rtbpub?account=102",
+        "uri": "https://pbs2.adksrv.com/rtbpub?account=102",
         "body": {
           "id": "0000000000001",
           "imp": [
diff --git a/adapters/adkernelAdn/adkerneladntest/exemplary/single-banner-impression.json b/adapters/adkernelAdn/adkerneladntest/exemplary/single-banner-impression.json
index 1b5472df..6df3e0f1 100644
--- a/adapters/adkernelAdn/adkerneladntest/exemplary/single-banner-impression.json
+++ b/adapters/adkernelAdn/adkerneladntest/exemplary/single-banner-impression.json
@@ -29,7 +29,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://tag.test.com/rtbpub?account=101",
+        "uri": "https://pbs2.adksrv.com/rtbpub?account=101",
         "body": {
           "id": "0000000000001",
           "imp": [
diff --git a/adapters/adkernelAdn/adkerneladntest/exemplary/single-video-impression.json b/adapters/adkernelAdn/adkerneladntest/exemplary/single-video-impression.json
index 848d0c3d..e60aa06c 100644
--- a/adapters/adkernelAdn/adkerneladntest/exemplary/single-video-impression.json
+++ b/adapters/adkernelAdn/adkerneladntest/exemplary/single-video-impression.json
@@ -33,7 +33,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://tag.test.com/rtbpub?account=102",
+        "uri": "https://pbs2.adksrv.com/rtbpub?account=102",
         "body": {
           "id": "0000000000001",
           "imp": [
diff --git a/adapters/adkernelAdn/adkerneladntest/supplemental/204status.json b/adapters/adkernelAdn/adkerneladntest/supplemental/204status.json
index 950a6c28..ae71ff41 100644
--- a/adapters/adkernelAdn/adkerneladntest/supplemental/204status.json
+++ b/adapters/adkernelAdn/adkerneladntest/supplemental/204status.json
@@ -26,7 +26,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://tag.test.com/rtbpub?account=101",
+        "uri": "https://pbs2.adksrv.com/rtbpub?account=101",
         "body": {
           "id": "0000000000001",
           "imp": [
diff --git a/adapters/adkernelAdn/adkerneladntest/supplemental/http-err-status.json b/adapters/adkernelAdn/adkerneladntest/supplemental/http-err-status.json
index a7b3e251..8a25aae0 100644
--- a/adapters/adkernelAdn/adkerneladntest/supplemental/http-err-status.json
+++ b/adapters/adkernelAdn/adkerneladntest/supplemental/http-err-status.json
@@ -26,7 +26,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://tag.test.com/rtbpub?account=101",
+        "uri": "https://pbs2.adksrv.com/rtbpub?account=101",
         "body": {
           "id": "0000000000001",
           "imp": [
diff --git a/adapters/adkernelAdn/adkerneladntest/supplemental/two-impressions-two-seatbids.json b/adapters/adkernelAdn/adkerneladntest/supplemental/two-impressions-two-seatbids.json
index 59296dc6..37a5a36d 100644
--- a/adapters/adkernelAdn/adkerneladntest/supplemental/two-impressions-two-seatbids.json
+++ b/adapters/adkernelAdn/adkerneladntest/supplemental/two-impressions-two-seatbids.json
@@ -37,7 +37,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://tag.test.com/rtbpub?account=101",
+        "uri": "https://pbs2.adksrv.com/rtbpub?account=101",
         "body": {
             "app": {
             "bundle": "com.rovia.angrybirds",
diff --git a/adapters/adyoulike/adyoulike_test.go b/adapters/adyoulike/adyoulike_test.go
index 9ab689f3..e4d21a87 100644
--- a/adapters/adyoulike/adyoulike_test.go
+++ b/adapters/adyoulike/adyoulike_test.go
@@ -12,7 +12,8 @@ const testsBidderEndpoint = "https://localhost/bid/4"
 
 func TestJsonSamples(t *testing.T) {
 	bidder, buildErr := Builder(openrtb_ext.BidderAdyoulike, config.Adapter{
-		Endpoint: testsBidderEndpoint})
+		Endpoint: testsBidderEndpoint,
+	})
 
 	if buildErr != nil {
 		t.Fatalf("Builder returned unexpected error %v", buildErr)
diff --git a/adapters/adyoulike/adyouliketest/exemplary/currency-conversion.json b/adapters/adyoulike/adyouliketest/exemplary/currency-conversion.json
new file mode 100644
index 00000000..85e14fbe
--- /dev/null
+++ b/adapters/adyoulike/adyouliketest/exemplary/currency-conversion.json
@@ -0,0 +1,115 @@
+{
+    "mockBidRequest": {
+      "id": "test-request-id",
+      "imp": [
+        {
+          "id": "banner-imp-id",
+          "banner": {
+            "format": [
+              {
+                "w": 300,
+                "h": 250
+              }
+            ]
+          },
+          "bidfloor": 1.00,
+          "bidfloorcur": "EUR",
+          "ext": {
+            "bidder": {
+              "placement": "12345"
+            }
+          }
+        }
+      ],
+      "ext": {
+        "prebid": {
+          "currency": {
+            "rates": {
+              "EUR": {
+                "USD": 0.05
+              }
+            },
+            "usepbsrates": false
+          }
+        }
+      }
+    },
+    "httpCalls": [
+      {
+        "expectedRequest": {
+          "uri": "https://localhost/bid/4",
+          "body": {
+            "id": "test-request-id",
+            "cur": ["USD"],
+            "imp": [
+              {
+                "banner": {
+                  "format": [
+                    {
+                      "h": 250,
+                      "w": 300
+                    }
+                  ]
+                },
+                "bidfloor": 0.05,
+                "bidfloorcur": "USD",
+                "ext": {
+                  "bidder": {
+                    "placement": "12345"
+                  }
+                },
+                "id": "banner-imp-id",
+                "tagid": "12345"
+              }
+            ],
+            "ext": {
+              "prebid": {
+                "currency": {
+                  "rates": {
+                    "EUR": {
+                      "USD": 0.05
+                    }
+                  },
+                  "usepbsrates": false
+                }
+              }
+             }
+          }
+        },
+        "mockResponse": {
+          "status": 200,
+          "body": {
+            "id": "test-response-id",
+            "cur": "USD",
+            "seatbid": [{
+              "seat": "1",
+              "bid": [{
+                  "id":    "12340",
+                  "impid": "banner-imp-id",
+                  "price": 300,
+                  "adm": "%3C%3Fxml%20version%3D%221.0%22%20encod%2Fhtml%3E",
+                  "nurl": "http://example.com/winnoticeurl0"
+                }
+              ]
+            }]
+          }
+        }
+      }
+    ],
+    "expectedBidResponses": [
+      {
+        "bids": [
+          {
+            "bid": {
+              "id":    "12340",
+              "impid": "banner-imp-id",
+              "price": 300,
+              "nurl":  "http://example.com/winnoticeurl0",
+              "adm":   "%3C%3Fxml%20version%3D%221.0%22%20encod%2Fhtml%3E"
+            },
+            "type": "banner"
+          }
+        ]
+      }
+    ]
+  }
diff --git a/adapters/adyoulike/adyouliketest/exemplary/multiformat-impression.json b/adapters/adyoulike/adyouliketest/exemplary/multiformat-impression.json
index 2ae99c04..8e69fcf5 100644
--- a/adapters/adyoulike/adyouliketest/exemplary/multiformat-impression.json
+++ b/adapters/adyoulike/adyouliketest/exemplary/multiformat-impression.json
@@ -49,6 +49,7 @@
           "uri": "https://localhost/bid/4",
           "body": {
             "id": "test-request-id",
+            "cur": ["USD"],
             "imp": [
               {
                 "banner": {
diff --git a/adapters/adyoulike/adyouliketest/supplemental/currency-missing-conversion.json b/adapters/adyoulike/adyouliketest/supplemental/currency-missing-conversion.json
new file mode 100644
index 00000000..d7d65e43
--- /dev/null
+++ b/adapters/adyoulike/adyouliketest/supplemental/currency-missing-conversion.json
@@ -0,0 +1,43 @@
+{
+    "mockBidRequest": {
+      "id": "test-request-id",
+      "imp": [
+        {
+          "id": "banner-imp-id",
+          "banner": {
+            "format": [
+              {
+                "w": 300,
+                "h": 250
+              }
+            ]
+          },
+          "bidfloor": 1.00,
+          "bidfloorcur": "JPY",
+          "ext": {
+            "bidder": {
+              "placement": "12345"
+            }
+          }
+        }
+      ],
+      "ext": {
+        "prebid": {
+          "currency": {
+              "rates": {
+              "MXN": {
+                  "USD": 0.05
+              }
+              },
+              "usepbsrates": false
+          }
+        }
+      }
+    },
+    "expectedMakeRequestsErrors": [
+      {
+        "value": "Currency conversion rate not found: 'JPY' => 'USD'",
+        "comparison": "literal"
+      }
+    ]
+  }
diff --git a/adapters/adyoulike/adyouliketest/supplemental/invalid-bid-response.json b/adapters/adyoulike/adyouliketest/supplemental/invalid-bid-response.json
index bda8d3e6..01a80efd 100644
--- a/adapters/adyoulike/adyouliketest/supplemental/invalid-bid-response.json
+++ b/adapters/adyoulike/adyouliketest/supplemental/invalid-bid-response.json
@@ -26,6 +26,7 @@
           "uri": "https://localhost/bid/4",
           "body": {
             "id": "test-request-id",
+            "cur": ["USD"],
             "imp": [
               {
                 "banner": {
diff --git a/adapters/adyoulike/adyouliketest/supplemental/status-bad-request.json b/adapters/adyoulike/adyouliketest/supplemental/status-bad-request.json
index 83b5f361..5c4c2df3 100644
--- a/adapters/adyoulike/adyouliketest/supplemental/status-bad-request.json
+++ b/adapters/adyoulike/adyouliketest/supplemental/status-bad-request.json
@@ -26,6 +26,7 @@
           "uri": "https://localhost/bid/4",
           "body": {
             "id": "test-request-id",
+            "cur": ["USD"],
             "imp": [
               {
                 "banner": {
diff --git a/adapters/adyoulike/adyouliketest/supplemental/status-no-content.json b/adapters/adyoulike/adyouliketest/supplemental/status-no-content.json
index d7d7df19..c296128e 100644
--- a/adapters/adyoulike/adyouliketest/supplemental/status-no-content.json
+++ b/adapters/adyoulike/adyouliketest/supplemental/status-no-content.json
@@ -26,6 +26,7 @@
           "uri": "https://localhost/bid/4",
           "body": {
             "id": "test-request-id",
+            "cur": ["USD"],
             "imp": [
               {
                 "banner": {
diff --git a/adapters/adyoulike/adyouliketest/supplemental/status-service-unavailable.json b/adapters/adyoulike/adyouliketest/supplemental/status-service-unavailable.json
index 2b5872a1..445d01e2 100644
--- a/adapters/adyoulike/adyouliketest/supplemental/status-service-unavailable.json
+++ b/adapters/adyoulike/adyouliketest/supplemental/status-service-unavailable.json
@@ -26,6 +26,7 @@
           "uri": "https://localhost/bid/4",
           "body": {
             "id": "test-request-id",
+            "cur": ["USD"],
             "imp": [
               {
                 "banner": {
diff --git a/adapters/adyoulike/adyouliketest/supplemental/status-unknown.json b/adapters/adyoulike/adyouliketest/supplemental/status-unknown.json
index 0b9ce745..5177befd 100644
--- a/adapters/adyoulike/adyouliketest/supplemental/status-unknown.json
+++ b/adapters/adyoulike/adyouliketest/supplemental/status-unknown.json
@@ -26,6 +26,7 @@
           "uri": "https://localhost/bid/4",
           "body": {
             "id": "test-request-id",
+            "cur": ["USD"],
             "imp": [
               {
                 "banner": {
diff --git a/adapters/infoawarebidder_test.go b/adapters/infoawarebidder_test.go
index 62bcc08d..7bd476b3 100644
--- a/adapters/infoawarebidder_test.go
+++ b/adapters/infoawarebidder_test.go
@@ -173,7 +173,7 @@ func TestImpFiltering(t *testing.T) {
 		}
 
 		// Extra MakeRequests() call check: our mockBidder returns an adapter request for every imp
-		assert.Len(t, actualAdapterRequests, test.expectedImpLen, "Test failed. Incorrect lenght of filtered imps: %s", test.description)
+		assert.Len(t, actualAdapterRequests, test.expectedImpLen, "Test failed. Incorrect length of filtered imps: %s", test.description)
 	}
 }
 
diff --git a/adapters/pubmatic/pubmatic_test.go b/adapters/pubmatic/pubmatic_test.go
index ac7dbdb7..da5f632e 100644
--- a/adapters/pubmatic/pubmatic_test.go
+++ b/adapters/pubmatic/pubmatic_test.go
@@ -1,11 +1,14 @@
 package pubmatic
 
 import (
+	"encoding/json"
 	"testing"
 
+	"github.com/mxmCherry/openrtb/v15/openrtb2"
 	"github.com/prebid/prebid-server/adapters/adapterstest"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestJsonSamples(t *testing.T) {
@@ -67,3 +70,83 @@ func TestGetBidTypeForUnsupportedCode(t *testing.T) {
 		t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeBanner, actualBidTypeValue)
 	}
 }
+
+func TestParseImpressionObject(t *testing.T) {
+	type args struct {
+		imp                      *openrtb2.Imp
+		extractWrapperExtFromImp bool
+		extractPubIDFromImp      bool
+	}
+	tests := []struct {
+		name                string
+		args                args
+		expectedWrapperExt  *pubmaticWrapperExt
+		expectedPublisherId string
+		wantErr             bool
+		expectedBidfloor    float64
+	}{
+		{
+			name: "imp.bidfloor empty and kadfloor set",
+			args: args{
+				imp: &openrtb2.Imp{
+					Video: &openrtb2.Video{},
+					Ext:   json.RawMessage(`{"bidder":{"kadfloor":"0.12"}}`),
+				},
+			},
+			expectedBidfloor: 0.12,
+		},
+		{
+			name: "imp.bidfloor set and kadfloor empty",
+			args: args{
+				imp: &openrtb2.Imp{
+					BidFloor: 0.12,
+					Video:    &openrtb2.Video{},
+					Ext:      json.RawMessage(`{"bidder":{}}`),
+				},
+			},
+			expectedBidfloor: 0.12,
+		},
+		{
+			name: "imp.bidfloor set and kadfloor invalid",
+			args: args{
+				imp: &openrtb2.Imp{
+					BidFloor: 0.12,
+					Video:    &openrtb2.Video{},
+					Ext:      json.RawMessage(`{"bidder":{"kadfloor":"aaa"}}`),
+				},
+			},
+			expectedBidfloor: 0.12,
+		},
+		{
+			name: "imp.bidfloor set and kadfloor set, preference to kadfloor",
+			args: args{
+				imp: &openrtb2.Imp{
+					BidFloor: 0.12,
+					Video:    &openrtb2.Video{},
+					Ext:      json.RawMessage(`{"bidder":{"kadfloor":"0.11"}}`),
+				},
+			},
+			expectedBidfloor: 0.11,
+		},
+		{
+			name: "kadfloor string set with whitespace",
+			args: args{
+				imp: &openrtb2.Imp{
+					BidFloor: 0.12,
+					Video:    &openrtb2.Video{},
+					Ext:      json.RawMessage(`{"bidder":{"kadfloor":" \t  0.13  "}}`),
+				},
+			},
+			expectedBidfloor: 0.13,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			receivedWrapperExt, receivedPublisherId, err := parseImpressionObject(tt.args.imp, tt.args.extractWrapperExtFromImp, tt.args.extractPubIDFromImp)
+			assert.Equal(t, tt.wantErr, err != nil)
+			assert.Equal(t, tt.expectedWrapperExt, receivedWrapperExt)
+			assert.Equal(t, tt.expectedPublisherId, receivedPublisherId)
+			assert.Equal(t, tt.expectedBidfloor, tt.args.imp.BidFloor)
+		})
+	}
+}
diff --git a/adapters/pubmatic/pubmatictest/exemplary/banner.json b/adapters/pubmatic/pubmatictest/exemplary/banner.json
index f525673c..313099f2 100644
--- a/adapters/pubmatic/pubmatictest/exemplary/banner.json
+++ b/adapters/pubmatic/pubmatictest/exemplary/banner.json
@@ -22,6 +22,7 @@
                          "value": ["sports", "movies"]
                       }
                     ],
+                    "kadfloor": "0.12",
                     "wrapper": {
                         "version": 1,
                         "profile": 5123
@@ -56,10 +57,11 @@
                       "w": 300,
                       "h": 250
                     }
-                ],
-                "h": 250,
-                "w": 300
-            },
+                  ],
+                  "h": 250,
+                  "w": 300
+                },
+                "bidfloor": 0.12,
                 "ext": {
                     "pmZoneId": "Zone1,Zone2",
                     "preference": "sports,movies"
diff --git a/adapters/pubmatic/pubmatictest/exemplary/video.json b/adapters/pubmatic/pubmatictest/exemplary/video.json
index 1715e477..693ee3eb 100644
--- a/adapters/pubmatic/pubmatictest/exemplary/video.json
+++ b/adapters/pubmatic/pubmatictest/exemplary/video.json
@@ -28,6 +28,7 @@
                         "value": ["Zone1", "Zone2"]
                       }
                     ],
+                    "kadfloor": "0.12",
                     "wrapper": {
                         "version": 1,
                         "profile": 5123
@@ -72,6 +73,7 @@
                     "minbitrate": 10,
                     "maxbitrate": 10
                 },
+                "bidfloor": 0.12,
                 "ext": {
                     "pmZoneId": "Zone1,Zone2"
                 }
diff --git a/adapters/pubmatic/pubmatictest/supplemental/app.json b/adapters/pubmatic/pubmatictest/supplemental/app.json
index e9f1048e..c6ed97a5 100644
--- a/adapters/pubmatic/pubmatictest/supplemental/app.json
+++ b/adapters/pubmatic/pubmatictest/supplemental/app.json
@@ -22,6 +22,7 @@
                             "value": ["sports", "movies"]
                         }
                     ],
+                    "kadfloor": "0.12",
                     "wrapper": {
                         "version": 1,
                         "profile": 5123
@@ -56,10 +57,11 @@
                       "w": 300,
                       "h": 250
                     }
-                ],
-                "h": 250,
-                "w": 300
-            },
+                  ],
+                  "h": 250,
+                  "w": 300
+                },
+                "bidfloor": 0.12,
                 "ext": {
                     "pmZoneId": "Zone1,Zone2",
                     "preference": "sports,movies"
diff --git a/adapters/yahoossp/yahoossptest/exemplary/simple-video.json b/adapters/yahoossp/yahoossptest/exemplary/simple-video.json
new file mode 100644
index 00000000..d6227264
--- /dev/null
+++ b/adapters/yahoossp/yahoossptest/exemplary/simple-video.json
@@ -0,0 +1,132 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 728, 
+          "h": 90,
+          "protocols": [2],
+          "placement": 1,
+          "startdelay": -2,
+          "playbackmethod": [2],
+          "mimes": ["foo", "bar"]
+        },
+        "ext": {
+          "bidder": {
+            "dcn": "dcn1",
+            "pos": "pos2"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
+      "ip": "127.0.0.1"
+    },
+    "site": {
+      "id": "test-site-id",
+      "publisher": {
+        "id": "1"
+      }
+    }
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "User-Agent": [
+            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36"
+          ],
+          "X-Openrtb-Version": [
+            "2.5"
+          ]
+        },
+        "uri": "",
+        "body": {
+          "device": {
+            "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
+            "ip": "127.0.0.1"
+          },
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "tagid": "pos2",
+              "video": {
+                "w": 728, 
+                "h": 90,
+                "protocols": [2],
+                "placement": 1,
+                "startdelay": -2,
+                "playbackmethod": [2],
+                "mimes": ["foo", "bar"]
+              },
+              "ext": {
+                "bidder": {
+                  "dcn": "dcn1",
+                  "pos": "pos2"
+                }
+              }
+            }
+          ],
+          "site": {
+            "id": "dcn1",
+            "publisher": {
+              "id": "1"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "yahoossp",
+              "bid": [{
+                "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+                "impid": "test-imp-id",
+                "price": 0.500000,
+                "adm": "some-test-vast-ad",
+                "crid": "crid_10",
+                "h": 90,
+                "w": 728
+              }]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adm": "some-test-vast-ad",
+            "crid": "crid_10",
+            "w": 728,
+            "h": 90
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/yahoossp/yahoossptest/supplemental/invalid-banner-height.json b/adapters/yahoossp/yahoossptest/supplemental/invalid-banner-height.json
new file mode 100644
index 00000000..da4f7918
--- /dev/null
+++ b/adapters/yahoossp/yahoossptest/supplemental/invalid-banner-height.json
@@ -0,0 +1,26 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "w": 728,
+          "h": 0
+        },
+        "ext": {
+          "bidder": {
+          }
+        }
+      }
+    ],
+    "site": {}
+  },
+
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Invalid sizes provided for Banner 728x0",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/yahoossp/yahoossptest/supplemental/non-banner-bids-ignored.json b/adapters/yahoossp/yahoossptest/supplemental/non-supported-requests-bids-ignored.json
similarity index 95%
rename from adapters/yahoossp/yahoossptest/supplemental/non-banner-bids-ignored.json
rename to adapters/yahoossp/yahoossptest/supplemental/non-supported-requests-bids-ignored.json
index 6f40e2a9..11a93f68 100644
--- a/adapters/yahoossp/yahoossptest/supplemental/non-banner-bids-ignored.json
+++ b/adapters/yahoossp/yahoossptest/supplemental/non-supported-requests-bids-ignored.json
@@ -4,8 +4,8 @@
     "imp": [
       {
         "id": "test-imp-id",
-        "video": {
-          "mimes": ["video/mp4"]
+        "audio": {
+          "mimes": ["audio/mp3"]
         },
         "ext": {
           "bidder": {
@@ -52,8 +52,8 @@
             {
               "id": "test-imp-id",
               "tagid": "pos2",
-              "video": {
-               "mimes": ["video/mp4"]
+              "audio": {
+               "mimes": ["audio/mp3"]
               },
               "ext": {
                 "bidder": {
diff --git a/adapters/yahoossp/yahoossptest/supplemental/simple-banner-ignore-width-when-height-missing.json b/adapters/yahoossp/yahoossptest/supplemental/simple-banner-ignore-width-when-height-missing.json
new file mode 100644
index 00000000..16e64c61
--- /dev/null
+++ b/adapters/yahoossp/yahoossptest/supplemental/simple-banner-ignore-width-when-height-missing.json
@@ -0,0 +1,118 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "w": 300,
+          "format": [{"w": 728, "h": 90}]
+        },
+        "tagid": "123",
+        "ext": {
+          "bidder": {
+            "dcn": "dcn1",
+            "pos": "pos2"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
+      "ip": "127.0.0.1"
+    },
+    "site": {
+      "id": "test-site-id"
+    }
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "User-Agent": [
+            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36"
+          ],
+          "X-Openrtb-Version": [
+            "2.5"
+          ]
+        },
+        "uri": "",
+        "body": {
+          "device": {
+            "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
+            "ip": "127.0.0.1"
+          },
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "tagid": "pos2",
+              "banner": {
+                "format": [{"w": 728, "h": 90}],
+                "w": 728,
+                "h": 90
+              },
+              "ext": {
+                "bidder": {
+                  "dcn": "dcn1",
+                  "pos": "pos2"
+                }
+              }
+            }
+          ],
+          "site": {
+            "id": "dcn1"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "yahoossp",
+              "bid": [{
+                "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+                "impid": "test-imp-id",
+                "price": 0.500000,
+                "adm": "some-test-ad",
+                "crid": "crid_10",
+                "h": 90,
+                "w": 728
+              }]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adm": "some-test-ad",
+            "crid": "crid_10",
+            "w": 728,
+            "h": 90
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go
index 8cbba3ec..263f8b1f 100644
--- a/endpoints/cookie_sync_test.go
+++ b/endpoints/cookie_sync_test.go
@@ -2,6 +2,7 @@ package endpoints
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"io"
 	"net/http"
@@ -12,6 +13,7 @@ import (
 
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
@@ -34,6 +36,7 @@ func TestNewCookieSyncEndpoint(t *testing.T) {
 		configCCPAEnforce = true
 		metrics           = metrics.MetricsEngineMock{}
 		analytics         = MockAnalytics{}
+		fetcher           = FakeAccountsFetcher{}
 		bidders           = map[string]openrtb_ext.BidderName{"bidderA": openrtb_ext.BidderName("bidderA"), "bidderB": openrtb_ext.BidderName("bidderB")}
 	)
 
@@ -48,21 +51,27 @@ func TestNewCookieSyncEndpoint(t *testing.T) {
 		&gdprPerms,
 		&metrics,
 		&analytics,
+		&fetcher,
 		bidders,
 	)
 
 	expected := &cookieSyncEndpoint{
-		chooser:          usersync.NewChooser(syncersByBidder),
-		config:           configUserSync,
-		hostCookieConfig: &configHostCookie,
+		chooser: usersync.NewChooser(syncersByBidder),
+		config: &config.Configuration{
+			UserSync:   configUserSync,
+			HostCookie: configHostCookie,
+			GDPR:       configGDPR,
+			CCPA:       config.CCPA{Enforce: configCCPAEnforce},
+		},
 		privacyConfig: usersyncPrivacyConfig{
 			gdprConfig:      configGDPR,
 			gdprPermissions: &gdprPerms,
 			ccpaEnforce:     configCCPAEnforce,
 			bidderHashSet:   map[string]struct{}{"bidderA": {}, "bidderB": {}},
 		},
-		metrics:      &metrics,
-		pbsAnalytics: &analytics,
+		metrics:         &metrics,
+		pbsAnalytics:    &analytics,
+		accountsFetcher: &fetcher,
 	}
 
 	assert.Equal(t, expected, endpoint)
@@ -238,8 +247,8 @@ func TestCookieSyncHandle(t *testing.T) {
 		writer := httptest.NewRecorder()
 
 		endpoint := cookieSyncEndpoint{
-			chooser:          FakeChooser{Result: test.givenChooserResult},
-			hostCookieConfig: &config.HostCookie{},
+			chooser: FakeChooser{Result: test.givenChooserResult},
+			config:  &config.Configuration{},
 			privacyConfig: usersyncPrivacyConfig{
 				gdprConfig: config.GDPR{
 					Enabled:      true,
@@ -263,14 +272,15 @@ func TestCookieSyncParseRequest(t *testing.T) {
 	expectedCCPAParsedPolicy, _ := ccpa.Policy{Consent: "1NYN"}.Parse(map[string]struct{}{})
 
 	testCases := []struct {
-		description      string
-		givenConfig      config.UserSync
-		givenBody        io.Reader
-		givenGDPRConfig  config.GDPR
-		givenCCPAEnabled bool
-		expectedError    string
-		expectedPrivacy  privacy.Policies
-		expectedRequest  usersync.Request
+		description          string
+		givenConfig          config.UserSync
+		givenBody            io.Reader
+		givenGDPRConfig      config.GDPR
+		givenCCPAEnabled     bool
+		givenAccountRequired bool
+		expectedError        string
+		expectedPrivacy      privacy.Policies
+		expectedRequest      usersync.Request
 	}{
 		{
 			description: "Complete Request",
@@ -647,18 +657,121 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			givenCCPAEnabled: true,
 			expectedError:    "Failed to read request body",
 		},
+		{
+			description: "Account Defaults - Max Limit + Default Coop",
+			givenBody: strings.NewReader(`{` +
+				`"bidders":["a", "b"],` +
+				`"limit":42,` +
+				`"account":"TestAccount"` +
+				`}`),
+			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
+			givenCCPAEnabled: true,
+			givenConfig: config.UserSync{
+				Cooperative: config.UserSyncCooperative{
+					PriorityGroups: [][]string{{"a", "b", "c"}},
+				},
+			},
+			expectedPrivacy: privacy.Policies{},
+			expectedRequest: usersync.Request{
+				Bidders: []string{"a", "b"},
+				Cooperative: usersync.Cooperative{
+					Enabled:        true,
+					PriorityGroups: [][]string{{"a", "b", "c"}},
+				},
+				Limit: 30,
+				Privacy: usersyncPrivacy{
+					gdprSignal: gdpr.SignalAmbiguous,
+				},
+				SyncTypeFilter: usersync.SyncTypeFilter{
+					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
+					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
+				},
+			},
+		},
+		{
+			description: "Account Defaults - DefaultLimit",
+			givenBody: strings.NewReader(`{` +
+				`"bidders":["a", "b"],` +
+				`"account":"TestAccount"` +
+				`}`),
+			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
+			givenCCPAEnabled: true,
+			givenConfig: config.UserSync{
+				Cooperative: config.UserSyncCooperative{
+					EnabledByDefault: false,
+					PriorityGroups:   [][]string{{"a", "b", "c"}},
+				},
+			},
+			expectedPrivacy: privacy.Policies{},
+			expectedRequest: usersync.Request{
+				Bidders: []string{"a", "b"},
+				Cooperative: usersync.Cooperative{
+					Enabled:        true,
+					PriorityGroups: [][]string{{"a", "b", "c"}},
+				},
+				Limit: 20,
+				Privacy: usersyncPrivacy{
+					gdprSignal: gdpr.SignalAmbiguous,
+				},
+				SyncTypeFilter: usersync.SyncTypeFilter{
+					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
+					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
+				},
+			},
+		},
+		{
+			description: "Account Defaults - Error",
+			givenBody: strings.NewReader(`{` +
+				`"bidders":["a", "b"],` +
+				`"account":"DisabledAccount"` +
+				`}`),
+			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
+			givenCCPAEnabled: true,
+			givenConfig: config.UserSync{
+				Cooperative: config.UserSyncCooperative{
+					EnabledByDefault: false,
+					PriorityGroups:   [][]string{{"a", "b", "c"}},
+				},
+			},
+			expectedPrivacy: privacy.Policies{},
+			expectedRequest: usersync.Request{
+				Bidders: []string{"a", "b"},
+				Cooperative: usersync.Cooperative{
+					Enabled:        true,
+					PriorityGroups: [][]string{{"a", "b", "c"}},
+				},
+				Limit: 20,
+				Privacy: usersyncPrivacy{
+					gdprSignal: gdpr.SignalAmbiguous,
+				},
+				SyncTypeFilter: usersync.SyncTypeFilter{
+					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
+					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
+				},
+			},
+			expectedError:        errCookieSyncAccountBlocked.Error(),
+			givenAccountRequired: true,
+		},
 	}
 
 	for _, test := range testCases {
 		httpRequest := httptest.NewRequest("POST", "/cookiesync", test.givenBody)
 
 		endpoint := cookieSyncEndpoint{
-			config: test.givenConfig,
+			config: &config.Configuration{
+				UserSync:        test.givenConfig,
+				AccountRequired: test.givenAccountRequired,
+			},
 			privacyConfig: usersyncPrivacyConfig{
 				gdprConfig:  test.givenGDPRConfig,
 				ccpaEnforce: test.givenCCPAEnabled,
 			},
+			accountsFetcher: FakeAccountsFetcher{AccountData: map[string]json.RawMessage{
+				"TestAccount":     json.RawMessage(`{"cookie_sync": {"default_limit": 20, "max_limit": 30, "default_coop_sync": true}}`),
+				"DisabledAccount": json.RawMessage(`{"disabled":true}`),
+			}},
 		}
+		assert.NoError(t, endpoint.config.MarshalAccountDefaults())
 		request, privacyPolicies, err := endpoint.parseRequest(httpRequest)
 
 		if test.expectedError == "" {
@@ -673,6 +786,185 @@ func TestCookieSyncParseRequest(t *testing.T) {
 	}
 }
 
+func TestSetLimit(t *testing.T) {
+	coopSync := true
+
+	testCases := []struct {
+		description     string
+		givenRequest    cookieSyncRequest
+		givenConfig     config.UserSync
+		expectedError   string
+		expectedRequest cookieSyncRequest
+	}{
+		{
+			description: "Test that Max Limit and Default Coop Sync are Applied",
+			givenRequest: cookieSyncRequest{
+				Account: "TestAccount",
+				Limit:   42,
+			},
+			givenConfig: config.UserSync{
+				Cooperative: config.UserSyncCooperative{
+					PriorityGroups: [][]string{{"a", "b", "c"}},
+				},
+			},
+			expectedRequest: cookieSyncRequest{
+				Account:         "TestAccount",
+				Limit:           30,
+				CooperativeSync: &coopSync,
+			},
+			expectedError: "",
+		},
+		{
+			description: "Test that Default Limit is Applied",
+			givenRequest: cookieSyncRequest{
+				Account: "TestAccount",
+			},
+			givenConfig: config.UserSync{
+				Cooperative: config.UserSyncCooperative{
+					PriorityGroups: [][]string{{"a", "b", "c"}},
+				},
+			},
+			expectedRequest: cookieSyncRequest{
+				Account:         "TestAccount",
+				Limit:           20,
+				CooperativeSync: &coopSync,
+			},
+			expectedError: "",
+		},
+		{
+			description: "Test that max limit is applied when given limit is zero",
+			givenRequest: cookieSyncRequest{
+				Account: "ZeroLimitAccount",
+			},
+			givenConfig: config.UserSync{
+				Cooperative: config.UserSyncCooperative{
+					PriorityGroups: [][]string{{"a", "b", "c"}},
+				},
+			},
+			expectedRequest: cookieSyncRequest{
+				Account:         "ZeroLimitAccount",
+				Limit:           30,
+				CooperativeSync: &coopSync,
+			},
+			expectedError: "",
+		},
+		{
+			description: "Negative Value Check",
+			givenRequest: cookieSyncRequest{
+				Account: "NegativeLimitAccount",
+			},
+			givenConfig: config.UserSync{
+				Cooperative: config.UserSyncCooperative{
+					PriorityGroups: [][]string{{"a", "b", "c"}},
+				},
+			},
+			expectedRequest: cookieSyncRequest{
+				Account:         "NegativeLimitAccount",
+				Limit:           0,
+				CooperativeSync: &coopSync,
+			},
+			expectedError: "",
+		},
+		{
+			description: "Error Handler",
+			givenRequest: cookieSyncRequest{
+				Account: "DisabledAccount",
+			},
+			givenConfig: config.UserSync{
+				Cooperative: config.UserSyncCooperative{
+					PriorityGroups: [][]string{{"a", "b", "c"}},
+				},
+			},
+			expectedRequest: cookieSyncRequest{
+				Account: "DisabledAccount",
+			},
+			expectedError: errCookieSyncAccountBlocked.Error(),
+		},
+	}
+
+	for _, test := range testCases {
+		endpoint := cookieSyncEndpoint{
+			config: &config.Configuration{
+				UserSync: test.givenConfig,
+			},
+			accountsFetcher: FakeAccountsFetcher{AccountData: map[string]json.RawMessage{
+				"TestAccount":          json.RawMessage(`{"cookie_sync": {"default_limit": 20, "max_limit": 30, "default_coop_sync": true}}`),
+				"DisabledAccount":      json.RawMessage(`{"disabled":true}`),
+				"ZeroLimitAccount":     json.RawMessage(`{"cookie_sync": {"default_limit": 0, "max_limit": 30, "default_coop_sync": true}}`),
+				"NegativeLimitAccount": json.RawMessage(`{"cookie_sync": {"default_limit": -1, "max_limit": -1, "default_coop_sync": true}}`),
+			}},
+		}
+		assert.NoError(t, endpoint.config.MarshalAccountDefaults())
+		request, err := endpoint.setLimit(test.givenRequest)
+
+		if test.expectedError == "" {
+			assert.NoError(t, err, test.description)
+			assert.Equal(t, test.expectedRequest, request, test.description)
+		} else {
+			assert.EqualError(t, err, test.expectedError, test.description)
+		}
+	}
+}
+
+func TestWriteParseRequestErrorMetrics(t *testing.T) {
+	err := errors.New("anyError")
+
+	mockAnalytics := MockAnalytics{}
+	mockAnalytics.On("LogCookieSyncObject", mock.Anything)
+	writer := httptest.NewRecorder()
+
+	endpoint := cookieSyncEndpoint{pbsAnalytics: &mockAnalytics}
+	endpoint.handleError(writer, err, 418)
+
+	assert.Equal(t, writer.Code, 418)
+	assert.Equal(t, writer.Body.String(), "anyError\n")
+	mockAnalytics.AssertCalled(t, "LogCookieSyncObject", &analytics.CookieSyncObject{
+		Status:       418,
+		Errors:       []error{err},
+		BidderStatus: []*analytics.CookieSyncBidder{},
+	})
+}
+
+func TestCookieSyncWriteParseRequestErrorMetrics(t *testing.T) {
+	testCases := []struct {
+		description     string
+		err             error
+		setExpectations func(*metrics.MetricsEngineMock)
+	}{
+		{
+			description: "Account Blocked",
+			err:         errCookieSyncAccountBlocked,
+			setExpectations: func(m *metrics.MetricsEngineMock) {
+				m.On("RecordCookieSync", metrics.CookieSyncAccountBlocked).Once()
+			},
+		},
+		{
+			description: "Account Invalid",
+			err:         errCookieSyncAccountInvalid,
+			setExpectations: func(m *metrics.MetricsEngineMock) {
+				m.On("RecordCookieSync", metrics.CookieSyncAccountInvalid).Once()
+			},
+		},
+		{
+			description: "No Special Case",
+			err:         errors.New("any error"),
+			setExpectations: func(m *metrics.MetricsEngineMock) {
+				m.On("RecordCookieSync", metrics.CookieSyncBadRequest).Once()
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		mockMetrics := metrics.MetricsEngineMock{}
+		test.setExpectations(&mockMetrics)
+
+		endpoint := &cookieSyncEndpoint{metrics: &mockMetrics}
+		endpoint.writeParseRequestErrorMetrics(test.err)
+
+		mockMetrics.AssertExpectations(t)
+	}
+}
+
 func TestParseTypeFilter(t *testing.T) {
 	testCases := []struct {
 		description    string
@@ -1242,6 +1534,50 @@ func TestUsersyncPrivacyCCPAAllowsBidderSync(t *testing.T) {
 	}
 }
 
+func TestCombineErrors(t *testing.T) {
+	testCases := []struct {
+		description    string
+		givenErrorList []error
+		expectedError  error
+	}{
+		{
+			description:    "No errors given",
+			givenErrorList: []error{},
+			expectedError:  errors.New(""),
+		},
+		{
+			description:    "One error given",
+			givenErrorList: []error{errors.New("Error #1")},
+			expectedError:  errors.New("Error #1"),
+		},
+		{
+			description:    "Multiple errors given",
+			givenErrorList: []error{errors.New("Error #1"), errors.New("Error #2")},
+			expectedError:  errors.New("Error #1 Error #2"),
+		},
+		{
+			description:    "Special Case: blocked (rejected via block list)",
+			givenErrorList: []error{&errortypes.BlacklistedAcct{}},
+			expectedError:  errCookieSyncAccountBlocked,
+		},
+		{
+			description:    "Special Case: invalid (rejected via allow list)",
+			givenErrorList: []error{&errortypes.AcctRequired{}},
+			expectedError:  errCookieSyncAccountInvalid,
+		},
+		{
+			description:    "Special Case: multiple special cases, first one wins",
+			givenErrorList: []error{&errortypes.BlacklistedAcct{}, &errortypes.AcctRequired{}},
+			expectedError:  errCookieSyncAccountBlocked,
+		},
+	}
+
+	for _, test := range testCases {
+		combinedErrors := combineErrors(test.givenErrorList)
+		assert.Equal(t, test.expectedError, combinedErrors, test.description)
+	}
+}
+
 type FakeChooser struct {
 	Result usersync.Result
 }
@@ -1321,6 +1657,17 @@ func (m *MockGDPRPerms) AuctionActivitiesAllowed(ctx context.Context, bidderCore
 	return args.Bool(0), args.Bool(1), args.Bool(2), args.Error(3)
 }
 
+type FakeAccountsFetcher struct {
+	AccountData map[string]json.RawMessage
+}
+
+func (f FakeAccountsFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) {
+	if account, ok := f.AccountData[accountID]; ok {
+		return account, nil
+	}
+	return nil, []error{errors.New("Account not found")}
+}
+
 // ErrReader returns an io.Reader that returns 0, err from all Read calls. This is added in
 // Go 1.16. Copied here for now until we switch over.
 func ErrReader(err error) io.Reader {
diff --git a/endpoints/events/event_test.go b/endpoints/events/event_test.go
index 09ad6d57..f700c31e 100644
--- a/endpoints/events/event_test.go
+++ b/endpoints/events/event_test.go
@@ -14,6 +14,7 @@ import (
 
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/stretchr/testify/assert"
 )
@@ -645,6 +646,16 @@ func TestShouldParseEventCorrectly(t *testing.T) {
 				Analytics: analytics.Enabled,
 			},
 		},
+		"three - vtype = start": {
+			req: httptest.NewRequest("GET", "/event?t=vast&vtype=start&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			expected: &analytics.EventRequest{
+				Type:      analytics.Vast,
+				VType:     analytics.Start,
+				BidID:     "bidId",
+				Timestamp: 0,
+				Analytics: analytics.Enabled,
+			},
+		},
 	}
 
 	for name, test := range tests {
@@ -750,3 +761,147 @@ func TestReadIntegrationType(t *testing.T) {
 		}
 	}
 }
+
+func TestShouldReturnBadRequestWhenVTypeIsInvalid(t *testing.T) {
+
+	reqData := ""
+
+	tests := []struct {
+		description        string
+		req                *http.Request
+		expectedStatusCode int
+		expectedStatus     string
+	}{
+		{
+			description:        "vtype parameter is missing",
+			req:                httptest.NewRequest("GET", "/event?t=vast&b=bidID", strings.NewReader(reqData)),
+			expectedStatusCode: 400,
+			expectedStatus:     "invalid request: parameter 'vtype' is required\n",
+		},
+		{
+			description:        "invalid vtype parameter",
+			req:                httptest.NewRequest("GET", "/event?t=vast&vtype=abc&b=bidID", strings.NewReader(reqData)),
+			expectedStatusCode: 400,
+			expectedStatus:     "invalid request: unknown vtype: 'abc'\n",
+		},
+		{
+			description:        "vtype is passed when event != vast",
+			req:                httptest.NewRequest("GET", "/event?t=win&vtype=startc&b=bidID", strings.NewReader(reqData)),
+			expectedStatusCode: 400,
+			expectedStatus:     "invalid request: parameter 'vtype' is only required for t=vast\n",
+		},
+	}
+
+	for _, test := range tests {
+		mockAccountsFetcher := &mockAccountsFetcher{}
+
+		mockAnalyticsModule := &eventsMockAnalyticsModule{
+			Fail: false,
+		}
+
+		cfg := &config.Configuration{
+			AccountDefaults: config.Account{},
+		}
+
+		recorder := httptest.NewRecorder()
+
+		e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+		e(recorder, test.req, nil)
+
+		d, err := ioutil.ReadAll(recorder.Result().Body)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assert.Equal(t, test.expectedStatusCode, recorder.Result().StatusCode, test.description)
+		assert.Equal(t, test.expectedStatus, string(d), test.description)
+
+	}
+}
+
+func TestReadVType(t *testing.T) {
+	type args struct {
+		er  *analytics.EventRequest
+		req *http.Request
+	}
+	tests := []struct {
+		name          string
+		args          args
+		expectedError error
+		expectedVType analytics.VastType
+	}{
+		{
+			name: "vtype = start",
+			args: args{
+				er: &analytics.EventRequest{
+					Type: analytics.Vast,
+				},
+				req: httptest.NewRequest("GET", "/event?t=vast&vtype=start&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			},
+			expectedError: nil,
+			expectedVType: analytics.Start,
+		},
+		{
+			name: "vtype = firstQuartile",
+			args: args{
+				er: &analytics.EventRequest{
+					Type: analytics.Vast,
+				},
+				req: httptest.NewRequest("GET", "/event?t=vast&vtype=firstQuartile&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			},
+			expectedError: nil,
+			expectedVType: analytics.FirstQuartile,
+		},
+		{
+			name: "vtype = midPoint",
+			args: args{
+				er: &analytics.EventRequest{
+					Type: analytics.Vast,
+				},
+				req: httptest.NewRequest("GET", "/event?t=vast&vtype=midPoint&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			},
+			expectedError: nil,
+			expectedVType: analytics.MidPoint,
+		},
+		{
+			name: "vtype = thirdQuartile",
+			args: args{
+				er: &analytics.EventRequest{
+					Type: analytics.Vast,
+				},
+				req: httptest.NewRequest("GET", "/event?t=vast&vtype=thirdQuartile&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			},
+			expectedError: nil,
+			expectedVType: analytics.ThirdQuartile,
+		},
+		{
+			name: "vtype = complete",
+			args: args{
+				er: &analytics.EventRequest{
+					Type: analytics.Vast,
+				},
+				req: httptest.NewRequest("GET", "/event?t=vast&vtype=complete&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			},
+			expectedError: nil,
+			expectedVType: analytics.Complete,
+		},
+		{
+			name: "unknown vtype",
+			args: args{
+				er: &analytics.EventRequest{
+					Type: analytics.Vast,
+				},
+				req: httptest.NewRequest("GET", "/event?t=vast&vtype=test&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			},
+			expectedError: &errortypes.BadInput{Message: "unknown vtype: 'test'"},
+			expectedVType: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := readVType(tt.args.er, tt.args.req)
+			assert.Equal(t, tt.expectedError, err, tt.name)
+			assert.Equal(t, tt.expectedVType, tt.args.er.VType, tt.name)
+		})
+	}
+}
diff --git a/endpoints/info/bidders_test.go b/endpoints/info/bidders_test.go
index 7110e188..9be25f44 100644
--- a/endpoints/info/bidders_test.go
+++ b/endpoints/info/bidders_test.go
@@ -179,7 +179,7 @@ func TestBiddersHandler(t *testing.T) {
 		expectedHeaders http.Header
 	}{
 		{
-			description:     "No Query Paramters - Backwards Compatability",
+			description:     "No Query Parameters - Backwards Compatibility",
 			givenURL:        "/info/bidders",
 			expectedStatus:  http.StatusOK,
 			expectedBody:    `["a","b","x","y"]`,
diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go
index 48e054c7..f2726de0 100644
--- a/exchange/targeting_test.go
+++ b/exchange/targeting_test.go
@@ -247,6 +247,7 @@ type TargetingTestData struct {
 	IsApp                      bool
 	CategoryMapping            map[string]string
 	ExpectedBidTargetsByBidder map[string]map[openrtb_ext.BidderName]map[string]string
+	TruncateTargetAttr         *int
 }
 
 var bid123 *openrtb2.Bid = &openrtb2.Bid{
@@ -261,6 +262,9 @@ var bid084 *openrtb2.Bid = &openrtb2.Bid{
 	Price: 0.84,
 }
 
+var truncateTargetAttrValueLess int = 10
+var truncateTargetAttrValueGreater int = 25
+var truncateTargetAttrValueNegative int = -1
 var TargetingTests []TargetingTestData = []TargetingTestData{
 	{
 		Description: "Targeting winners only (most basic targeting example)",
@@ -291,6 +295,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 				openrtb_ext.BidderRubicon: {},
 			},
 		},
+		TruncateTargetAttr: nil,
 	},
 	{
 		Description: "Targeting on bidders only",
@@ -324,6 +329,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 				},
 			},
 		},
+		TruncateTargetAttr: nil,
 	},
 	{
 		Description: "Full basic targeting with hd_format",
@@ -364,6 +370,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 				},
 			},
 		},
+		TruncateTargetAttr: nil,
 	},
 	{
 		Description: "Cache and deal targeting test",
@@ -410,6 +417,109 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 				},
 			},
 		},
+		TruncateTargetAttr: nil,
+	},
+	{
+		Description: "Truncate Targeting Attribute value is given and is less than const MaxKeyLength",
+		TargetData: targetData{
+			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			includeBidderKeys: true,
+		},
+		Auction: auction{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+				"ImpId-1": {
+					openrtb_ext.BidderAppnexus: {
+						bid:     bid123,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+					openrtb_ext.BidderRubicon: {
+						bid:     bid084,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+		},
+		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+			"ImpId-1": {
+				openrtb_ext.BidderAppnexus: {
+					"hb_bidder_": "appnexus",
+					"hb_pb_appn": "1.20",
+				},
+				openrtb_ext.BidderRubicon: {
+					"hb_bidder_": "rubicon",
+					"hb_pb_rubi": "0.80",
+				},
+			},
+		},
+		TruncateTargetAttr: &truncateTargetAttrValueLess,
+	},
+	{
+		Description: "Truncate Targeting Attribute value is given and is greater than const MaxKeyLength",
+		TargetData: targetData{
+			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			includeBidderKeys: true,
+		},
+		Auction: auction{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+				"ImpId-1": {
+					openrtb_ext.BidderAppnexus: {
+						bid:     bid123,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+					openrtb_ext.BidderRubicon: {
+						bid:     bid084,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+		},
+		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+			"ImpId-1": {
+				openrtb_ext.BidderAppnexus: {
+					"hb_bidder_appnexus": "appnexus",
+					"hb_pb_appnexus":     "1.20",
+				},
+				openrtb_ext.BidderRubicon: {
+					"hb_bidder_rubicon": "rubicon",
+					"hb_pb_rubicon":     "0.80",
+				},
+			},
+		},
+		TruncateTargetAttr: &truncateTargetAttrValueGreater,
+	},
+	{
+		Description: "Truncate Targeting Attribute value is given and is negative",
+		TargetData: targetData{
+			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			includeBidderKeys: true,
+		},
+		Auction: auction{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+				"ImpId-1": {
+					openrtb_ext.BidderAppnexus: {
+						bid:     bid123,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+					openrtb_ext.BidderRubicon: {
+						bid:     bid084,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+		},
+		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+			"ImpId-1": {
+				openrtb_ext.BidderAppnexus: {
+					"hb_bidder_appnexus": "appnexus",
+					"hb_pb_appnexus":     "1.20",
+				},
+				openrtb_ext.BidderRubicon: {
+					"hb_bidder_rubicon": "rubicon",
+					"hb_pb_rubicon":     "0.80",
+				},
+			},
+		},
+		TruncateTargetAttr: &truncateTargetAttrValueNegative,
 	},
 }
 
@@ -433,7 +543,7 @@ func TestSetTargeting(t *testing.T) {
 		}
 		auc.winningBids = winningBids
 		targData := test.TargetData
-		targData.setTargeting(auc, test.IsApp, test.CategoryMapping)
+		targData.setTargeting(auc, test.IsApp, test.CategoryMapping, test.TruncateTargetAttr)
 		for imp, targetsByBidder := range test.ExpectedBidTargetsByBidder {
 			for bidder, expected := range targetsByBidder {
 				assert.Equal(t,
diff --git a/firstpartydata/first_party_data_test.go b/firstpartydata/first_party_data_test.go
index 1e09bab1..1e0dc015 100644
--- a/firstpartydata/first_party_data_test.go
+++ b/firstpartydata/first_party_data_test.go
@@ -762,7 +762,7 @@ func TestResolveUser(t *testing.T) {
 	fpdConfigUser[yobKey] = []byte(`1980`)
 	fpdConfigUser[genderKey] = []byte(`"M"`)
 	fpdConfigUser[keywordsKey] = []byte(`"fpdConfigUserKeywords"`)
-	fpdConfigUser["data"] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
+	fpdConfigUser[dataKey] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
 	fpdConfigUser["ext"] = []byte(`{"data":{"fpdConfigUserExt": 123}}`)
 
 	bidRequestUser := &openrtb2.User{
@@ -789,6 +789,8 @@ func TestResolveUser(t *testing.T) {
 		Data: []openrtb2.Data{
 			{ID: "openRtbGlobalFPDUserDataId1", Name: "openRtbGlobalFPDUserDataName1"},
 			{ID: "openRtbGlobalFPDUserDataId2", Name: "openRtbGlobalFPDUserDataName2"},
+			{ID: "UserDataId1", Name: "UserDataName1"},
+			{ID: "UserDataId2", Name: "UserDataName2"},
 		},
 	}
 
@@ -801,10 +803,6 @@ func TestResolveUser(t *testing.T) {
 			description:       "bid request user.ext is nil",
 			bidRequestUserExt: nil,
 			expectedUserExt: `{"data":{
-									"data":[
-										{"id":"UserDataId1","name":"UserDataName1"},
-										{"id":"UserDataId2","name":"UserDataName2"}
-									],
 									"fpdConfigUserExt":123,
 									"globalFPDUserData":"globalFPDUserDataValue",
 									"id":"fpdConfigUserId"
@@ -815,10 +813,6 @@ func TestResolveUser(t *testing.T) {
 			description:       "bid request user.ext is not nil",
 			bidRequestUserExt: []byte(`{"bidRequestUserExt": 1234}`),
 			expectedUserExt: `{"data":{
-									"data":[
-										{"id":"UserDataId1","name":"UserDataName1"},
-										{"id":"UserDataId2","name":"UserDataName2"}
-									],
 									"fpdConfigUserExt":123,
 									"globalFPDUserData":"globalFPDUserDataValue",
 									"id":"fpdConfigUserId"
@@ -836,7 +830,7 @@ func TestResolveUser(t *testing.T) {
 		fpdConfigUser[yobKey] = []byte(`1980`)
 		fpdConfigUser[genderKey] = []byte(`"M"`)
 		fpdConfigUser[keywordsKey] = []byte(`"fpdConfigUserKeywords"`)
-		fpdConfigUser["data"] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
+		fpdConfigUser[dataKey] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
 		fpdConfigUser["ext"] = []byte(`{"data":{"fpdConfigUserExt": 123}}`)
 		fpdConfig := &openrtb_ext.ORTB2{User: fpdConfigUser}
 
@@ -885,7 +879,7 @@ func TestMergeUsers(t *testing.T) {
 	fpdConfigUser[yobKey] = []byte(`1980`)
 	fpdConfigUser[genderKey] = []byte(`"M"`)
 	fpdConfigUser[keywordsKey] = []byte(`"fpdConfigUserKeywords"`)
-	fpdConfigUser["data"] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
+	fpdConfigUser[dataKey] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
 	fpdConfigUser["ext"] = []byte(`{"data":{"fpdConfigUserExt": 123}}`)
 
 	resultUser, err := mergeUsers(originalUser, fpdConfigUser)
@@ -893,9 +887,6 @@ func TestMergeUsers(t *testing.T) {
 
 	expectedUserExt := `{"bidRequestUserExt":1234,
 						 "data":{
-							"data":[
-								{"id":"UserDataId1","name":"UserDataName1"},
-								{"id":"UserDataId2","name":"UserDataName2"}],
 							"fpdConfigUserExt":123,
 							"id":"fpdConfigUserId"}
 						 }`
@@ -910,6 +901,8 @@ func TestMergeUsers(t *testing.T) {
 		Data: []openrtb2.Data{
 			{ID: "openRtbGlobalFPDUserDataId1", Name: "openRtbGlobalFPDUserDataName1"},
 			{ID: "openRtbGlobalFPDUserDataId2", Name: "openRtbGlobalFPDUserDataName2"},
+			{ID: "UserDataId1", Name: "UserDataName1"},
+			{ID: "UserDataId2", Name: "UserDataName2"},
 		},
 	}
 	assert.Equal(t, expectedUser, resultUser, "Result user is incorrect")
@@ -959,7 +952,7 @@ func TestResolveSite(t *testing.T) {
 	fpdConfigSite[keywordsKey] = []byte(`"fpdConfigSiteKeywords"`)
 	fpdConfigSite[nameKey] = []byte(`"fpdConfigSiteName"`)
 	fpdConfigSite[pageKey] = []byte(`"fpdConfigSitePage"`)
-	fpdConfigSite["data"] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
+	fpdConfigSite[dataKey] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
 	fpdConfigSite["ext"] = []byte(`{"data":{"fpdConfigSiteExt": 123}}`)
 
 	bidRequestSite := &openrtb2.Site{
@@ -1070,7 +1063,7 @@ func TestResolveSite(t *testing.T) {
 		fpdConfigSite := make(map[string]json.RawMessage, 0)
 		fpdConfigSite["id"] = []byte(`"fpdConfigSiteId"`)
 		fpdConfigSite[keywordsKey] = []byte(`"fpdConfigSiteKeywords"`)
-		fpdConfigSite["data"] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
+		fpdConfigSite[dataKey] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
 		fpdConfigSite["ext"] = []byte(`{"data":{"fpdConfigSiteExt": 123}}`)
 		fpdConfig := &openrtb_ext.ORTB2{Site: fpdConfigSite}
 
@@ -1134,7 +1127,7 @@ func TestMergeSites(t *testing.T) {
 	fpdConfigSite[pageCatKey] = []byte(`["cars3", "auto3"]`)
 	fpdConfigSite[searchKey] = []byte(`"fpdConfigSiteSearch"`)
 	fpdConfigSite[refKey] = []byte(`"fpdConfigSiteRef"`)
-	fpdConfigSite["data"] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
+	fpdConfigSite[dataKey] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
 	fpdConfigSite["ext"] = []byte(`{"data":{"fpdConfigSiteExt": 123}}`)
 
 	resultSite, err := mergeSites(originalSite, fpdConfigSite, "appnexus")
@@ -1181,7 +1174,7 @@ func TestResolveApp(t *testing.T) {
 	fpdConfigApp[keywordsKey] = []byte(`"fpdConfigAppKeywords"`)
 	fpdConfigApp[nameKey] = []byte(`"fpdConfigAppName"`)
 	fpdConfigApp[bundleKey] = []byte(`"fpdConfigAppBundle"`)
-	fpdConfigApp["data"] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
+	fpdConfigApp[dataKey] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
 	fpdConfigApp["ext"] = []byte(`{"data":{"fpdConfigAppExt": 123}}`)
 
 	bidRequestApp := &openrtb2.App{
@@ -1292,7 +1285,7 @@ func TestResolveApp(t *testing.T) {
 		fpdConfigApp := make(map[string]json.RawMessage, 0)
 		fpdConfigApp["id"] = []byte(`"fpdConfigAppId"`)
 		fpdConfigApp[keywordsKey] = []byte(`"fpdConfigAppKeywords"`)
-		fpdConfigApp["data"] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
+		fpdConfigApp[dataKey] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
 		fpdConfigApp["ext"] = []byte(`{"data":{"fpdConfigAppExt": 123}}`)
 		fpdConfig := &openrtb_ext.ORTB2{App: fpdConfigApp}
 
@@ -1356,7 +1349,7 @@ func TestMergeApps(t *testing.T) {
 	fpdConfigApp[catKey] = []byte(`["cars1", "auto1"]`)
 	fpdConfigApp[sectionCatKey] = []byte(`["cars2", "auto2"]`)
 	fpdConfigApp[pageCatKey] = []byte(`["cars3", "auto3"]`)
-	fpdConfigApp["data"] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
+	fpdConfigApp[dataKey] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
 	fpdConfigApp["ext"] = []byte(`{"data":{"fpdConfigAppExt": 123}}`)
 
 	resultApp, err := mergeApps(originalApp, fpdConfigApp)
diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json
index c04838c7..ed81b415 100644
--- a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json
+++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json
@@ -200,19 +200,19 @@
         "gender": "F",
         "yob": 2000,
         "keywords": "apnFpdUserKeyword",
+        "data": [
+          {
+            "id": "apnFpdUserContentDataId1",
+            "name": "apnFpdUserContentDataName1"
+          },
+          {
+            "id": "apnFpdUserContentDataId2",
+            "name": "apnFpdUserContentDataName2"
+          }
+        ],
         "ext": {
           "data": {
-            "id": "apnUserId",
-            "data": [
-              {
-                "id": "apnFpdUserContentDataId1",
-                "name": "apnFpdUserContentDataName1"
-              },
-              {
-                "id": "apnFpdUserContentDataId2",
-                "name": "apnFpdUserContentDataName2"
-              }
-            ]
+            "id": "apnUserId"
           }
         }
       }
diff --git a/privacy/scrubber_test.go b/privacy/scrubber_test.go
index 1198d0bb..13ac1f5c 100644
--- a/privacy/scrubber_test.go
+++ b/privacy/scrubber_test.go
@@ -37,7 +37,7 @@ func TestScrubDevice(t *testing.T) {
 		geo         ScrubStrategyGeo
 	}{
 		{
-			description: "All Strageties - None",
+			description: "All Strategies - None",
 			expected:    device,
 			id:          ScrubStrategyDeviceIDNone,
 			ipv4:        ScrubStrategyIPV4None,
@@ -45,7 +45,7 @@ func TestScrubDevice(t *testing.T) {
 			geo:         ScrubStrategyGeoNone,
 		},
 		{
-			description: "All Strageties - Strictest",
+			description: "All Strategies - Strictest",
 			expected: &openrtb2.Device{
 				DIDMD5:   "",
 				DIDSHA1:  "",
diff --git a/stored_requests/data/by_id/accounts/test.json b/stored_requests/data/by_id/accounts/test.json
index 76bafff7..07492f75 100644
--- a/stored_requests/data/by_id/accounts/test.json
+++ b/stored_requests/data/by_id/accounts/test.json
@@ -10,5 +10,10 @@
     },
     "events": {
         "enabled": true
+    },
+    "cookie_sync": {
+        "default_limt": 20,
+        "max_limit": 50,
+        "default_coop_sync": true
     }
 }
diff --git a/util/task/ticker_task_test.go b/util/task/ticker_task_test.go
index 27551c9a..c02eec15 100644
--- a/util/task/ticker_task_test.go
+++ b/util/task/ticker_task_test.go
@@ -1,6 +1,7 @@
 package task_test
 
 import (
+	"sync"
 	"testing"
 	"time"
 
@@ -9,55 +10,78 @@ import (
 )
 
 type MockRunner struct {
-	RunCount int
+	ExpectationMet chan struct{}
+	actualCalls    int
+	expectedCalls  int
+	mutex          sync.Mutex
 }
 
-func (mcc *MockRunner) Run() error {
-	mcc.RunCount++
-	return nil
+func NewMockRunner(expectedCalls int) *MockRunner {
+	return &MockRunner{
+		ExpectationMet: make(chan struct{}),
+		expectedCalls:  expectedCalls,
+	}
 }
 
-func TestStartWithSingleRun(t *testing.T) {
-	// Setup:
-	runner := &MockRunner{RunCount: 0}
-	interval := 0 * time.Millisecond
-	ticker := task.NewTickerTask(interval, runner)
+func (m *MockRunner) Run() error {
+	m.mutex.Lock()
+	defer m.mutex.Unlock()
+	m.actualCalls++
 
-	// Execute:
-	ticker.Start()
-	time.Sleep(10 * time.Millisecond)
+	if m.expectedCalls == m.actualCalls {
+		close(m.ExpectationMet)
+	}
 
-	// Verify:
-	assert.Equal(t, runner.RunCount, 1, "runner should have run one time")
+	return nil
 }
 
-func TestStartWithPeriodicRun(t *testing.T) {
-	// Setup:
-	runner := &MockRunner{RunCount: 0}
-	interval := 10 * time.Millisecond
+func (m *MockRunner) RunCount() int {
+	m.mutex.Lock()
+	defer m.mutex.Unlock()
+	return m.actualCalls
+}
+
+func TestStartWithSingleRun(t *testing.T) {
+	// Setup Initial Run Only:
+	expectedRuns := 1
+	runner := NewMockRunner(expectedRuns)
+	interval := 0 * time.Millisecond // forces a single run
 	ticker := task.NewTickerTask(interval, runner)
 
 	// Execute:
 	ticker.Start()
-	time.Sleep(25 * time.Millisecond)
-	ticker.Stop()
 
 	// Verify:
-	assert.Equal(t, runner.RunCount, 3, "runner should have run three times")
+	select {
+	case <-runner.ExpectationMet:
+	case <-time.After(250 * time.Millisecond):
+		assert.Failf(t, "Runner Calls", "expected %v calls, observed %v calls", expectedRuns, runner.RunCount())
+	}
+
+	// Verify No Additional Runs:
+	time.Sleep(50 * time.Millisecond)
+	assert.Equal(t, expectedRuns, runner.RunCount(), "runner should not run after Stop is called")
 }
 
-func TestStop(t *testing.T) {
-	// Setup:
-	runner := &MockRunner{RunCount: 0}
+func TestStartWithPeriodicRun(t *testing.T) {
+	// Setup Initial Run + One Periodic Run:
+	expectedRuns := 2
+	runner := NewMockRunner(expectedRuns)
 	interval := 10 * time.Millisecond
 	ticker := task.NewTickerTask(interval, runner)
 
 	// Execute:
 	ticker.Start()
-	time.Sleep(25 * time.Millisecond)
-	ticker.Stop()
-	time.Sleep(25 * time.Millisecond) // wait in case stop failed so additional runs can happen
 
-	// Verify:
-	assert.Equal(t, runner.RunCount, 3, "runner should have run three times")
+	// Verify Expected Runs:
+	select {
+	case <-runner.ExpectationMet:
+		ticker.Stop()
+	case <-time.After(250 * time.Millisecond):
+		assert.Failf(t, "Runner Calls", "expected %v calls, observed %v calls", expectedRuns, runner.RunCount())
+	}
+
+	// Verify No Additional Runs After Stop:
+	time.Sleep(50 * time.Millisecond)
+	assert.Equal(t, expectedRuns, runner.RunCount(), "runner should not run after Stop is called")
 }