Repository: prebid/prebid-server
Pull request #1995 (2021-09-13 12:06:01)

Adot: Add Usersync


Patch:
diff --git a/adapters/iqzone/iqzone.go b/adapters/iqzone/iqzone.go
index 190466b3..79dc04d8 100644
--- a/adapters/iqzone/iqzone.go
+++ b/adapters/iqzone/iqzone.go
@@ -23,19 +23,71 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters
 	return bidder, nil
 }
 
-func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
-	requestJSON, err := json.Marshal(request)
-	if err != nil {
-		return nil, []error{err}
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	var errs []error
+	var err error
+	var adapterRequests []*adapters.RequestData
+
+	reqCopy := *request
+	for _, imp := range request.Imp {
+		reqCopy.Imp = []openrtb2.Imp{imp}
+
+		var bidderExt adapters.ExtImpBidder
+		var iqzoneExt openrtb_ext.ImpExtIQZone
+
+		if err = json.Unmarshal(reqCopy.Imp[0].Ext, &bidderExt); err != nil {
+			return nil, append(errs, err)
+		}
+		if err = json.Unmarshal(bidderExt.Bidder, &iqzoneExt); err != nil {
+			return nil, append(errs, err)
+		}
+
+		finalyImpExt := reqCopy.Imp[0].Ext
+		if iqzoneExt.PlacementID != "" {
+			finalyImpExt, _ = json.Marshal(map[string]interface{}{
+				"bidder": map[string]interface{}{
+					"placementId": iqzoneExt.PlacementID,
+					"type":        "publisher",
+				},
+			})
+		} else if iqzoneExt.EndpointID != "" {
+			finalyImpExt, _ = json.Marshal(map[string]interface{}{
+				"bidder": map[string]interface{}{
+					"endpointId": iqzoneExt.EndpointID,
+					"type":       "network",
+				},
+			})
+		}
+
+		reqCopy.Imp[0].Ext = finalyImpExt
+
+		adapterReq, errors := a.makeRequest(&reqCopy)
+		if adapterReq != nil {
+			adapterRequests = append(adapterRequests, adapterReq)
+		}
+		errs = append(errs, errors...)
 	}
+	return adapterRequests, errs
+}
+
+func (a *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, []error) {
+	var errs []error
 
-	requestData := &adapters.RequestData{
-		Method: "POST",
-		Uri:    a.endpoint,
-		Body:   requestJSON,
+	reqJSON, err := json.Marshal(request)
+	if err != nil {
+		errs = append(errs, err)
+		return nil, errs
 	}
 
-	return []*adapters.RequestData{requestData}, nil
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+	return &adapters.RequestData{
+		Method:  "POST",
+		Uri:     a.endpoint,
+		Body:    reqJSON,
+		Headers: headers,
+	}, errs
 }
 
 func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
@@ -75,20 +127,16 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R
 }
 
 func getMediaTypeForImp(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) {
-	var mediaType openrtb_ext.BidType = ""
 	for _, imp := range imps {
 		if imp.ID == impID {
 			if imp.Banner != nil {
-				mediaType = openrtb_ext.BidTypeBanner
-				return mediaType, nil
+				return openrtb_ext.BidTypeBanner, nil
 			}
 			if imp.Banner == nil && imp.Video != nil {
-				mediaType = openrtb_ext.BidTypeVideo
-				return mediaType, nil
+				return openrtb_ext.BidTypeVideo, nil
 			}
 			if imp.Banner == nil && imp.Video == nil && imp.Native != nil {
-				mediaType = openrtb_ext.BidTypeNative
-				return mediaType, nil
+				return openrtb_ext.BidTypeNative, nil
 			}
 		}
 	}
diff --git a/config/config.go b/config/config.go
index 0286744d..f6f6f187 100644
--- a/config/config.go
+++ b/config/config.go
@@ -83,6 +83,8 @@ type Configuration struct {
 	AutoGenSourceTID bool `mapstructure:"auto_gen_source_tid"`
 	//When true, new bid id will be generated in seatbid[].bid[].ext.prebid.bidid and used in event urls instead
 	GenerateBidID bool `mapstructure:"generate_bid_id"`
+	// GenerateRequestID overrides the bidrequest.id in an AMP Request or an App Stored Request with a generated UUID if set to true. The default is false.
+	GenerateRequestID bool `mapstructure:"generate_request_id"`
 }
 
 const MIN_COOKIE_SIZE_BYTES = 500
@@ -944,6 +946,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("certificates_file", "")
 	v.SetDefault("auto_gen_source_tid", true)
 	v.SetDefault("generate_bid_id", false)
+	v.SetDefault("generate_request_id", false)
 
 	v.SetDefault("request_timeout_headers.request_time_in_queue", "")
 	v.SetDefault("request_timeout_headers.request_timeout_in_queue", "")
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 7a0afb0d..159e8a15 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -31,6 +31,7 @@ import (
 	"github.com/golang/glog"
 	"github.com/julienschmidt/httprouter"
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	"github.com/prebid/prebid-server/util/uuidutil"
 )
 
 const defaultAmpRequestTimeoutMillis = 900
@@ -45,6 +46,7 @@ type AmpResponse struct {
 // NewAmpEndpoint modifies the OpenRTB endpoint to handle AMP requests. This will basically modify the parsing
 // of the request, and the return value, using the OpenRTB machinery to handle everything in between.
 func NewAmpEndpoint(
+	uuidGenerator uuidutil.UUIDGenerator,
 	ex exchange.Exchange,
 	validator openrtb_ext.BidderParamValidator,
 	requestsById stored_requests.Fetcher,
@@ -69,6 +71,7 @@ func NewAmpEndpoint(
 	}
 
 	return httprouter.Handle((&endpointDeps{
+		uuidGenerator,
 		ex,
 		validator,
 		requestsById,
@@ -345,6 +348,15 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req
 		return
 	}
 
+	if deps.cfg.GenerateRequestID {
+		newBidRequestId, err := deps.uuidGenerator.Generate()
+		if err != nil {
+			errs = []error{err}
+			return
+		}
+		req.ID = newBidRequestId
+	}
+
 	if ampParams.Debug {
 		req.Test = 1
 	}
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 1d879443..579d6687 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -14,7 +14,7 @@ import (
 	"time"
 
 	"github.com/buger/jsonparser"
-	"github.com/evanphx/json-patch"
+	jsonpatch "github.com/evanphx/json-patch"
 	"github.com/gofrs/uuid"
 	"github.com/golang/glog"
 	"github.com/julienschmidt/httprouter"
@@ -36,6 +36,7 @@ import (
 	"github.com/prebid/prebid-server/usersync"
 	"github.com/prebid/prebid-server/util/httputil"
 	"github.com/prebid/prebid-server/util/iputil"
+	"github.com/prebid/prebid-server/util/uuidutil"
 	"golang.org/x/net/publicsuffix"
 	"golang.org/x/text/currency"
 )
@@ -49,6 +50,7 @@ var (
 )
 
 func NewEndpoint(
+	uuidGenerator uuidutil.UUIDGenerator,
 	ex exchange.Exchange,
 	validator openrtb_ext.BidderParamValidator,
 	requestsById stored_requests.Fetcher,
@@ -72,6 +74,7 @@ func NewEndpoint(
 	}
 
 	return httprouter.Handle((&endpointDeps{
+		uuidGenerator,
 		ex,
 		validator,
 		requestsById,
@@ -90,6 +93,7 @@ func NewEndpoint(
 }
 
 type endpointDeps struct {
+	uuidGenerator             uuidutil.UUIDGenerator
 	ex                        exchange.Exchange
 	paramsValidator           openrtb_ext.BidderParamValidator
 	storedReqFetcher          stored_requests.Fetcher
@@ -1354,21 +1358,34 @@ func (deps *endpointDeps) processStoredRequests(ctx context.Context, requestJson
 
 	// Apply the Stored BidRequest, if it exists
 	resolvedRequest := requestJson
-	if hasStoredBidRequest {
-		resolvedRequest, err = jsonpatch.MergePatch(storedRequests[storedBidRequestId], requestJson)
+
+	if deps.cfg.GenerateRequestID {
+		isAppRequest, err := checkIfAppRequest(requestJson)
 		if err != nil {
-			hasErr, Err := getJsonSyntaxError(requestJson)
-			if hasErr {
-				err = fmt.Errorf("Invalid JSON in Incoming Request: %s", Err)
-			} else {
-				hasErr, Err = getJsonSyntaxError(storedRequests[storedBidRequestId])
-				if hasErr {
-					err = fmt.Errorf("Invalid JSON in Stored Request with ID %s: %s", storedBidRequestId, Err)
-					err = fmt.Errorf("ext.prebid.storedrequest.id refers to Stored Request %s which contains Invalid JSON: %s", storedBidRequestId, Err)
-				}
-			}
 			return nil, nil, []error{err}
 		}
+		if isAppRequest && hasStoredBidRequest {
+			uuidPatch, err := generateUuidForBidRequest(deps.uuidGenerator)
+			if err != nil {
+				return nil, nil, []error{err}
+			}
+			uuidPatch, err = jsonpatch.MergePatch(storedRequests[storedBidRequestId], uuidPatch)
+			if err != nil {
+				errL := storedRequestErrorChecker(requestJson, storedRequests, storedBidRequestId)
+				return nil, nil, errL
+			}
+			resolvedRequest, err = jsonpatch.MergePatch(requestJson, uuidPatch)
+			if err != nil {
+				errL := storedRequestErrorChecker(requestJson, storedRequests, storedBidRequestId)
+				return nil, nil, errL
+			}
+		}
+	} else if hasStoredBidRequest {
+		resolvedRequest, err = jsonpatch.MergePatch(storedRequests[storedBidRequestId], requestJson)
+		if err != nil {
+			errL := storedRequestErrorChecker(requestJson, storedRequests, storedBidRequestId)
+			return nil, nil, errL
+		}
 	}
 
 	// Apply default aliases, if they are provided
@@ -1471,7 +1488,8 @@ type ImpExtPrebidData struct {
 // (e.g. malformed json, id not a string, etc).
 func getStoredRequestId(data []byte) (string, bool, error) {
 	// These keys must be kept in sync with openrtb_ext.ExtStoredRequest
-	value, dataType, _, err := jsonparser.Get(data, "ext", openrtb_ext.PrebidExtKey, "storedrequest", "id")
+	storedRequestId, dataType, _, err := jsonparser.Get(data, "ext", openrtb_ext.PrebidExtKey, "storedrequest", "id")
+
 	if dataType == jsonparser.NotExist {
 		return "", false, nil
 	}
@@ -1481,8 +1499,7 @@ func getStoredRequestId(data []byte) (string, bool, error) {
 	if dataType != jsonparser.String {
 		return "", true, errors.New("ext.prebid.storedrequest.id must be a string")
 	}
-
-	return string(value), true, nil
+	return string(storedRequestId), true, nil
 }
 
 // setIPImplicitly sets the IP address on bidReq, if it's not explicitly defined and we can figure it out.
@@ -1584,3 +1601,35 @@ func getAccountID(pub *openrtb2.Publisher) string {
 	}
 	return metrics.PublisherUnknown
 }
+
+func storedRequestErrorChecker(requestJson []byte, storedRequests map[string]json.RawMessage, storedBidRequestId string) []error {
+	if hasErr, syntaxErr := getJsonSyntaxError(requestJson); hasErr {
+		return []error{fmt.Errorf("Invalid JSON in Incoming Request: %s", syntaxErr)}
+	}
+	if hasErr, syntaxErr := getJsonSyntaxError(storedRequests[storedBidRequestId]); hasErr {
+		return []error{fmt.Errorf("ext.prebid.storedrequest.id refers to Stored Request %s which contains Invalid JSON: %s", storedBidRequestId, syntaxErr)}
+	}
+	return nil
+}
+
+func generateUuidForBidRequest(uuidGenerator uuidutil.UUIDGenerator) ([]byte, error) {
+	newBidRequestID, err := uuidGenerator.Generate()
+	if err != nil {
+		return nil, err
+	}
+	return []byte(`{"id":"` + newBidRequestID + `"}`), nil
+}
+
+func checkIfAppRequest(request []byte) (bool, error) {
+	requestApp, dataType, _, err := jsonparser.Get(request, "app")
+	if dataType == jsonparser.NotExist {
+		return false, nil
+	}
+	if err != nil {
+		return false, err
+	}
+	if requestApp != nil {
+		return true, nil
+	}
+	return false, nil
+}
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index c7e3dfbd..56c67c4d 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -20,6 +20,7 @@ import (
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/util/iputil"
+	"github.com/prebid/prebid-server/util/uuidutil"
 
 	"github.com/golang/glog"
 	"github.com/julienschmidt/httprouter"
@@ -37,6 +38,7 @@ import (
 var defaultRequestTimeout int64 = 5000
 
 func NewVideoEndpoint(
+	uuidGenerator uuidutil.UUIDGenerator,
 	ex exchange.Exchange,
 	validator openrtb_ext.BidderParamValidator,
 	requestsById stored_requests.Fetcher,
@@ -65,6 +67,7 @@ func NewVideoEndpoint(
 	videoEndpointRegexp := regexp.MustCompile(`[<>]`)
 
 	return httprouter.Handle((&endpointDeps{
+		uuidGenerator,
 		ex,
 		validator,
 		requestsById,
diff --git a/exchange/exchange.go b/exchange/exchange.go
index c612a35b..f6ec713f 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -24,6 +24,7 @@ import (
 	"github.com/prebid/prebid-server/prebid_cache_client"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/usersync"
+	"github.com/prebid/prebid-server/util/maputil"
 
 	"github.com/buger/jsonparser"
 	"github.com/gofrs/uuid"
@@ -966,9 +967,6 @@ func (e *exchange) makeBid(bids []*pbsOrtbBid, auc *auction, returnCreative bool
 }
 
 func makeBidExtJSON(ext json.RawMessage, prebid *openrtb_ext.ExtBidPrebid, impExtInfoMap map[string]ImpExtInfo, impId string) (json.RawMessage, error) {
-	// update existing bid.ext with prebid section
-	// if bid.ext.prebid already exists, it will be overwritten.
-	// if echoVideoAttrs set to true stored video attributes will be added to bid.ext.storedrequestattributes
 	var extMap map[string]interface{}
 
 	if len(ext) != 0 {
@@ -979,10 +977,22 @@ func makeBidExtJSON(ext json.RawMessage, prebid *openrtb_ext.ExtBidPrebid, impEx
 		extMap = make(map[string]interface{})
 	}
 
+	// ext.prebid
+	if prebid.Meta == nil && maputil.HasElement(extMap, "prebid", "meta") {
+		metaContainer := struct {
+			Prebid struct {
+				Meta openrtb_ext.ExtBidPrebidMeta `json:"meta"`
+			} `json:"prebid"`
+		}{}
+		if err := json.Unmarshal(ext, &metaContainer); err != nil {
+			return nil, fmt.Errorf("error validaing response from server, %s", err)
+		}
+		prebid.Meta = &metaContainer.Prebid.Meta
+	}
 	extMap[openrtb_ext.PrebidExtKey] = prebid
 
+	// ext.storedrequestattributes
 	if impExtInfo, ok := impExtInfoMap[impId]; ok && impExtInfo.EchoVideoAttrs {
-
 		videoData, _, _, err := jsonparser.Get(impExtInfo.StoredImp, "video")
 		if err != nil && err != jsonparser.KeyPathNotFoundError {
 			return nil, err
diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go
index ce6e25fd..7c73fb0d 100644
--- a/openrtb_ext/bid.go
+++ b/openrtb_ext/bid.go
@@ -1,6 +1,7 @@
 package openrtb_ext
 
 import (
+	"encoding/json"
 	"fmt"
 )
 
@@ -38,18 +39,20 @@ type ExtBidPrebidCacheBids struct {
 
 // ExtBidPrebidMeta defines the contract for bidresponse.seatbid.bid[i].ext.prebid.meta
 type ExtBidPrebidMeta struct {
-	AdvertiserDomains    []string `json:"advertiserDomains,omitempty"` // or advertiserDomain?
-	AdvertiserID         int      `json:"advertiserId,omitempty"`
-	AdvertiserName       string   `json:"advertiserName,omitempty"`
-	AgencyID             int      `json:"agencyId,omitempty"`
-	AgencyName           string   `json:"agencyName,omitempty"`
-	BrandID              int      `json:"brandId,omitempty"`
-	BrandName            string   `json:"brandName,omitempty"`
-	MediaType            string   `json:"mediaType,omitempty"`
-	NetworkID            int      `json:"networkId,omitempty"`
-	NetworkName          string   `json:"networkName,omitempty"`
-	PrimaryCategoryID    string   `json:"primaryCatId,omitempty"`
-	SecondaryCategoryIDs []string `json:"secondaryCatIds,omitempty"`
+	AdvertiserDomains    []string        `json:"advertiserDomains,omitempty"`
+	AdvertiserID         int             `json:"advertiserId,omitempty"`
+	AdvertiserName       string          `json:"advertiserName,omitempty"`
+	AgencyID             int             `json:"agencyId,omitempty"`
+	AgencyName           string          `json:"agencyName,omitempty"`
+	BrandID              int             `json:"brandId,omitempty"`
+	BrandName            string          `json:"brandName,omitempty"`
+	DemandSource         string          `json:"demandSource,omitempty"`
+	DChain               json.RawMessage `json:"dchain,omitempty"`
+	MediaType            string          `json:"mediaType,omitempty"`
+	NetworkID            int             `json:"networkId,omitempty"`
+	NetworkName          string          `json:"networkName,omitempty"`
+	PrimaryCategoryID    string          `json:"primaryCatId,omitempty"`
+	SecondaryCategoryIDs []string        `json:"secondaryCatIds,omitempty"`
 }
 
 // ExtBidPrebidVideo defines the contract for bidresponse.seatbid.bid[i].ext.prebid.video
diff --git a/openrtb_ext/imp_iqzone.go b/openrtb_ext/imp_iqzone.go
index 67a03376..e885625c 100644
--- a/openrtb_ext/imp_iqzone.go
+++ b/openrtb_ext/imp_iqzone.go
@@ -2,4 +2,5 @@ package openrtb_ext
 
 type ImpExtIQZone struct {
 	PlacementID string `json:"placementId"`
+	EndpointID  string `json:"endpointId"`
 }
diff --git a/router/router.go b/router/router.go
index 446d4280..17145b5a 100644
--- a/router/router.go
+++ b/router/router.go
@@ -12,6 +12,13 @@ import (
 	"strings"
 	"time"
 
+	"github.com/prebid/prebid-server/currency"
+	"github.com/prebid/prebid-server/endpoints/events"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/util/uuidutil"
+
+	"github.com/prebid/prebid-server/metrics"
+
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/adapters/adform"
 	"github.com/prebid/prebid-server/adapters/appnexus"
@@ -27,15 +34,11 @@ import (
 	"github.com/prebid/prebid-server/cache/filecache"
 	"github.com/prebid/prebid-server/cache/postgrescache"
 	"github.com/prebid/prebid-server/config"
-	"github.com/prebid/prebid-server/currency"
 	"github.com/prebid/prebid-server/endpoints"
-	"github.com/prebid/prebid-server/endpoints/events"
 	infoEndpoints "github.com/prebid/prebid-server/endpoints/info"
 	"github.com/prebid/prebid-server/endpoints/openrtb2"
-	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/exchange"
 	"github.com/prebid/prebid-server/gdpr"
-	"github.com/prebid/prebid-server/metrics"
 	metricsConf "github.com/prebid/prebid-server/metrics/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbs"
@@ -276,18 +279,18 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
 	}
 
 	theExchange := exchange.NewExchange(adapters, cacheClient, cfg, syncersByBidder, r.MetricsEngine, bidderInfos, gdprPerms, rateConvertor, categoriesFetcher)
-
-	openrtbEndpoint, err := openrtb2.NewEndpoint(theExchange, paramsValidator, fetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders)
+	var uuidGenerator uuidutil.UUIDRandomGenerator
+	openrtbEndpoint, err := openrtb2.NewEndpoint(uuidGenerator, theExchange, paramsValidator, fetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders)
 	if err != nil {
 		glog.Fatalf("Failed to create the openrtb2 endpoint handler. %v", err)
 	}
 
-	ampEndpoint, err := openrtb2.NewAmpEndpoint(theExchange, paramsValidator, ampFetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders)
+	ampEndpoint, err := openrtb2.NewAmpEndpoint(uuidGenerator, theExchange, paramsValidator, ampFetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders)
 	if err != nil {
 		glog.Fatalf("Failed to create the amp endpoint handler. %v", err)
 	}
 
-	videoEndpoint, err := openrtb2.NewVideoEndpoint(theExchange, paramsValidator, fetcher, videoFetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders, cacheClient)
+	videoEndpoint, err := openrtb2.NewVideoEndpoint(uuidGenerator, theExchange, paramsValidator, fetcher, videoFetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders, cacheClient)
 	if err != nil {
 		glog.Fatalf("Failed to create the video endpoint handler. %v", err)
 	}
@@ -337,7 +340,9 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
 
 func applyBidderInfoConfigOverrides(bidderInfos config.BidderInfos, adaptersCfg map[string]config.Adapter) error {
 	for bidderName, bidderInfo := range bidderInfos {
-		if adapterCfg, exists := adaptersCfg[bidderName]; exists {
+		// bidder name from bidderInfos is case-sensitive, but bidder name from adaptersCfg
+		// is always expressed as lower case. need to adapt for the difference here.
+		if adapterCfg, exists := adaptersCfg[strings.ToLower(bidderName)]; exists {
 			bidderInfo.Syncer = adapterCfg.Syncer.Override(bidderInfo.Syncer)
 
 			// validate and try to apply the legacy usersync_url configuration in attempt to provide
diff --git a/static/bidder-info/adot.yaml b/static/bidder-info/adot.yaml
index 529ff3ae..6a7aca06 100644
--- a/static/bidder-info/adot.yaml
+++ b/static/bidder-info/adot.yaml
@@ -12,3 +12,7 @@ capabilities:
       - banner
       - video
       - native
+userSync:
+  redirect:
+    url: "https://sync.adotmob.com/cookie/pbs?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r={{.RedirectURL}}"
+    userMacro: "{amob_user_id}"
\ No newline at end of file
diff --git a/static/bidder-params/iqzone.json b/static/bidder-params/iqzone.json
index 38e98c53..5113dfaf 100644
--- a/static/bidder-params/iqzone.json
+++ b/static/bidder-params/iqzone.json
@@ -1,15 +1,21 @@
 {
     "$schema": "http://json-schema.org/draft-04/schema#",
-    "title": "IQZone Adapter Params",
-    "description": "A schema which validates params accepted by the IQZone adapter",
+    "title": "IQzone Adapter Params",
+    "description": "A schema which validates params accepted by the IQzone adapter",
     "type": "object",
   
     "properties": {
       "placementId": {
         "type": "string",
         "description": "Placement ID"
+      },
+      "endpointId": {
+        "type": "string",
+        "description": "Endpoint ID"
       }
     },
-  
-    "required": ["placementId"]
+    "oneOf": [
+      { "required": ["placementId"] },
+      { "required": ["endpointId"] }
+    ]
   }
\ No newline at end of file
diff --git a/util/maputil/maputil.go b/util/maputil/maputil.go
index 595e9321..49df0772 100644
--- a/util/maputil/maputil.go
+++ b/util/maputil/maputil.go
@@ -20,7 +20,7 @@ func ReadEmbeddedSlice(m map[string]interface{}, k string) ([]interface{}, bool)
 	return nil, false
 }
 
-// ReadEmbeddedString reads element k from the map m as a string
+// ReadEmbeddedString reads element k from the map m as a string.
 func ReadEmbeddedString(m map[string]interface{}, k string) (string, bool) {
 	if v, ok := m[k]; ok {
 		vCasted, ok := v.(string)
@@ -28,3 +28,23 @@ func ReadEmbeddedString(m map[string]interface{}, k string) (string, bool) {
 	}
 	return "", false
 }
+
+// HasElement returns true if nested element k exists.
+func HasElement(m map[string]interface{}, k ...string) bool {
+	exists := false
+	kLastIndex := len(k) - 1
+
+	for i, k := range k {
+		isLastKey := i == kLastIndex
+
+		if isLastKey {
+			_, exists = m[k]
+		} else {
+			if m, exists = ReadEmbeddedMap(m, k); !exists {
+				break
+			}
+		}
+	}
+
+	return exists
+}
diff --git a/util/uuidutil/uuidutil.go b/util/uuidutil/uuidutil.go
new file mode 100644
index 00000000..6e4848f6
--- /dev/null
+++ b/util/uuidutil/uuidutil.go
@@ -0,0 +1,19 @@
+package uuidutil
+
+import (
+	"github.com/gofrs/uuid"
+)
+
+type UUIDGenerator interface {
+	Generate() (string, error)
+}
+
+type UUIDRandomGenerator struct{}
+
+func (UUIDRandomGenerator) Generate() (string, error) {
+	id, err := uuid.NewV4()
+	if err != nil {
+		return "", err
+	}
+	return id.String(), nil
+}


Test patch:
diff --git a/adapters/iqzone/iqzonetest/exemplary/endpointId.json b/adapters/iqzone/iqzonetest/exemplary/endpointId.json
new file mode 100644
index 00000000..79887443
--- /dev/null
+++ b/adapters/iqzone/iqzonetest/exemplary/endpointId.json
@@ -0,0 +1,133 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "endpointId": "test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://smartssp-us-east.iqzone.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "endpointId": "test",
+                                    "type": "network"
+                                }
+                            }
+                        }
+                    ],
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://smartssp-us-east.iqzone.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "banner"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "iqzone"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://smartssp-us-east.iqzone.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250,
+                        "ext": {
+                            "prebid": {
+                                "type": "banner"
+                            }
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/iqzone/iqzonetest/exemplary/simple-banner.json b/adapters/iqzone/iqzonetest/exemplary/simple-banner.json
index f816191e..12f97e1b 100644
--- a/adapters/iqzone/iqzonetest/exemplary/simple-banner.json
+++ b/adapters/iqzone/iqzonetest/exemplary/simple-banner.json
@@ -57,7 +57,8 @@
                             },
                             "ext": {
                                 "bidder": {
-                                    "placementId": "test"
+                                    "placementId": "test",
+                                    "type": "publisher"
                                 }
                             }
                         }
diff --git a/adapters/iqzone/iqzonetest/exemplary/simple-native.json b/adapters/iqzone/iqzonetest/exemplary/simple-native.json
index 819e14bf..967a846d 100644
--- a/adapters/iqzone/iqzonetest/exemplary/simple-native.json
+++ b/adapters/iqzone/iqzonetest/exemplary/simple-native.json
@@ -41,7 +41,8 @@
                             },
                             "ext": {
                                 "bidder": {
-                                    "placementId": "test"
+                                    "placementId": "test",
+                                    "type": "publisher"
                                 }
                             }
                         }
diff --git a/adapters/iqzone/iqzonetest/exemplary/simple-video.json b/adapters/iqzone/iqzonetest/exemplary/simple-video.json
index 16e6f4c7..7e2b7971 100644
--- a/adapters/iqzone/iqzonetest/exemplary/simple-video.json
+++ b/adapters/iqzone/iqzonetest/exemplary/simple-video.json
@@ -63,7 +63,8 @@
                             },
                             "ext": {
                                 "bidder": {
-                                    "placementId": "test"
+                                    "placementId": "test",
+                                    "type": "publisher"
                                 }
                             }
                         }
diff --git a/adapters/iqzone/iqzonetest/exemplary/simple-web-banner.json b/adapters/iqzone/iqzonetest/exemplary/simple-web-banner.json
index 968c0305..4d69ef41 100644
--- a/adapters/iqzone/iqzonetest/exemplary/simple-web-banner.json
+++ b/adapters/iqzone/iqzonetest/exemplary/simple-web-banner.json
@@ -57,7 +57,8 @@
                             },
                             "ext": {
                                 "bidder": {
-                                    "placementId": "test"
+                                    "placementId": "test",
+                                    "type": "publisher"
                                 }
                             }
                         }
diff --git a/adapters/iqzone/iqzonetest/supplemental/bad_media_type.json b/adapters/iqzone/iqzonetest/supplemental/bad_media_type.json
index 3d192310..4dddd568 100644
--- a/adapters/iqzone/iqzonetest/supplemental/bad_media_type.json
+++ b/adapters/iqzone/iqzonetest/supplemental/bad_media_type.json
@@ -30,7 +30,8 @@
                         "id": "test-imp-id",
                         "ext": {
                             "bidder": {
-                                "placementId": "test"
+                                "placementId": "test",
+                                "type": "publisher"
                             }
                         }
                     }
diff --git a/adapters/iqzone/iqzonetest/supplemental/bad_response.json b/adapters/iqzone/iqzonetest/supplemental/bad_response.json
index ad420705..74c603b4 100644
--- a/adapters/iqzone/iqzonetest/supplemental/bad_response.json
+++ b/adapters/iqzone/iqzonetest/supplemental/bad_response.json
@@ -54,7 +54,8 @@
                         },
                         "ext": {
                             "bidder": {
-                                "placementId": "test"
+                                "placementId": "test",
+                                "type": "publisher"
                             }
                         }
                     }
diff --git a/adapters/iqzone/iqzonetest/supplemental/status-204.json b/adapters/iqzone/iqzonetest/supplemental/status-204.json
index 7c2f21c0..8ef22bda 100644
--- a/adapters/iqzone/iqzonetest/supplemental/status-204.json
+++ b/adapters/iqzone/iqzonetest/supplemental/status-204.json
@@ -54,7 +54,8 @@
                         },
                         "ext": {
                             "bidder": {
-                                "placementId": "test"
+                                "placementId": "test",
+                                "type": "publisher"
                             }
                         }
                     }
diff --git a/adapters/iqzone/iqzonetest/supplemental/status-not-200.json b/adapters/iqzone/iqzonetest/supplemental/status-not-200.json
index 22160ac7..2cb4ba98 100644
--- a/adapters/iqzone/iqzonetest/supplemental/status-not-200.json
+++ b/adapters/iqzone/iqzonetest/supplemental/status-not-200.json
@@ -18,7 +18,7 @@
                 },
                 "ext": {
                     "bidder": {
-                        "TagID": "test"
+                        "placementId": "test"
                     }
                 }
             }
@@ -54,7 +54,8 @@
                         },
                         "ext": {
                             "bidder": {
-                                "TagID": "test"
+                                "placementId": "test",
+                                "type": "publisher"
                             }
                         }
                     }
diff --git a/adapters/iqzone/params_test.go b/adapters/iqzone/params_test.go
index deb4af2e..c06f5e7f 100644
--- a/adapters/iqzone/params_test.go
+++ b/adapters/iqzone/params_test.go
@@ -36,10 +36,12 @@ func TestInvalidParams(t *testing.T) {
 var validParams = []string{
 	`{"placementId": "test"}`,
 	`{"placementId": "1"}`,
+	`{"endpointId": "test"}`,
+	`{"endpointId": "1"}`,
 }
 
 var invalidParams = []string{
 	`{"placementId": 42}`,
-	`{}`,
-	`{"someOtherParam": "value"}`,
+	`{"endpointId": 42}`,
+	`{"placementId": "1", "endpointId": "1"}`,
 }
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 667c4aef..ae3c036a 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -11,6 +11,8 @@ import (
 	"strconv"
 	"testing"
 
+	"github.com/julienschmidt/httprouter"
+	"github.com/mxmCherry/openrtb/v15/openrtb2"
 	"github.com/prebid/prebid-server/analytics"
 	analyticsConf "github.com/prebid/prebid-server/analytics/config"
 	"github.com/prebid/prebid-server/config"
@@ -19,7 +21,6 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
 
-	"github.com/mxmCherry/openrtb/v15/openrtb2"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -38,6 +39,7 @@ func TestGoodAmpRequests(t *testing.T) {
 	}
 
 	endpoint, _ := NewAmpEndpoint(
+		fakeUUIDGenerator{},
 		&mockAmpExchange{},
 		newParamsValidator(t),
 		&mockAmpStoredReqFetcher{goodRequests},
@@ -91,6 +93,7 @@ func TestAMPPageInfo(t *testing.T) {
 	exchange := &mockAmpExchange{}
 
 	endpoint, _ := NewAmpEndpoint(
+		fakeUUIDGenerator{},
 		exchange,
 		newParamsValidator(t),
 		&mockAmpStoredReqFetcher{stored},
@@ -188,6 +191,7 @@ func TestGDPRConsent(t *testing.T) {
 		// Build Exchange Endpoint
 		mockExchange := &mockAmpExchange{}
 		endpoint, _ := NewAmpEndpoint(
+			fakeUUIDGenerator{},
 			mockExchange,
 			newParamsValidator(t),
 			&mockAmpStoredReqFetcher{stored},
@@ -340,6 +344,7 @@ func TestCCPAConsent(t *testing.T) {
 		// Build Exchange Endpoint
 		mockExchange := &mockAmpExchange{}
 		endpoint, _ := NewAmpEndpoint(
+			fakeUUIDGenerator{},
 			mockExchange,
 			newParamsValidator(t),
 			&mockAmpStoredReqFetcher{stored},
@@ -450,6 +455,7 @@ func TestConsentWarnings(t *testing.T) {
 			mockExchange = &mockAmpExchange{}
 		}
 		endpoint, _ := NewAmpEndpoint(
+			fakeUUIDGenerator{},
 			mockExchange,
 			newParamsValidator(t),
 			&mockAmpStoredReqFetcher{stored},
@@ -542,6 +548,7 @@ func TestNewAndLegacyConsentBothProvided(t *testing.T) {
 		// Build Exchange Endpoint
 		mockExchange := &mockAmpExchange{}
 		endpoint, _ := NewAmpEndpoint(
+			fakeUUIDGenerator{},
 			mockExchange,
 			newParamsValidator(t),
 			&mockAmpStoredReqFetcher{stored},
@@ -593,6 +600,7 @@ func TestAMPSiteExt(t *testing.T) {
 	}
 	exchange := &mockAmpExchange{}
 	endpoint, _ := NewAmpEndpoint(
+		fakeUUIDGenerator{},
 		exchange,
 		newParamsValidator(t),
 		&mockAmpStoredReqFetcher{stored},
@@ -629,6 +637,7 @@ func TestAmpBadRequests(t *testing.T) {
 	}
 
 	endpoint, _ := NewAmpEndpoint(
+		fakeUUIDGenerator{},
 		&mockAmpExchange{},
 		newParamsValidator(t),
 		&mockAmpStoredReqFetcher{badRequests},
@@ -659,6 +668,7 @@ func TestAmpDebug(t *testing.T) {
 	}
 
 	endpoint, _ := NewAmpEndpoint(
+		fakeUUIDGenerator{},
 		&mockAmpExchange{},
 		newParamsValidator(t),
 		&mockAmpStoredReqFetcher{requests},
@@ -731,6 +741,7 @@ func TestQueryParamOverrides(t *testing.T) {
 	}
 
 	endpoint, _ := NewAmpEndpoint(
+		fakeUUIDGenerator{},
 		&mockAmpExchange{},
 		newParamsValidator(t),
 		&mockAmpStoredReqFetcher{requests},
@@ -883,6 +894,7 @@ func (s formatOverrideSpec) execute(t *testing.T) {
 	}
 
 	endpoint, _ := NewAmpEndpoint(
+		fakeUUIDGenerator{},
 		&mockAmpExchange{},
 		newParamsValidator(t),
 		&mockAmpStoredReqFetcher{requests},
@@ -1249,31 +1261,8 @@ func TestBuildAmpObject(t *testing.T) {
 	recorder := httptest.NewRecorder()
 
 	for _, test := range testCases {
-
 		// Set up test, declare a new mock logger every time
-		actualAmpObject := new(analytics.AmpObject)
-
-		logger := newMockLogger(actualAmpObject)
-
-		mockAmpFetcher := &mockAmpStoredReqFetcher{
-			data: map[string]json.RawMessage{
-				test.inTagId: json.RawMessage(test.inStoredRequest),
-			},
-		}
-
-		endpoint, _ := NewAmpEndpoint(
-			&mockAmpExchange{},
-			newParamsValidator(t),
-			mockAmpFetcher,
-			empty_fetcher.EmptyFetcher{},
-			&config.Configuration{MaxRequestSize: maxSize},
-			&metricsConfig.DummyMetricsEngine{},
-			logger,
-			map[string]string{},
-			[]byte{},
-			openrtb_ext.BuildBidderMap(),
-		)
-
+		actualAmpObject, endpoint := AmpObjectTestSetup(t, test.inTagId, test.inStoredRequest, false)
 		// Run test
 		endpoint(recorder, request, nil)
 
@@ -1286,3 +1275,76 @@ func TestBuildAmpObject(t *testing.T) {
 		assert.Equalf(t, test.expectedAmpObject.Origin, actualAmpObject.Origin, "Amp Object Origin field doesn't match expected: %s\n", test.description)
 	}
 }
+
+func TestIdGeneration(t *testing.T) {
+	uuid := "foo"
+
+	testCases := []struct {
+		description            string
+		givenInStoredRequest   json.RawMessage
+		givenGenerateRequestID bool
+		expectedID             string
+	}{
+		{
+			description:            "The givenGenerateRequestID flag is set to true, so even though the stored amp request already has an id, we should still generate a new uuid",
+			givenInStoredRequest:   json.RawMessage(`{"id":"ThisID","site":{"page":"prebid.org"},"imp":[{"id":"some-imp-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"appnexus":{"placementId":1}}}],"tmax":1}`),
+			givenGenerateRequestID: true,
+			expectedID:             uuid,
+		},
+		{
+			description:            "The givenGenerateRequestID flag is set to true and the stored amp request ID is blank, so we should generate a new uuid for the request",
+			givenInStoredRequest:   json.RawMessage(`{"id":"","site":{"page":"prebid.org"},"imp":[{"id":"some-imp-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"appnexus":{"placementId":1}}}],"tmax":1}`),
+			givenGenerateRequestID: true,
+			expectedID:             uuid,
+		},
+		{
+			description:            "The givenGenerateRequestID flag is false, so the ID shouldn't change",
+			givenInStoredRequest:   json.RawMessage(`{"id":"ThisID","site":{"page":"prebid.org"},"imp":[{"id":"some-imp-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"appnexus":{"placementId":1}}}],"tmax":1}`),
+			givenGenerateRequestID: false,
+			expectedID:             "ThisID",
+		},
+		{
+			description:            "The givenGenerateRequestID flag is true, and if the id field isn't included in the stored request",
+			givenInStoredRequest:   json.RawMessage(`{"site":{"page":"prebid.org"},"imp":[{"id":"some-imp-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"appnexus":{"placementId":1}}}],"tmax":1}`),
+			givenGenerateRequestID: true,
+			expectedID:             uuid,
+		},
+	}
+
+	request := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=test", nil)
+	recorder := httptest.NewRecorder()
+
+	for _, test := range testCases {
+		// Set up and run test
+		actualAmpObject, endpoint := AmpObjectTestSetup(t, "test", test.givenInStoredRequest, test.givenGenerateRequestID)
+		endpoint(recorder, request, nil)
+
+		assert.Equalf(t, test.expectedID, actualAmpObject.Request.ID, "Bid Request ID is incorrect: %s\n", test.description)
+	}
+}
+
+func AmpObjectTestSetup(t *testing.T, inTagId string, inStoredRequest json.RawMessage, generateRequestID bool) (*analytics.AmpObject, httprouter.Handle) {
+	actualAmpObject := analytics.AmpObject{}
+	logger := newMockLogger(&actualAmpObject)
+
+	mockAmpFetcher := &mockAmpStoredReqFetcher{
+		data: map[string]json.RawMessage{
+			inTagId: json.RawMessage(inStoredRequest),
+		},
+	}
+
+	endpoint, _ := NewAmpEndpoint(
+		fakeUUIDGenerator{id: "foo", err: nil},
+		&mockAmpExchange{},
+		newParamsValidator(t),
+		mockAmpFetcher,
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{MaxRequestSize: maxSize, GenerateRequestID: generateRequestID},
+		&metricsConfig.DummyMetricsEngine{},
+		logger,
+		map[string]string{},
+		[]byte{},
+		openrtb_ext.BuildBidderMap(),
+	)
+	return &actualAmpObject, endpoint
+}
diff --git a/endpoints/openrtb2/auction_benchmark_test.go b/endpoints/openrtb2/auction_benchmark_test.go
index ecc0554f..f2e0fb2a 100644
--- a/endpoints/openrtb2/auction_benchmark_test.go
+++ b/endpoints/openrtb2/auction_benchmark_test.go
@@ -87,6 +87,7 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) {
 	)
 
 	endpoint, _ := NewEndpoint(
+		fakeUUIDGenerator{},
 		exchange,
 		paramValidator,
 		empty_fetcher.EmptyFetcher{},
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 31c3c109..ebf18994 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -430,6 +430,7 @@ func TestExplicitUserId(t *testing.T) {
 	})
 
 	endpoint, _ := NewEndpoint(
+		fakeUUIDGenerator{},
 		ex,
 		newParamsValidator(t),
 		empty_fetcher.EmptyFetcher{},
@@ -464,6 +465,7 @@ func doRequest(t *testing.T, test testCase) (int, string) {
 	mockExchange := newMockBidExchange(test.Config.MockBidder, test.Config.CurrencyRates)
 
 	endpoint, _ := NewEndpoint(
+		fakeUUIDGenerator{},
 		mockExchange,
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -534,6 +536,7 @@ func doBadAliasRequest(t *testing.T, filename string, expectMsg string) {
 	// NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it.
 	// As a side effect this gives us some coverage of the go_metrics piece of the metrics engine.
 	endpoint, _ := NewEndpoint(
+		fakeUUIDGenerator{},
 		&nobidExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -584,6 +587,7 @@ func TestNilExchange(t *testing.T) {
 	// NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it.
 	// As a side effect this gives us some coverage of the go_metrics piece of the metrics engine.
 	_, err := NewEndpoint(
+		fakeUUIDGenerator{},
 		nil,
 		newParamsValidator(t),
 		empty_fetcher.EmptyFetcher{},
@@ -604,6 +608,7 @@ func TestNilValidator(t *testing.T) {
 	// NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it.
 	// As a side effect this gives us some coverage of the go_metrics piece of the metrics engine.
 	_, err := NewEndpoint(
+		fakeUUIDGenerator{},
 		&nobidExchange{},
 		nil,
 		empty_fetcher.EmptyFetcher{},
@@ -625,6 +630,7 @@ func TestExchangeError(t *testing.T) {
 	// NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it.
 	// As a side effect this gives us some coverage of the go_metrics piece of the metrics engine.
 	endpoint, _ := NewEndpoint(
+		fakeUUIDGenerator{},
 		&brokenExchange{},
 		newParamsValidator(t),
 		empty_fetcher.EmptyFetcher{},
@@ -747,6 +753,7 @@ func TestImplicitIPsEndToEnd(t *testing.T) {
 			},
 		}
 		endpoint, _ := NewEndpoint(
+			fakeUUIDGenerator{},
 			exchange,
 			newParamsValidator(t),
 			&mockStoredReqFetcher{},
@@ -941,6 +948,7 @@ func TestImplicitDNTEndToEnd(t *testing.T) {
 	for _, test := range testCases {
 		exchange := &nobidExchange{}
 		endpoint, _ := NewEndpoint(
+			fakeUUIDGenerator{},
 			exchange,
 			newParamsValidator(t),
 			&mockStoredReqFetcher{},
@@ -1094,6 +1102,7 @@ func TestParseImpInfoMultipleImpressions(t *testing.T) {
 // Test the stored request functionality
 func TestStoredRequests(t *testing.T) {
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		&nobidExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -1140,10 +1149,81 @@ func TestStoredRequests(t *testing.T) {
 	}
 }
 
+func TestStoredRequestGenerateUuid(t *testing.T) {
+	uuid := "foo"
+
+	deps := &endpointDeps{
+		fakeUUIDGenerator{id: "foo", err: nil},
+		&nobidExchange{},
+		newParamsValidator(t),
+		&mockStoredReqFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{MaxRequestSize: maxSize},
+		&metricsConfig.DummyMetricsEngine{},
+		analyticsConf.NewPBSAnalytics(&config.Analytics{}),
+		map[string]string{},
+		false,
+		[]byte{},
+		openrtb_ext.BuildBidderMap(),
+		nil,
+		nil,
+		hardcodedResponseIPValidator{response: true},
+	}
+
+	req := &openrtb2.BidRequest{}
+
+	testCases := []struct {
+		description            string
+		givenRawData           string
+		givenGenerateRequestID bool
+		expectedID             string
+	}{
+		{
+			description:            "GenerateRequestID is true, rawData is an app request, and stored bid we should generate uuid",
+			givenRawData:           testStoredRequestsUuid[2],
+			givenGenerateRequestID: true,
+			expectedID:             uuid,
+		},
+		{
+			description:            "GenerateRequestID is true, rawData is an app request, but no stored bid, we should not generate uuid",
+			givenRawData:           testStoredRequestsUuid[0],
+			givenGenerateRequestID: true,
+			expectedID:             "ThisID",
+		},
+		{
+			description:            "GenerateRequestID is false so we should not generate uuid",
+			givenRawData:           testStoredRequestsUuid[0],
+			givenGenerateRequestID: false,
+			expectedID:             "ThisID",
+		},
+		{
+			description:            "GenerateRequestID is true, but rawData is a site request, we should not generate uuid",
+			givenRawData:           testStoredRequestsUuid[1],
+			givenGenerateRequestID: true,
+			expectedID:             "ThisID",
+		},
+	}
+
+	for _, test := range testCases {
+		deps.cfg.GenerateRequestID = test.givenGenerateRequestID
+		impInfo, errs := parseImpInfo([]byte(test.givenRawData))
+		assert.Empty(t, errs, test.description)
+		newRequest, _, errList := deps.processStoredRequests(context.Background(), json.RawMessage(test.givenRawData), impInfo)
+		assert.Empty(t, errList, test.description)
+
+		if err := json.Unmarshal(newRequest, req); err != nil {
+			t.Errorf("processStoredRequests Error: %s", err.Error())
+		}
+		assert.Equalf(t, test.expectedID, req.ID, "The Bid Request ID is incorrect: %s\n", test.description)
+	}
+}
+
 // TestOversizedRequest makes sure we behave properly when the request size exceeds the configured max.
 func TestOversizedRequest(t *testing.T) {
 	reqBody := validRequest(t, "site.json")
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		&nobidExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -1179,6 +1259,7 @@ func TestOversizedRequest(t *testing.T) {
 func TestRequestSizeEdgeCase(t *testing.T) {
 	reqBody := validRequest(t, "site.json")
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		&nobidExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -1213,6 +1294,7 @@ func TestRequestSizeEdgeCase(t *testing.T) {
 // TestNoEncoding prevents #231.
 func TestNoEncoding(t *testing.T) {
 	endpoint, _ := NewEndpoint(
+		fakeUUIDGenerator{},
 		&mockExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -1288,6 +1370,7 @@ func TestExplicitAMP(t *testing.T) {
 // TestContentType prevents #328
 func TestContentType(t *testing.T) {
 	endpoint, _ := NewEndpoint(
+		fakeUUIDGenerator{},
 		&mockExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -1607,6 +1690,7 @@ func TestValidateImpExt(t *testing.T) {
 	}
 
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		&nobidExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -1653,6 +1737,7 @@ func validRequest(t *testing.T, filename string) string {
 
 func TestCurrencyTrunc(t *testing.T) {
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		&nobidExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -1697,6 +1782,7 @@ func TestCurrencyTrunc(t *testing.T) {
 
 func TestCCPAInvalid(t *testing.T) {
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		&nobidExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -1745,6 +1831,7 @@ func TestCCPAInvalid(t *testing.T) {
 
 func TestNoSaleInvalid(t *testing.T) {
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		&nobidExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -1796,6 +1883,7 @@ func TestValidateSourceTID(t *testing.T) {
 	}
 
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		&nobidExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -1837,6 +1925,7 @@ func TestValidateSourceTID(t *testing.T) {
 
 func TestSChainInvalid(t *testing.T) {
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		&nobidExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -2056,6 +2145,7 @@ func TestValidateAndFillSourceTID(t *testing.T) {
 
 func TestEidPermissionsInvalid(t *testing.T) {
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		&nobidExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -2307,6 +2397,7 @@ func TestIOS14EndToEnd(t *testing.T) {
 	exchange := &nobidExchange{}
 
 	endpoint, _ := NewEndpoint(
+		fakeUUIDGenerator{},
 		exchange,
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -2334,6 +2425,7 @@ func TestIOS14EndToEnd(t *testing.T) {
 func TestAuctionWarnings(t *testing.T) {
 	reqBody := validRequest(t, "us-privacy-invalid.json")
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		&warningsCheckExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -2373,6 +2465,7 @@ func TestAuctionWarnings(t *testing.T) {
 func TestParseRequestParseImpInfoError(t *testing.T) {
 	reqBody := validRequest(t, "imp-info-invalid.json")
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		&warningsCheckExchange{},
 		newParamsValidator(t),
 		&mockStoredReqFetcher{},
@@ -3452,6 +3545,105 @@ var testStoredImps = []string{
 	``,
 }
 
+var testStoredRequestsUuid = []string{
+	`{
+		"id": "ThisID",
+		"app": {
+			"id": "123"
+		},
+		"imp": [
+			{
+				"video":{
+					"h":300,
+					"w":200
+				},
+				"ext": {
+					"prebid": {
+						"storedrequest": {
+							"id": "1"
+						},
+						"options": {
+							"echovideoattrs": true
+						}
+					}
+				}
+			}
+		],
+		"ext": {
+			"prebid": {
+				"cache": {
+					"markup": 1
+				},
+				"targeting": {
+				}
+			}
+		}
+	}`,
+	`{
+		"id": "ThisID",
+		"site": {
+			"page": "prebid.org"
+		},
+		"imp": [
+			{
+				"id": "adUnit2",
+				"ext": {
+					"prebid": {
+						"storedrequest": {
+							"id": "1"
+						},
+						"options": {
+							"echovideoattrs": true
+						}
+					},
+					"appnexus": {
+						"placementId": "def",
+						"trafficSourceCode": "mysite.com",
+						"reserve": null
+					},
+					"rubicon": null
+				}
+			}
+		],
+		"ext": {
+			"prebid": {
+				"cache": {
+					"markup": 1
+				},
+				"targeting": {
+				}
+			}
+		}
+	}`,
+	`{
+		"id": "ThisID",
+		"app": {
+			"id": "123"
+		},
+		"imp": [
+			{
+				"ext": {
+					"prebid": {
+						"storedrequest": {
+							"id": "2"
+						},
+						"options": {
+							"echovideoattrs": false
+						}
+					}
+				}
+			}
+		],
+		"ext": {
+			"prebid": {
+				"storedrequest": {
+					"id": "2"
+				}
+			}
+		}
+	}`,
+}
+
 type mockStoredReqFetcher struct {
 }
 
@@ -3514,3 +3706,12 @@ type hardcodedResponseIPValidator struct {
 func (v hardcodedResponseIPValidator) IsValid(net.IP, iputil.IPVersion) bool {
 	return v.response
 }
+
+type fakeUUIDGenerator struct {
+	id  string
+	err error
+}
+
+func (f fakeUUIDGenerator) Generate() (string, error) {
+	return f.id, f.err
+}
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index b00467d9..26d9f028 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -1210,6 +1210,7 @@ func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *m
 	metrics := metrics.NewMetrics(gometrics.NewRegistry(), openrtb_ext.CoreBidderNames(), config.DisabledMetrics{}, nil)
 
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		ex,
 		newParamsValidator(t),
 		&mockVideoStoredReqFetcher{},
@@ -1252,6 +1253,7 @@ func (m *mockAnalyticsModule) LogNotificationEventObject(ne *analytics.Notificat
 
 func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps {
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		ex,
 		newParamsValidator(t),
 		&mockVideoStoredReqFetcher{},
@@ -1274,6 +1276,7 @@ func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps {
 
 func mockDepsAppendBidderNames(t *testing.T, ex *mockExchangeAppendBidderNames) *endpointDeps {
 	deps := &endpointDeps{
+		fakeUUIDGenerator{},
 		ex,
 		newParamsValidator(t),
 		&mockVideoStoredReqFetcher{},
@@ -1296,6 +1299,7 @@ func mockDepsAppendBidderNames(t *testing.T, ex *mockExchangeAppendBidderNames)
 
 func mockDepsNoBids(t *testing.T, ex *mockExchangeVideoNoBids) *endpointDeps {
 	edep := &endpointDeps{
+		fakeUUIDGenerator{},
 		ex,
 		newParamsValidator(t),
 		&mockVideoStoredReqFetcher{},
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 706d118d..ed378e7b 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -3491,11 +3491,19 @@ func TestMakeBidExtJSON(t *testing.T) {
 
 	testCases := []aTest{
 		{
-			description:        "Valid extension, non empty extBidPrebid and valid imp ext info",
+			description:        "Valid extension, non empty extBidPrebid, valid imp ext info, meta from adapter",
 			ext:                json.RawMessage(`{"video":{"h":100}}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video"), Meta: &openrtb_ext.ExtBidPrebidMeta{BrandName: "foo"}},
+			impExtInfo:         map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`)}},
+			expectedBidExt:     `{"prebid":{"meta": {"brandName": "foo"}, "type":"video"},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]},"video":{"h":100}}`,
+			expectedErrMessage: "",
+		},
+		{
+			description:        "Valid extension, non empty extBidPrebid, valid imp ext info, meta from response",
+			ext:                json.RawMessage(`{"video":{"h":100},"prebid":{"meta": {"brandName": "foo"}}}`),
 			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")},
 			impExtInfo:         map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`)}},
-			expectedBidExt:     `{"prebid":{"type":"video"},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]},"video":{"h":100}}`,
+			expectedBidExt:     `{"prebid":{"meta": {"brandName": "foo"}, "type":"video"},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]},"video":{"h":100}}`,
 			expectedErrMessage: "",
 		},
 		{
@@ -3554,6 +3562,54 @@ func TestMakeBidExtJSON(t *testing.T) {
 			expectedBidExt:     `{"prebid":{"type":"video"}, "storedrequestattributes":{"h":480,"mimes":["video/mp4"]}}`,
 			expectedErrMessage: "",
 		},
+		{
+			description:        "Meta - Defined By Bid - Nil Extension",
+			ext:                nil,
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner"), Meta: &openrtb_ext.ExtBidPrebidMeta{BrandName: "foo"}},
+			impExtInfo:         map[string]ImpExtInfo{},
+			expectedBidExt:     `{"prebid":{"meta":{"brandName":"foo"},"type":"banner"}}`,
+			expectedErrMessage: "",
+		},
+		{
+			description:        "Meta - Defined By Bid - Empty Extension",
+			ext:                json.RawMessage(`{}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner"), Meta: &openrtb_ext.ExtBidPrebidMeta{BrandName: "foo"}},
+			impExtInfo:         nil,
+			expectedBidExt:     `{"prebid":{"meta":{"brandName":"foo"},"type":"banner"}}`,
+			expectedErrMessage: "",
+		},
+		{
+			description:        "Meta - Defined By Bid - Existing Extension Overwritten",
+			ext:                json.RawMessage(`{"prebid":{"meta":{"brandName":"notfoo", "brandId": 42}}}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner"), Meta: &openrtb_ext.ExtBidPrebidMeta{BrandName: "foo"}},
+			impExtInfo:         nil,
+			expectedBidExt:     `{"prebid":{"meta":{"brandName":"foo"},"type":"banner"}}`,
+			expectedErrMessage: "",
+		},
+		{
+			description:        "Meta - Not Defined By Bid - Persists From Bid Ext",
+			ext:                json.RawMessage(`{"prebid":{"meta":{"brandName":"foo"}}}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner")},
+			impExtInfo:         nil,
+			expectedBidExt:     `{"prebid":{"meta":{"brandName":"foo"},"type":"banner"}}`,
+			expectedErrMessage: "",
+		},
+		{
+			description:        "Meta - Not Defined By Bid - Persists From Bid Ext - Invalid Fields Ignored",
+			ext:                json.RawMessage(`{"prebid":{"meta":{"brandName":"foo","unknown":"value"}}}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner")},
+			impExtInfo:         nil,
+			expectedBidExt:     `{"prebid":{"meta":{"brandName":"foo"},"type":"banner"}}`,
+			expectedErrMessage: "",
+		},
+		{
+			description:        "Meta - Not Defined",
+			ext:                nil,
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner")},
+			impExtInfo:         nil,
+			expectedBidExt:     `{"prebid":{"type":"banner"}}`,
+			expectedErrMessage: "",
+		},
 		//Error cases
 		{
 			description:        "Invalid extension, valid extBidPrebid and valid imp ext info",
@@ -3571,6 +3627,14 @@ func TestMakeBidExtJSON(t *testing.T) {
 			expectedBidExt:     ``,
 			expectedErrMessage: "invalid character",
 		},
+		{
+			description:        "Meta - Invalid",
+			ext:                json.RawMessage(`{"prebid":{"meta":{"brandId":"foo"}}}`), // brandId should be an int, but is a string in this test case
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("banner")},
+			impExtInfo:         nil,
+			expectedErrMessage: "error validaing response from server, json: cannot unmarshal string into Go struct field ExtBidPrebidMeta.prebid.meta.brandId of type int",
+		},
+		// add invalid
 	}
 
 	for _, test := range testCases {
diff --git a/router/router_test.go b/router/router_test.go
index 07e59388..24a7709c 100644
--- a/router/router_test.go
+++ b/router/router_test.go
@@ -88,6 +88,14 @@ func TestApplyBidderInfoConfigOverrides(t *testing.T) {
 			givenAdaptersCfg:    map[string]config.Adapter{"a": {Syncer: &config.Syncer{Key: "override"}}},
 			expectedBidderInfos: config.BidderInfos{"a": {Syncer: &config.Syncer{Key: "override"}}},
 		},
+		{
+			// Adapter Configs use a lower case bidder name, but the Bidder Infos follow the official
+			// bidder name casing.
+			description:         "Syncer Override - Case Sensitivity",
+			givenBidderInfos:    config.BidderInfos{"A": {Syncer: &config.Syncer{Key: "original"}}},
+			givenAdaptersCfg:    map[string]config.Adapter{"a": {Syncer: &config.Syncer{Key: "override"}}},
+			expectedBidderInfos: config.BidderInfos{"A": {Syncer: &config.Syncer{Key: "override"}}},
+		},
 		{
 			description:         "UserSyncURL Override IFrame",
 			givenBidderInfos:    config.BidderInfos{"a": {Syncer: &config.Syncer{IFrame: &config.SyncerEndpoint{URL: "original"}}}},
diff --git a/util/maputil/maputil_test.go b/util/maputil/maputil_test.go
index abce102f..114c2332 100644
--- a/util/maputil/maputil_test.go
+++ b/util/maputil/maputil_test.go
@@ -164,3 +164,84 @@ func TestReadEmbeddedString(t *testing.T) {
 		assert.Equal(t, test.expectedOK, resultOK, test.description+":ok")
 	}
 }
+
+func TestHasElement(t *testing.T) {
+	testCases := []struct {
+		description string
+		value       map[string]interface{}
+		keys        []string
+		expected    bool
+	}{
+		{
+			description: "Level 1 - Exists",
+			value:       map[string]interface{}{"foo": "exists"},
+			keys:        []string{"foo"},
+			expected:    true,
+		},
+		{
+			description: "Level 1 - Does Not Exist",
+			value:       map[string]interface{}{"foo": "exists"},
+			keys:        []string{"doesnotexist"},
+			expected:    false,
+		},
+		{
+			description: "Level 2 - Exists",
+			value:       map[string]interface{}{"foo": map[string]interface{}{"bar": "exists"}},
+			keys:        []string{"foo", "bar"},
+			expected:    true,
+		},
+		{
+			description: "Level 2 - Top Level Does Not Exist",
+			value:       map[string]interface{}{"foo": map[string]interface{}{"bar": "exists"}},
+			keys:        []string{"doesnotexist", "bar"},
+			expected:    false,
+		},
+		{
+			description: "Level 2 - Lower Level Does Not Exist",
+			value:       map[string]interface{}{"foo": map[string]interface{}{"bar": "exists"}},
+			keys:        []string{"foo", "doesnotexist"},
+			expected:    false,
+		},
+		{
+			description: "Level 2 - Does Not Exist At All",
+			value:       map[string]interface{}{"foo": map[string]interface{}{"bar": "exists"}},
+			keys:        []string{"doesnotexist", "doesnotexist"},
+			expected:    false,
+		},
+		{
+			description: "Keys Nil",
+			value:       map[string]interface{}{"foo": "exists"},
+			keys:        nil,
+			expected:    false,
+		},
+		{
+			description: "Keys Empty",
+			value:       map[string]interface{}{"foo": "exists"},
+			keys:        []string{},
+			expected:    false,
+		},
+		{
+			description: "Map Nil",
+			value:       nil,
+			keys:        []string{"foo"},
+			expected:    false,
+		},
+		{
+			description: "Map Empty",
+			value:       map[string]interface{}{},
+			keys:        []string{"foo"},
+			expected:    false,
+		},
+		{
+			description: "Nil",
+			value:       nil,
+			keys:        nil,
+			expected:    false,
+		},
+	}
+
+	for _, test := range testCases {
+		result := HasElement(test.value, test.keys...)
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}