Repository: prebid/prebid-server
Pull request #1622 (2020-12-11 14:29:40)

New Adapter: Revcontent
Hello!

I'm looking to add support for Revcontent ORTB to prebid-server. We really just need a [generic adapter](https://github.com/prebid/prebid-server/issues/1429), but from the discussion I don't think that's a thing yet.

I'm fairly new to go and this project, so just let me know what I need to fix. :)

jcamp-revc: Thanks for the feedback. I think this should all be addressed in the latest commit. The new docs were very helpful, thank you!
jcamp-revc: > Except for the following minor comments, LGTM!

Thanks, these should be addressed now.
stale[bot]: This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

SyntaxNode: Removing stale label to end of year holidays.
jcamp-revc: Thanks, I'll get those checks added and then hopefully we can merge soon.
SyntaxNode: Thank you for the changes. Looks like it's failing on `app_and_site.json` and that's ok. The adapter json unit tests bypass the core checks. You can resolve by removing that test. We have coverage elsewhere.
jcamp-revc: > Thank you for the changes. Looks like it's failing on `app_and_site.json` and in retrospec that's expected. The adapter json unit tests bypass the core checks. You can resolve by removing that test. We have coverage elsewhere.

That's what I get for not running the tests, even on a simple change. Sorry about that.
SyntaxNode: LGTM. Thank you. I'll seek a second review tomorrow and there's a very good chance this can be incorporated into the next release. Before we can merge, we just need you to open the documentation PR. For details, please see the bottom of the new adapter docs: https://docs.prebid.org/prebid-server/developers/add-new-bidder-go.html#user-documentation
SyntaxNode: @jcamp-revc We're ready to merge in once you have a PR to update your adapter docs.
jcamp-revc: Thanks, [PR is opened](https://github.com/prebid/prebid.github.io/pull/2614).
SyntaxNode: @jcamp-revc Thank you.

I noticed that "Revcontent will provide you with a custom endpoint that will contain your account details". With this in mind, does the default URL provided in this PR work? .. or should this adapter be disabled by default until hosts work with Revcontent to set it up?
jcamp-revc: I tried to disable it, but was having trouble with getting tests to pass. Let me try again.

Patch:
diff --git a/adapters/mediafuse/usersync.go b/adapters/mediafuse/usersync.go
new file mode 100644
index 00000000..5381bf36
--- /dev/null
+++ b/adapters/mediafuse/usersync.go
@@ -0,0 +1,12 @@
+package mediafuse
+
+import (
+	"text/template"
+
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/usersync"
+)
+
+func NewMediafuseSyncer(temp *template.Template) usersync.Usersyncer {
+	return adapters.NewSyncer("mediafuse", 411, temp, adapters.SyncTypeRedirect)
+}
diff --git a/adapters/revcontent/revcontent.go b/adapters/revcontent/revcontent.go
new file mode 100644
index 00000000..5a34f3ef
--- /dev/null
+++ b/adapters/revcontent/revcontent.go
@@ -0,0 +1,105 @@
+package revcontent
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"net/http"
+)
+
+type adapter struct {
+	endpoint string
+}
+
+// Builder builds a new instance of the Revcontent adapter for the given bidder with the given config.
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) {
+	bidder := &adapter{
+		endpoint: config.Endpoint,
+	}
+	return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	reqBody, err := json.Marshal(request)
+
+	if err != nil {
+		return nil, []error{err}
+	}
+
+	if err := checkRequest(request); err != nil {
+		return nil, []error{err}
+	}
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+
+	req := &adapters.RequestData{
+		Method:  "POST",
+		Uri:     a.endpoint,
+		Body:    reqBody,
+		Headers: headers,
+	}
+	return []*adapters.RequestData{req}, nil
+}
+
+func checkRequest(request *openrtb.BidRequest) error {
+	if (request.App == nil || len(request.App.Name) == 0) && (request.Site == nil || len(request.Site.Domain) == 0) {
+		return &errortypes.BadInput{
+			Message: "Impression is missing app name or site domain, and must contain one.",
+		}
+	}
+
+	return nil
+}
+
+// MakeBids make the bids for the bid response.
+func (a *adapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if response.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+
+	if response.StatusCode == http.StatusBadRequest {
+		return nil, []error{&errortypes.BadInput{
+			Message: fmt.Sprintf("unexpected status code: %d.", response.StatusCode),
+		}}
+	}
+
+	if response.StatusCode != http.StatusOK {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: fmt.Sprintf("unexpected status code: %d.", response.StatusCode),
+		}}
+	}
+
+	var bidResp openrtb.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(5)
+
+	for _, sb := range bidResp.SeatBid {
+		for i := range sb.Bid {
+			var mediaType = getBidType(sb.Bid[i].AdM)
+			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+				Bid:     &sb.Bid[i],
+				BidType: mediaType,
+			})
+		}
+	}
+	return bidResponse, nil
+
+}
+
+func getBidType(bidAdm string) openrtb_ext.BidType {
+	// native: {"ver":"1.1","assets":...
+	// banner: <div id='rtb-widget...
+	if bidAdm != "" && bidAdm[:1] == "<" {
+		return openrtb_ext.BidTypeBanner
+	}
+	return openrtb_ext.BidTypeNative
+}
diff --git a/amp/parse.go b/amp/parse.go
new file mode 100644
index 00000000..9e0e019f
--- /dev/null
+++ b/amp/parse.go
@@ -0,0 +1,110 @@
+package amp
+
+import (
+	"errors"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"github.com/mxmCherry/openrtb"
+)
+
+// Params defines the paramters of an AMP request.
+type Params struct {
+	Account         string
+	CanonicalURL    string
+	Consent         string
+	Debug           bool
+	Origin          string
+	Size            Size
+	Slot            string
+	StoredRequestID string
+	Timeout         *uint64
+}
+
+// Size defines size information of an AMP request.
+type Size struct {
+	Height         uint64
+	Multisize      []openrtb.Format
+	OverrideHeight uint64
+	OverrideWidth  uint64
+	Width          uint64
+}
+
+// ParseParams parses the AMP paramters from a HTTP request.
+func ParseParams(httpRequest *http.Request) (Params, error) {
+	query := httpRequest.URL.Query()
+
+	tagID := query.Get("tag_id")
+	if len(tagID) == 0 {
+		return Params{}, errors.New("AMP requests require an AMP tag_id")
+	}
+
+	params := Params{
+		Account:      query.Get("account"),
+		CanonicalURL: query.Get("curl"),
+		Consent:      chooseConsent(query.Get("consent_string"), query.Get("gdpr_consent")),
+		Debug:        query.Get("debug") == "1",
+		Origin:       query.Get("__amp_source_origin"),
+		Size: Size{
+			Height:         parseInt(query.Get("h")),
+			Multisize:      parseMultisize(query.Get("ms")),
+			OverrideHeight: parseInt(query.Get("oh")),
+			OverrideWidth:  parseInt(query.Get("ow")),
+			Width:          parseInt(query.Get("w")),
+		},
+		Slot:            query.Get("slot"),
+		StoredRequestID: tagID,
+		Timeout:         parseIntPtr(query.Get("timeout")),
+	}
+	return params, nil
+}
+
+func parseIntPtr(value string) *uint64 {
+	if parsed, err := strconv.ParseUint(value, 10, 64); err == nil {
+		return &parsed
+	}
+	return nil
+}
+
+func parseInt(value string) uint64 {
+	if parsed, err := strconv.ParseUint(value, 10, 64); err == nil {
+		return parsed
+	}
+	return 0
+}
+
+func parseMultisize(multisize string) []openrtb.Format {
+	if multisize == "" {
+		return nil
+	}
+
+	sizeStrings := strings.Split(multisize, ",")
+	sizes := make([]openrtb.Format, 0, len(sizeStrings))
+	for _, sizeString := range sizeStrings {
+		wh := strings.Split(sizeString, "x")
+		if len(wh) != 2 {
+			return nil
+		}
+		f := openrtb.Format{
+			W: parseInt(wh[0]),
+			H: parseInt(wh[1]),
+		}
+		if f.W == 0 && f.H == 0 {
+			return nil
+		}
+
+		sizes = append(sizes, f)
+	}
+	return sizes
+}
+
+func chooseConsent(consent, gdprConsent string) string {
+	if len(consent) > 0 {
+		return consent
+	}
+
+	// Fallback to 'gdpr_consent' for compatability 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/config/config.go b/config/config.go
index 4d8f6ab0..a35b3c2a 100755
--- a/config/config.go
+++ b/config/config.go
@@ -606,6 +606,7 @@ func (cfg *Configuration) setDerivedDefaults() {
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLogicad, "https://cr-p31.ladsp.jp/cookiesender/31?r=true&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ru="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dlogicad%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLunaMedia, "https://api.lunamedia.io/xp/user-sync?redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dlunamedia%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderMarsmedia, "https://dmp.rtbsrv.com/dmp/profiles/cm?p_id=179&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmarsmedia%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderMediafuse, "https://sync.hbmp.mediafuse.com/csync?t=p&ep=0&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmediafuse%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderMgid, "https://cm.mgid.com/m?cdsp=363893&adu="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmgid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Bmuidn%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderNanoInteractive, "https://ad.audiencemanager.de/hbs/cookie_sync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dnanointeractive%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderNinthDecimal, "https://rtb.ninthdecimal.com/xp/user-sync?acctid={aid}&&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dninthdecimal%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
@@ -845,6 +846,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.logicad.endpoint", "https://pbs.ladsp.com/adrequest/prebidserver")
 	v.SetDefault("adapters.lunamedia.endpoint", "http://api.lunamedia.io/xp/get?pubid={{.PublisherID}}")
 	v.SetDefault("adapters.marsmedia.endpoint", "https://bid306.rtbsrv.com/bidder/?bid=f3xtet")
+	v.SetDefault("adapters.mediafuse.endpoint", "http://ghb.hbmp.mediafuse.com/pbs/ortb")
 	v.SetDefault("adapters.mgid.endpoint", "https://prebid.mgid.com/prebid/")
 	v.SetDefault("adapters.mobilefuse.endpoint", "http://mfx.mobilefuse.com/openrtb?pub_id={{.PublisherID}}")
 	v.SetDefault("adapters.mobfoxpb.endpoint", "http://bes.mobfox.com/?c=o&m=ortb")
@@ -856,6 +858,8 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.pubmatic.endpoint", "https://hbopenbid.pubmatic.com/translator?source=prebid-server")
 	v.SetDefault("adapters.pubnative.endpoint", "http://dsp.pubnative.net/bid/v1/request")
 	v.SetDefault("adapters.pulsepoint.endpoint", "http://bid.contextweb.com/header/s/ortb/prebid-s2s")
+	v.SetDefault("adapters.revcontent.disabled", true)
+	v.SetDefault("adapters.revcontent.endpoint", "https://trends.revcontent.com/rtb")
 	v.SetDefault("adapters.rhythmone.endpoint", "http://tag.1rx.io/rmp")
 	v.SetDefault("adapters.rtbhouse.endpoint", "http://prebidserver-s2s-ams.creativecdn.com/bidder/prebidserver/bids")
 	v.SetDefault("adapters.rubicon.disabled", true)
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index afc32e5e..dc66163a 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -8,7 +8,6 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
-	"strconv"
 	"strings"
 	"time"
 
@@ -17,6 +16,7 @@ import (
 	"github.com/julienschmidt/httprouter"
 	"github.com/mxmCherry/openrtb"
 	accountService "github.com/prebid/prebid-server/account"
+	"github.com/prebid/prebid-server/amp"
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
@@ -314,43 +314,41 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req
 	req = &openrtb.BidRequest{}
 	errs = nil
 
-	ampID := httpRequest.FormValue("tag_id")
-	if ampID == "" {
-		errs = []error{errors.New("AMP requests require an AMP tag_id")}
-		return
+	ampParams, err := amp.ParseParams(httpRequest)
+	if err != nil {
+		return nil, []error{err}
 	}
 
 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(storedRequestTimeoutMillis)*time.Millisecond)
 	defer cancel()
 
-	storedRequests, _, errs := deps.storedReqFetcher.FetchRequests(ctx, []string{ampID}, nil)
+	storedRequests, _, errs := deps.storedReqFetcher.FetchRequests(ctx, []string{ampParams.StoredRequestID}, nil)
 	if len(errs) > 0 {
 		return nil, errs
 	}
 	if len(storedRequests) == 0 {
-		errs = []error{fmt.Errorf("No AMP config found for tag_id '%s'", ampID)}
+		errs = []error{fmt.Errorf("No AMP config found for tag_id '%s'", ampParams.StoredRequestID)}
 		return
 	}
 
 	// The fetched config becomes the entire OpenRTB request
-	requestJSON := storedRequests[ampID]
+	requestJSON := storedRequests[ampParams.StoredRequestID]
 	if err := json.Unmarshal(requestJSON, req); err != nil {
 		errs = []error{err}
 		return
 	}
 
-	debugParam := httpRequest.FormValue("debug")
-	if debugParam == "1" {
+	if ampParams.Debug {
 		req.Test = 1
 	}
 
 	// Two checks so users know which way the Imp check failed.
 	if len(req.Imp) == 0 {
-		errs = []error{fmt.Errorf("data for tag_id='%s' does not define the required imp array", ampID)}
+		errs = []error{fmt.Errorf("data for tag_id='%s' does not define the required imp array", ampParams.StoredRequestID)}
 		return
 	}
 	if len(req.Imp) > 1 {
-		errs = []error{fmt.Errorf("data for tag_id '%s' includes %d imp elements. Only one is allowed", ampID, len(req.Imp))}
+		errs = []error{fmt.Errorf("data for tag_id '%s' includes %d imp elements. Only one is allowed", ampParams.StoredRequestID, len(req.Imp))}
 		return
 	}
 
@@ -367,35 +365,30 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req
 		*req.Imp[0].Secure = 1
 	}
 
-	errs = deps.overrideWithParams(httpRequest, req)
+	errs = deps.overrideWithParams(ampParams, req)
 	return
 }
 
-func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *openrtb.BidRequest) []error {
+func (deps *endpointDeps) overrideWithParams(ampParams amp.Params, req *openrtb.BidRequest) []error {
 	if req.Site == nil {
 		req.Site = &openrtb.Site{}
 	}
 
 	// Override the stored request sizes with AMP ones, if they exist.
 	if req.Imp[0].Banner != nil {
-		width := parseFormInt(httpRequest, "w", 0)
-		height := parseFormInt(httpRequest, "h", 0)
-		overrideWidth := parseFormInt(httpRequest, "ow", 0)
-		overrideHeight := parseFormInt(httpRequest, "oh", 0)
-		if format := makeFormatReplacement(overrideWidth, overrideHeight, width, height, httpRequest.FormValue("ms")); len(format) != 0 {
+		if format := makeFormatReplacement(ampParams.Size); len(format) != 0 {
 			req.Imp[0].Banner.Format = format
-		} else if width != 0 {
-			setWidths(req.Imp[0].Banner.Format, width)
-		} else if height != 0 {
-			setHeights(req.Imp[0].Banner.Format, height)
+		} else if ampParams.Size.Width != 0 {
+			setWidths(req.Imp[0].Banner.Format, ampParams.Size.Width)
+		} else if ampParams.Size.Height != 0 {
+			setHeights(req.Imp[0].Banner.Format, ampParams.Size.Height)
 		}
 	}
 
-	canonicalURL := httpRequest.FormValue("curl")
-	if canonicalURL != "" {
-		req.Site.Page = canonicalURL
+	if ampParams.CanonicalURL != "" {
+		req.Site.Page = ampParams.CanonicalURL
 		// Fixes #683
-		if parsedURL, err := url.Parse(canonicalURL); err == nil {
+		if parsedURL, err := url.Parse(ampParams.CanonicalURL); err == nil {
 			domain := parsedURL.Host
 			if colonIndex := strings.LastIndex(domain, ":"); colonIndex != -1 {
 				domain = domain[:colonIndex]
@@ -406,14 +399,13 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope
 
 	setAmpExt(req.Site, "1")
 
-	setEffectiveAmpPubID(req, httpRequest.URL.Query())
+	setEffectiveAmpPubID(req, ampParams.Account)
 
-	slot := httpRequest.FormValue("slot")
-	if slot != "" {
-		req.Imp[0].TagID = slot
+	if ampParams.Slot != "" {
+		req.Imp[0].TagID = ampParams.Slot
 	}
 
-	policyWriter, policyWriterErr := readPolicyFromUrl(httpRequest.URL)
+	policyWriter, policyWriterErr := readPolicy(ampParams.Consent)
 	if policyWriterErr != nil {
 		return []error{policyWriterErr}
 	}
@@ -421,42 +413,38 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope
 		return []error{err}
 	}
 
-	if timeout, err := strconv.ParseInt(httpRequest.FormValue("timeout"), 10, 64); err == nil {
-		req.TMax = timeout - deps.cfg.AMPTimeoutAdjustment
+	if ampParams.Timeout != nil {
+		req.TMax = int64(*ampParams.Timeout) - deps.cfg.AMPTimeoutAdjustment
 	}
 
 	return nil
 }
 
-func makeFormatReplacement(overrideWidth uint64, overrideHeight uint64, width uint64, height uint64, multisize string) []openrtb.Format {
+func makeFormatReplacement(size amp.Size) []openrtb.Format {
 	var formats []openrtb.Format
-	if overrideWidth != 0 && overrideHeight != 0 {
+	if size.OverrideWidth != 0 && size.OverrideHeight != 0 {
 		formats = []openrtb.Format{{
-			W: overrideWidth,
-			H: overrideHeight,
+			W: size.OverrideWidth,
+			H: size.OverrideHeight,
 		}}
-	} else if overrideWidth != 0 && height != 0 {
+	} else if size.OverrideWidth != 0 && size.Height != 0 {
 		formats = []openrtb.Format{{
-			W: overrideWidth,
-			H: height,
+			W: size.OverrideWidth,
+			H: size.Height,
 		}}
-	} else if width != 0 && overrideHeight != 0 {
+	} else if size.Width != 0 && size.OverrideHeight != 0 {
 		formats = []openrtb.Format{{
-			W: width,
-			H: overrideHeight,
+			W: size.Width,
+			H: size.OverrideHeight,
 		}}
-	} else if width != 0 && height != 0 {
+	} else if size.Width != 0 && size.Height != 0 {
 		formats = []openrtb.Format{{
-			W: width,
-			H: height,
+			W: size.Width,
+			H: size.Height,
 		}}
 	}
 
-	if parsedSizes := parseMultisize(multisize); len(parsedSizes) != 0 {
-		formats = append(formats, parsedSizes...)
-	}
-
-	return formats
+	return append(formats, size.Multisize...)
 }
 
 func setWidths(formats []openrtb.Format, width uint64) {
@@ -471,42 +459,6 @@ func setHeights(formats []openrtb.Format, height uint64) {
 	}
 }
 
-func parseMultisize(multisize string) []openrtb.Format {
-	if multisize == "" {
-		return nil
-	}
-
-	sizeStrings := strings.Split(multisize, ",")
-	sizes := make([]openrtb.Format, 0, len(sizeStrings))
-	for _, sizeString := range sizeStrings {
-		wh := strings.Split(sizeString, "x")
-		if len(wh) != 2 {
-			return nil
-		}
-		f := openrtb.Format{
-			W: parseIntErrorless(wh[0], 0),
-			H: parseIntErrorless(wh[1], 0),
-		}
-		if f.W == 0 && f.H == 0 {
-			return nil
-		}
-
-		sizes = append(sizes, f)
-	}
-	return sizes
-}
-
-func parseFormInt(req *http.Request, value string, defaultTo uint64) uint64 {
-	return parseIntErrorless(req.FormValue(value), defaultTo)
-}
-
-func parseIntErrorless(value string, defaultTo uint64) uint64 {
-	if parsed, err := strconv.ParseUint(value, 10, 64); err == nil {
-		return parsed
-	}
-	return defaultTo
-}
-
 // AMP won't function unless ext.prebid.targeting and ext.prebid.cache.bids are defined.
 // If the user didn't include them, default those here.
 func defaultRequestExt(req *openrtb.BidRequest) (errs []error) {
@@ -563,9 +515,7 @@ func setAmpExt(site *openrtb.Site, value string) {
 	}
 }
 
-func readPolicyFromUrl(url *url.URL) (privacy.PolicyWriter, error) {
-	consent := readConsentFromURL(url)
-
+func readPolicy(consent string) (privacy.PolicyWriter, error) {
 	if len(consent) == 0 {
 		return privacy.NilPolicyWriter{}, nil
 	}
@@ -583,17 +533,8 @@ func readPolicyFromUrl(url *url.URL) (privacy.PolicyWriter, error) {
 	}
 }
 
-func readConsentFromURL(url *url.URL) string {
-	if v := url.Query().Get("consent_string"); v != "" {
-		return v
-	}
-
-	// Fallback to 'gdpr_consent' for compatability until it's no longer used by AMP.
-	return url.Query().Get("gdpr_consent")
-}
-
 // Sets the effective publisher ID for amp request
-func setEffectiveAmpPubID(req *openrtb.BidRequest, urlQueryParams url.Values) {
+func setEffectiveAmpPubID(req *openrtb.BidRequest, account string) {
 	var pub *openrtb.Publisher
 	if req.App != nil {
 		if req.App.Publisher == nil {
@@ -608,10 +549,9 @@ func setEffectiveAmpPubID(req *openrtb.BidRequest, urlQueryParams url.Values) {
 	}
 
 	if pub.ID == "" {
-		// For amp requests, the publisher ID could be sent via the account
-		// query string
-		if acc := urlQueryParams.Get("account"); acc != "" && acc != "ACCOUNT_ID" {
-			pub.ID = acc
+		// ACCOUNT_ID is the unresolved macro name and should be ignored.
+		if account != "" && account != "ACCOUNT_ID" {
+			pub.ID = account
 		}
 	}
 }
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 97f8adfe..31342a4c 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -65,6 +65,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/orbidder"
 	"github.com/prebid/prebid-server/adapters/pubmatic"
 	"github.com/prebid/prebid-server/adapters/pubnative"
+	"github.com/prebid/prebid-server/adapters/revcontent"
 	"github.com/prebid/prebid-server/adapters/rhythmone"
 	"github.com/prebid/prebid-server/adapters/rtbhouse"
 	"github.com/prebid/prebid-server/adapters/rubicon"
@@ -154,6 +155,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderLogicad:          logicad.Builder,
 		openrtb_ext.BidderLunaMedia:        lunamedia.Builder,
 		openrtb_ext.BidderMarsmedia:        marsmedia.Builder,
+		openrtb_ext.BidderMediafuse:        adtelligent.Builder,
 		openrtb_ext.BidderMgid:             mgid.Builder,
 		openrtb_ext.BidderMobfoxpb:         mobfoxpb.Builder,
 		openrtb_ext.BidderMobileFuse:       mobilefuse.Builder,
@@ -164,6 +166,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderOrbidder:         orbidder.Builder,
 		openrtb_ext.BidderPubmatic:         pubmatic.Builder,
 		openrtb_ext.BidderPubnative:        pubnative.Builder,
+		openrtb_ext.BidderRevcontent:       revcontent.Builder,
 		openrtb_ext.BidderRhythmone:        rhythmone.Builder,
 		openrtb_ext.BidderRTBHouse:         rtbhouse.Builder,
 		openrtb_ext.BidderRubicon:          rubicon.Builder,
diff --git a/exchange/utils.go b/exchange/utils.go
index 21c31a29..d574c2a6 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -118,11 +118,13 @@ func cleanOpenRTBRequests(ctx context.Context,
 		if gdprEnforced {
 			var publisherID = req.LegacyLabels.PubID
 			_, geo, id, err := gDPR.PersonalInfoAllowed(ctx, bidderRequest.BidderCoreName, publisherID, gdprSignal, consent)
-			privacyEnforcement.GDPRGeo = !geo && err == nil
-			privacyEnforcement.GDPRID = !id && err == nil
-		} else {
-			privacyEnforcement.GDPRGeo = false
-			privacyEnforcement.GDPRID = false
+			if err == nil {
+				privacyEnforcement.GDPRGeo = !geo
+				privacyEnforcement.GDPRID = !id
+			} else {
+				privacyEnforcement.GDPRGeo = true
+				privacyEnforcement.GDPRID = true
+			}
 		}
 
 		privacyEnforcement.Apply(bidderRequest.BidRequest)
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 80a45108..c0839632 100755
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -94,6 +94,7 @@ const (
 	BidderLogicad          BidderName = "logicad"
 	BidderLunaMedia        BidderName = "lunamedia"
 	BidderMarsmedia        BidderName = "marsmedia"
+	BidderMediafuse        BidderName = "mediafuse"
 	BidderMgid             BidderName = "mgid"
 	BidderMobfoxpb         BidderName = "mobfoxpb"
 	BidderMobileFuse       BidderName = "mobilefuse"
@@ -105,6 +106,7 @@ const (
 	BidderPubmatic         BidderName = "pubmatic"
 	BidderPubnative        BidderName = "pubnative"
 	BidderPulsepoint       BidderName = "pulsepoint"
+	BidderRevcontent       BidderName = "revcontent"
 	BidderRhythmone        BidderName = "rhythmone"
 	BidderRTBHouse         BidderName = "rtbhouse"
 	BidderRubicon          BidderName = "rubicon"
@@ -192,6 +194,7 @@ func CoreBidderNames() []BidderName {
 		BidderLogicad,
 		BidderLunaMedia,
 		BidderMarsmedia,
+		BidderMediafuse,
 		BidderMgid,
 		BidderMobfoxpb,
 		BidderMobileFuse,
@@ -203,6 +206,7 @@ func CoreBidderNames() []BidderName {
 		BidderPubmatic,
 		BidderPubnative,
 		BidderPulsepoint,
+		BidderRevcontent,
 		BidderRhythmone,
 		BidderRTBHouse,
 		BidderRubicon,
diff --git a/static/bidder-info/mediafuse.yaml b/static/bidder-info/mediafuse.yaml
new file mode 100644
index 00000000..112f67fe
--- /dev/null
+++ b/static/bidder-info/mediafuse.yaml
@@ -0,0 +1,11 @@
+maintainer:
+  email: "support@mediafuse.com"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+  site:
+    mediaTypes:
+      - banner
+      - video
diff --git a/static/bidder-info/revcontent.yaml b/static/bidder-info/revcontent.yaml
new file mode 100644
index 00000000..c0216dfc
--- /dev/null
+++ b/static/bidder-info/revcontent.yaml
@@ -0,0 +1,11 @@
+maintainer:
+  email: "developers@revcontent.com"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - native
+  site:
+    mediaTypes:
+      - banner
+      - native
\ No newline at end of file
diff --git a/static/bidder-params/mediafuse.json b/static/bidder-params/mediafuse.json
new file mode 100644
index 00000000..4f38f89d
--- /dev/null
+++ b/static/bidder-params/mediafuse.json
@@ -0,0 +1,26 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Mediafuse Adapter Params",
+  "description": "A schema which validates params accepted by the Mediafuse adapter",
+
+  "type": "object",
+  "properties": {
+    "placementId": {
+      "type": "integer",
+      "description": "An ID which identifies this placement of the impression"
+    },
+    "siteId": {
+      "type": "integer",
+      "description": "An ID which identifies the site selling the impression"
+    },
+    "aid": {
+      "type": "integer",
+      "description": "An ID which identifies the channel"
+    },
+    "bidFloor": {
+      "type": "number",
+      "description": "BidFloor, US Dollars"
+    }
+  },
+  "required": ["aid"]
+}
diff --git a/static/bidder-params/revcontent.json b/static/bidder-params/revcontent.json
new file mode 100644
index 00000000..e6f99cb2
--- /dev/null
+++ b/static/bidder-params/revcontent.json
@@ -0,0 +1,9 @@
+
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Revcontent Adapter Params",
+  "description": "A schema which validates params accepted by the Revcontent adapter",
+  "type": "object",
+  "properties": {},
+  "required": []
+}
\ No newline at end of file
diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go
index 84044e85..c41f7c6c 100755
--- a/usersync/usersyncers/syncer.go
+++ b/usersync/usersyncers/syncer.go
@@ -50,6 +50,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/logicad"
 	"github.com/prebid/prebid-server/adapters/lunamedia"
 	"github.com/prebid/prebid-server/adapters/marsmedia"
+	"github.com/prebid/prebid-server/adapters/mediafuse"
 	"github.com/prebid/prebid-server/adapters/mgid"
 	"github.com/prebid/prebid-server/adapters/nanointeractive"
 	"github.com/prebid/prebid-server/adapters/ninthdecimal"
@@ -136,6 +137,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderLogicad, logicad.NewLogicadSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderLunaMedia, lunamedia.NewLunaMediaSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderMarsmedia, marsmedia.NewMarsmediaSyncer)
+	insertIntoMap(cfg, syncers, openrtb_ext.BidderMediafuse, mediafuse.NewMediafuseSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderMgid, mgid.NewMgidSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderNanoInteractive, nanointeractive.NewNanoInteractiveSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderNinthDecimal, ninthdecimal.NewNinthDecimalSyncer)


Test patch:
diff --git a/adapters/mediafuse/usersync_test.go b/adapters/mediafuse/usersync_test.go
new file mode 100644
index 00000000..30b5b535
--- /dev/null
+++ b/adapters/mediafuse/usersync_test.go
@@ -0,0 +1,30 @@
+package mediafuse
+
+import (
+	"testing"
+	"text/template"
+
+	"github.com/prebid/prebid-server/privacy"
+	"github.com/prebid/prebid-server/privacy/gdpr"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMediafuseSyncer(t *testing.T) {
+	syncURL := "//sync.hbmp.mediafuse.com/csync?t=p&ep=0&redir=localhost%2Fsetuid%3Fbidder%3Dmediafuse%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D"
+	syncURLTemplate := template.Must(
+		template.New("sync-template").Parse(syncURL),
+	)
+
+	syncer := NewMediafuseSyncer(syncURLTemplate)
+	syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{
+		GDPR: gdpr.Policy{
+			Signal: "0",
+		},
+	})
+
+	assert.NoError(t, err)
+	assert.Equal(t, "//sync.hbmp.mediafuse.com/csync?t=p&ep=0&redir=localhost%2Fsetuid%3Fbidder%3Dmediafuse%26gdpr%3D0%26gdpr_consent%3D%26uid%3D%7Buid%7D", syncInfo.URL)
+	assert.Equal(t, "redirect", syncInfo.Type)
+	assert.EqualValues(t, 411, syncer.GDPRVendorID())
+	assert.Equal(t, false, syncInfo.SupportCORS)
+}
diff --git a/adapters/revcontent/revcontent_test.go b/adapters/revcontent/revcontent_test.go
new file mode 100644
index 00000000..836ef138
--- /dev/null
+++ b/adapters/revcontent/revcontent_test.go
@@ -0,0 +1,21 @@
+package revcontent
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestJsonSamples(t *testing.T) {
+	bidder, buildErr := Builder(openrtb_ext.BidderRevcontent, config.Adapter{
+		Endpoint: "https://trends.revcontent.com/rtb?userId=1234&apiKey=abcd",
+	})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "revcontenttest", bidder)
+}
diff --git a/adapters/revcontent/revcontenttest/exemplary/no-bid.json b/adapters/revcontent/revcontenttest/exemplary/no-bid.json
new file mode 100644
index 00000000..b71c3a93
--- /dev/null
+++ b/adapters/revcontent/revcontenttest/exemplary/no-bid.json
@@ -0,0 +1,45 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [{"w": 300, "h": 50}]
+        }
+      }
+    ],
+    "site": {
+      "domain": "test.com"
+    }
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://trends.revcontent.com/rtb?userId=1234&apiKey=abcd",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "banner": {
+                "format": [{"w": 300, "h": 50}]
+              }
+            }
+          ],
+          "site": {
+            "domain": "test.com"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 204,
+        "body": {}
+      }
+    }
+  ],
+
+  "expectedBidResponses": []
+
+}
diff --git a/adapters/revcontent/revcontenttest/exemplary/simple-banner.json b/adapters/revcontent/revcontenttest/exemplary/simple-banner.json
new file mode 100644
index 00000000..a63f9ac8
--- /dev/null
+++ b/adapters/revcontent/revcontenttest/exemplary/simple-banner.json
@@ -0,0 +1,85 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [{"w": 300, "h": 50}]
+        }
+      }
+    ],
+    "site": {
+      "id": "example",
+      "domain": "example.com",
+      "page": "example.com",
+      "publisher": {
+        "id": "example"
+      }
+    }
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://trends.revcontent.com/rtb?userId=1234&apiKey=abcd",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "banner": {
+                "format": [{"w": 300, "h": 50}]
+              }
+            }
+          ],
+          "site": {
+            "id": "example",
+            "domain": "example.com",
+            "page": "example.com",
+            "publisher": {
+              "id": "example"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "ttx",
+              "bid": [{
+                "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+                "impid": "test-imp-id",
+                "price": 1.2,
+                "adm": "<div></div>",
+                "crid": "crid_testid"
+              }]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+            "impid": "test-imp-id",
+            "price": 1.2,
+            "adm": "<div></div>",
+            "crid": "crid_testid"
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/revcontent/revcontenttest/exemplary/simple-native.json b/adapters/revcontent/revcontenttest/exemplary/simple-native.json
new file mode 100644
index 00000000..8cd20913
--- /dev/null
+++ b/adapters/revcontent/revcontenttest/exemplary/simple-native.json
@@ -0,0 +1,76 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "native": {
+          "request": "{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":2,\"required\":1,\"title\":{\"len\":90}},{\"id\":6,\"required\":1,\"img\":{\"type\":3,\"wmin\":128,\"hmin\":128,\"mimes\":[\"image/jpg\",\"image/jpeg\",\"image/png\"]}},{\"id\":7,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}",
+          "ver": "1.2"
+        }
+      }
+    ],
+    "site": {
+      "domain": "test.com"
+    }
+  },
+  "httpcalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://trends.revcontent.com/rtb?userId=1234&apiKey=abcd",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "native": {
+                "request": "{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":2,\"required\":1,\"title\":{\"len\":90}},{\"id\":6,\"required\":1,\"img\":{\"type\":3,\"wmin\":128,\"hmin\":128,\"mimes\":[\"image/jpg\",\"image/jpeg\",\"image/png\"]}},{\"id\":7,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}",
+                "ver": "1.2"
+              }
+            }
+          ],
+          "site": {
+            "domain": "test.com"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "8400d766-58b3-47d4-80d7-6658b337d403",
+                  "impid": "test-imp-id",
+                  "price": 1.2,
+                  "adm": "{\\\"ver\\\":\\\"1.1\\\",\\\"assets\\\":[]}",
+                  "crid": "crid_testid"
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "8400d766-58b3-47d4-80d7-6658b337d403",
+            "impid": "test-imp-id",
+            "price": 1.2,
+            "adm": "{\\\"ver\\\":\\\"1.1\\\",\\\"assets\\\":[]}",
+            "crid": "crid_testid"
+
+          },
+          "type": "native"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/revcontent/revcontenttest/supplemental/bad_response.json b/adapters/revcontent/revcontenttest/supplemental/bad_response.json
new file mode 100644
index 00000000..bd562373
--- /dev/null
+++ b/adapters/revcontent/revcontenttest/supplemental/bad_response.json
@@ -0,0 +1,49 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [{"w": 300, "h": 50}]
+        }
+      }
+    ],
+    "site": {
+      "domain": "test.com"
+    }
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://trends.revcontent.com/rtb?userId=1234&apiKey=abcd",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "banner": {
+                "format": [{"w": 300, "h": 50}]
+              }
+            }
+          ],
+          "site": {
+            "domain": "test.com"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": "{\"id\":test-request-id"
+      }
+    }
+  ],
+
+  "expectedMakeBidsErrors": [
+    {
+      "comparison": "literal",
+      "value": "json: cannot unmarshal string into Go value of type openrtb.BidResponse"
+    }
+  ]
+}
diff --git a/adapters/revcontent/revcontenttest/supplemental/missing_app_name.json b/adapters/revcontent/revcontenttest/supplemental/missing_app_name.json
new file mode 100644
index 00000000..800690fc
--- /dev/null
+++ b/adapters/revcontent/revcontenttest/supplemental/missing_app_name.json
@@ -0,0 +1,23 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [{"w": 300, "h": 50}]
+        }
+      }
+    ],
+    "site": {
+      "id":"test-imp-id"
+    }
+  },
+
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Impression is missing app name or site domain, and must contain one.",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/revcontent/revcontenttest/supplemental/missing_domain.json b/adapters/revcontent/revcontenttest/supplemental/missing_domain.json
new file mode 100644
index 00000000..8b0c68d8
--- /dev/null
+++ b/adapters/revcontent/revcontenttest/supplemental/missing_domain.json
@@ -0,0 +1,23 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [{"w": 300, "h": 50}]
+        }
+      }
+    ],
+    "app": {
+      "id":"test-imp-id"
+    }
+  },
+
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Impression is missing app name or site domain, and must contain one.",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/revcontent/revcontenttest/supplemental/status_400.json b/adapters/revcontent/revcontenttest/supplemental/status_400.json
new file mode 100644
index 00000000..5ded1415
--- /dev/null
+++ b/adapters/revcontent/revcontenttest/supplemental/status_400.json
@@ -0,0 +1,49 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [{"w": 300, "h": 50}]
+        }
+      }
+    ],
+    "site": {
+      "domain": "test.com"
+    }
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://trends.revcontent.com/rtb?userId=1234&apiKey=abcd",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "banner": {
+                "format": [{"w": 300, "h": 50}]
+              }
+            }
+          ],
+          "site": {
+            "domain": "test.com"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 400,
+        "body": {}
+      }
+    }
+  ],
+
+  "expectedMakeBidsErrors": [
+    {
+      "comparison": "literal",
+      "value": "unexpected status code: 400."
+    }
+  ]
+}
diff --git a/adapters/revcontent/revcontenttest/supplemental/status_500.json b/adapters/revcontent/revcontenttest/supplemental/status_500.json
new file mode 100644
index 00000000..3732abbd
--- /dev/null
+++ b/adapters/revcontent/revcontenttest/supplemental/status_500.json
@@ -0,0 +1,49 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [{"w": 300, "h": 50}]
+        }
+      }
+    ],
+    "site": {
+      "domain": "test.com"
+    }
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://trends.revcontent.com/rtb?userId=1234&apiKey=abcd",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "banner": {
+                "format": [{"w": 300, "h": 50}]
+              }
+            }
+          ],
+          "site": {
+            "domain": "test.com"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 500,
+        "body": {}
+      }
+    }
+  ],
+
+  "expectedMakeBidsErrors": [
+    {
+      "comparison": "literal",
+      "value": "unexpected status code: 500."
+    }
+  ]
+}
diff --git a/amp/parse_test.go b/amp/parse_test.go
new file mode 100644
index 00000000..e2c82d71
--- /dev/null
+++ b/amp/parse_test.go
@@ -0,0 +1,168 @@
+package amp
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParseParams(t *testing.T) {
+	var expectedTimeout uint64 = 42
+
+	testCases := []struct {
+		description    string
+		query          string
+		expectedParams Params
+		expectedError  string
+	}{
+		{
+			description:   "Empty",
+			query:         "",
+			expectedError: "AMP requests require an AMP tag_id",
+		},
+		{
+			description: "All Fields",
+			query: "tag_id=anyTagID&account=anyAccount&curl=anyCurl&consent_string=anyConsent&debug=1&__amp_source_origin=anyOrigin" +
+				"&slot=anySlot&timeout=42&h=1&w=2&oh=3&ow=4&ms=10x11,12x13",
+			expectedParams: Params{
+				Account:         "anyAccount",
+				CanonicalURL:    "anyCurl",
+				Consent:         "anyConsent",
+				Debug:           true,
+				Origin:          "anyOrigin",
+				Slot:            "anySlot",
+				StoredRequestID: "anyTagID",
+				Timeout:         &expectedTimeout,
+				Size: Size{
+					Height:         1,
+					OverrideHeight: 3,
+					OverrideWidth:  4,
+					Width:          2,
+					Multisize: []openrtb.Format{
+						{W: 10, H: 11}, {W: 12, H: 13},
+					},
+				},
+			},
+		},
+		{
+			description:    "Integer Values Ignored If Invalid",
+			query:          "tag_id=anyTagID&h=invalid&w=invalid&oh=invalid&ow=invalid&ms=invalid",
+			expectedParams: Params{StoredRequestID: "anyTagID"},
+		},
+		{
+			description:    "consent_string Preferred Over gdpr_consent",
+			query:          "tag_id=anyTagID&consent_string=consent1&gdpr_consent=consent2",
+			expectedParams: Params{StoredRequestID: "anyTagID", Consent: "consent1"},
+		},
+		{
+			description:    "consent_string Preferred Over gdpr_consent - Order Doesn't Matter",
+			query:          "tag_id=anyTagID&gdpr_consent=consent2&consent_string=consent1",
+			expectedParams: Params{StoredRequestID: "anyTagID", Consent: "consent1"},
+		},
+		{
+			description:    "Just gdpr_consent",
+			query:          "tag_id=anyTagID&gdpr_consent=consent2",
+			expectedParams: Params{StoredRequestID: "anyTagID", Consent: "consent2"},
+		},
+		{
+			description:    "Debug 0",
+			query:          "tag_id=anyTagID&debug=0",
+			expectedParams: Params{StoredRequestID: "anyTagID", Debug: false},
+		},
+		{
+			description:    "Debug Ignored If Invalid",
+			query:          "tag_id=anyTagID&debug=invalid",
+			expectedParams: Params{StoredRequestID: "anyTagID", Debug: false},
+		},
+	}
+
+	for _, test := range testCases {
+		httpRequest, err := http.NewRequest("GET", "http://any.url/anypage?"+test.query, nil)
+		assert.NoError(t, err, test.description+":request")
+
+		params, err := ParseParams(httpRequest)
+		assert.Equal(t, test.expectedParams, params, test.description+":params")
+		if test.expectedError == "" {
+			assert.NoError(t, err, test.description+":err")
+		} else {
+			assert.EqualError(t, err, test.expectedError)
+		}
+	}
+}
+
+func TestParseMultisize(t *testing.T) {
+	testCases := []struct {
+		description     string
+		multisize       string
+		expectedFormats []openrtb.Format
+	}{
+		{
+			description:     "Empty",
+			multisize:       "",
+			expectedFormats: nil,
+		},
+		{
+			description:     "One",
+			multisize:       "1x2",
+			expectedFormats: []openrtb.Format{{W: 1, H: 2}},
+		},
+		{
+			description:     "Many",
+			multisize:       "1x2,3x4",
+			expectedFormats: []openrtb.Format{{W: 1, H: 2}, {W: 3, H: 4}},
+		},
+		{
+			// Existing Behavior: The " 3" token in the second size is parsed as 0.
+			description:     "Many With Space - Quirky Result",
+			multisize:       "1x2, 3x4",
+			expectedFormats: []openrtb.Format{{W: 1, H: 2}, {W: 0, H: 4}},
+		},
+		{
+			description:     "One - Zero Size - Ignored",
+			multisize:       "0x0",
+			expectedFormats: nil,
+		},
+		{
+			description:     "Many - Zero Size - All Ignored",
+			multisize:       "0x0,3x4",
+			expectedFormats: nil,
+		},
+		{
+			description:     "One - Extra Dimension - Ignored",
+			multisize:       "1x2x3",
+			expectedFormats: nil,
+		},
+		{
+			description:     "Many - Extra Dimension - All Ignored",
+			multisize:       "1x2x3,4x5",
+			expectedFormats: nil,
+		},
+		{
+			description:     "One - Invalid Values - Ignored",
+			multisize:       "INVALIDxINVALID",
+			expectedFormats: nil,
+		},
+		{
+			description:     "Many - Invalid Values - All Ignored",
+			multisize:       "1x2,INVALIDxINVALID",
+			expectedFormats: nil,
+		},
+		{
+			description:     "One - No Pair - Ignored",
+			multisize:       "INVALID",
+			expectedFormats: nil,
+		},
+		{
+			description:     "Many - No Pair - All Ignored",
+			multisize:       "1x2,INVALID",
+			expectedFormats: nil,
+		},
+	}
+
+	for _, test := range testCases {
+		result := parseMultisize(test.multisize)
+		assert.ElementsMatch(t, test.expectedFormats, result, test.description)
+	}
+}
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 746d436d..97e99c6b 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -1031,14 +1031,12 @@ func getTestBidRequest(nilUser bool, userExt *openrtb_ext.ExtUser, nilRegs bool,
 
 func TestSetEffectiveAmpPubID(t *testing.T) {
 	testPubID := "test-pub"
-	testURLQueryParams := url.Values{}
-	testURLQueryParams.Add("account", testPubID)
 
 	testCases := []struct {
-		description    string
-		req            *openrtb.BidRequest
-		urlQueryParams url.Values
-		expectedPubID  string
+		description   string
+		req           *openrtb.BidRequest
+		account       string
+		expectedPubID string
 	}{
 		{
 			description: "No publisher ID provided",
@@ -1072,7 +1070,7 @@ func TestSetEffectiveAmpPubID(t *testing.T) {
 			expectedPubID: testPubID,
 		},
 		{
-			description: "Publisher ID present in account query parameter",
+			description: "Publisher ID present in account parameter",
 			req: &openrtb.BidRequest{
 				App: &openrtb.App{
 					Publisher: &openrtb.Publisher{
@@ -1080,8 +1078,8 @@ func TestSetEffectiveAmpPubID(t *testing.T) {
 					},
 				},
 			},
-			urlQueryParams: testURLQueryParams,
-			expectedPubID:  testPubID,
+			account:       testPubID,
+			expectedPubID: testPubID,
 		},
 		{
 			description: "req.Site.Publisher present but ID set to empty string",
@@ -1097,7 +1095,7 @@ func TestSetEffectiveAmpPubID(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		setEffectiveAmpPubID(test.req, test.urlQueryParams)
+		setEffectiveAmpPubID(test.req, test.account)
 		if test.req.Site != nil {
 			assert.Equal(t, test.expectedPubID, test.req.Site.Publisher.ID,
 				"should return the expected Publisher ID for test case: %s", test.description)
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index f9a04f25..e13f956b 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -20,7 +20,8 @@ import (
 //
 // It only allows appnexus for GDPR consent
 type permissionsMock struct {
-	personalInfoAllowed bool
+	personalInfoAllowed      bool
+	personalInfoAllowedError error
 }
 
 func (p *permissionsMock) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) {
@@ -32,7 +33,7 @@ func (p *permissionsMock) BidderSyncAllowed(ctx context.Context, bidder openrtb_
 }
 
 func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdpr gdpr.Signal, consent string) (bool, bool, bool, error) {
-	return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowed, nil
+	return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowedError
 }
 
 func assertReq(t *testing.T, bidderRequests []BidderRequest,
@@ -1045,6 +1046,8 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 }
 
 func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
+	tcf1Consent := "BONV8oqONXwgmADACHENAO7pqzAAppY"
+	tcf2Consent := "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA"
 	trueValue, falseValue := true, false
 
 	testCases := []struct {
@@ -1054,6 +1057,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 		gdpr                string
 		gdprConsent         string
 		gdprScrub           bool
+		permissionsError    error
 		userSyncIfAmbiguous bool
 		expectPrivacyLabels metrics.PrivacyLabels
 	}{
@@ -1074,7 +1078,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: &trueValue,
 			gdprHostEnabled:    true,
 			gdpr:               "1",
-			gdprConsent:        "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:        tcf1Consent,
 			gdprScrub:          true,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   true,
@@ -1086,7 +1090,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: &trueValue,
 			gdprHostEnabled:    true,
 			gdpr:               "1",
-			gdprConsent:        "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA",
+			gdprConsent:        tcf2Consent,
 			gdprScrub:          true,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   true,
@@ -1098,7 +1102,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: &trueValue,
 			gdprHostEnabled:    true,
 			gdpr:               "0",
-			gdprConsent:        "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:        tcf1Consent,
 			gdprScrub:          false,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   false,
@@ -1110,7 +1114,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: &trueValue,
 			gdprHostEnabled:    false,
 			gdpr:               "1",
-			gdprConsent:        "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:        tcf1Consent,
 			gdprScrub:          true,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   true,
@@ -1122,7 +1126,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: &falseValue,
 			gdprHostEnabled:    true,
 			gdpr:               "1",
-			gdprConsent:        "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:        tcf1Consent,
 			gdprScrub:          false,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   false,
@@ -1134,7 +1138,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: nil,
 			gdprHostEnabled:    true,
 			gdpr:               "1",
-			gdprConsent:        "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:        tcf1Consent,
 			gdprScrub:          true,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   true,
@@ -1146,7 +1150,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled: nil,
 			gdprHostEnabled:    false,
 			gdpr:               "1",
-			gdprConsent:        "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:        tcf1Consent,
 			gdprScrub:          false,
 			expectPrivacyLabels: metrics.PrivacyLabels{
 				GDPREnforced:   false,
@@ -1158,7 +1162,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled:  nil,
 			gdprHostEnabled:     true,
 			gdpr:                "null",
-			gdprConsent:         "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:         tcf1Consent,
 			gdprScrub:           true,
 			userSyncIfAmbiguous: false,
 			expectPrivacyLabels: metrics.PrivacyLabels{
@@ -1171,7 +1175,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			gdprAccountEnabled:  nil,
 			gdprHostEnabled:     true,
 			gdpr:                "null",
-			gdprConsent:         "BONV8oqONXwgmADACHENAO7pqzAAppY",
+			gdprConsent:         tcf1Consent,
 			gdprScrub:           false,
 			userSyncIfAmbiguous: true,
 			expectPrivacyLabels: metrics.PrivacyLabels{
@@ -1179,6 +1183,19 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 				GDPRTCFVersion: "",
 			},
 		},
+		{
+			description:        "Enforce - error while checking if personal info is allowed",
+			gdprAccountEnabled: nil,
+			gdprHostEnabled:    true,
+			gdpr:               "1",
+			gdprConsent:        tcf1Consent,
+			gdprScrub:          true,
+			permissionsError:   errors.New("Some error"),
+			expectPrivacyLabels: metrics.PrivacyLabels{
+				GDPREnforced:   true,
+				GDPRTCFVersion: metrics.TCFVersionV1,
+			},
+		},
 	}
 
 	for _, test := range testCases {
@@ -1214,7 +1231,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			context.Background(),
 			auctionReq,
 			nil,
-			&permissionsMock{personalInfoAllowed: !test.gdprScrub},
+			&permissionsMock{personalInfoAllowed: !test.gdprScrub, personalInfoAllowedError: test.permissionsError},
 			test.userSyncIfAmbiguous,
 			privacyConfig)
 		result := results[0]
diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go
index ec8920ee..bca8623d 100755
--- a/usersync/usersyncers/syncer_test.go
+++ b/usersync/usersyncers/syncer_test.go
@@ -59,6 +59,7 @@ func TestNewSyncerMap(t *testing.T) {
 			string(openrtb_ext.BidderLogicad):          syncConfig,
 			string(openrtb_ext.BidderLunaMedia):        syncConfig,
 			string(openrtb_ext.BidderMarsmedia):        syncConfig,
+			string(openrtb_ext.BidderMediafuse):        syncConfig,
 			string(openrtb_ext.BidderMgid):             syncConfig,
 			string(openrtb_ext.BidderNanoInteractive):  syncConfig,
 			string(openrtb_ext.BidderNinthDecimal):     syncConfig,
@@ -98,6 +99,7 @@ func TestNewSyncerMap(t *testing.T) {
 		openrtb_ext.BidderAdhese:       true,
 		openrtb_ext.BidderAdoppler:     true,
 		openrtb_ext.BidderAdot:         true,
+		openrtb_ext.BidderAdprime:      true,
 		openrtb_ext.BidderApplogy:      true,
 		openrtb_ext.BidderInMobi:       true,
 		openrtb_ext.BidderKidoz:        true,
@@ -106,11 +108,11 @@ func TestNewSyncerMap(t *testing.T) {
 		openrtb_ext.BidderMobileFuse:   true,
 		openrtb_ext.BidderOrbidder:     true,
 		openrtb_ext.BidderPubnative:    true,
+		openrtb_ext.BidderRevcontent:   true,
 		openrtb_ext.BidderSilverMob:    true,
 		openrtb_ext.BidderSmaato:       true,
 		openrtb_ext.BidderTappx:        true,
 		openrtb_ext.BidderYeahmobi:     true,
-		openrtb_ext.BidderAdprime:      true,
 	}
 
 	for bidder, config := range cfg.Adapters {