Repository: prebid/prebid-server
Pull request #2533 (2023-01-19 02:26:49)

Deprecate events_enabled in favor of events.enabled
Addresses #2479.

Adds instance level config to enable PBS analytics events (`event.enabled`)

- Instance level config overrides account level config. This is because with the current `event_enable` config at account level, we need to create a stored account for every account that we want to enable events for. To avoid that, instance level config can override that. This way if hosting PBS, we can enable events for all accounts without the need for creating stored accounts for each. 
- When using PBS analytics events, I don't see many use cases that events would need to be disabled for an account, but if there's any, instance level `event.enabled` should be removed and events enablement should be managed through stored accounts then.

Example `pbs.yml` config to enable events at instance level:
```
event:
  enabled: true
```

Note: had to make stored account event enable flag pointer bool in order to be able to distinguish between nil stored account (in case stored account is not defined for the account therefore its enable flag being nil) and stored account with enable event flag set to false (for example, in case overriding instance level flag).

![Screen Shot 2022-12-16 at 11 55 22 AM](https://user-images.githubusercontent.com/56240400/209899765-691d7779-2abc-49fa-82f0-7fefe22d4ecd.png)

![Screen Shot 2022-12-16 at 11 56 37 AM](https://user-images.githubusercontent.com/56240400/209899779-460dff02-91b1-44a7-9258-492953210e38.png)

![Screen Shot 2022-12-16 at 11 56 32 AM](https://user-images.githubusercontent.com/56240400/209899783-ab013c1a-bf45-4dbe-8baf-1206ac23a88d.png)


bretg: In the interest of open source harmony, I've updated the [PRD](https://docs.google.com/document/d/1a-U1DytiCTtacM27qH3OZwhPa7bDwQzfpVRHDhyikWw/edit#) to add Req 2.1.19


> This is because with the current event_enable config at account level, we need to create a stored account for every account that we want to enable events for. 

The assumption is that the majority of PBS host companies run in "validate account ID" mode, which requires creation of account info per customer anyhow.

> When using PBS analytics events, I don't see many use cases that events would need to be disabled for an account

Prebid's philosophy has been that publishers should be in control of what happens on their pages. In many scenarios, analytics is taking place from javascript analytics adapters, so by globally enabling events on the server, you run the risk of double-counting wins. In addition, events are an extra request the browser has to invoke, and pubs should be in control of when that happens.

> Instance level config overrides account level config. 

This is unacceptable. The standard is that account-level config always overrides host-level config, and this directly conflicts with the PRD
```
enabled: enable/disable event tracking for this account/installation.
Overrides the configured host level default.
```

The way PBS-Java has implemented account config is that there's a `default-account-config` . This is specifically for account defaults of all types so account-level overrides can be constrained to exceptions from the default. That's a reasonable place to put this flag. I agree that the disjoined `events-enabled` vs `enabled` seems unnecessary, so I've removed the `events` in the default.


shahinrahbariasl: Hi @bretg, thank you for your detailed feedback! Instance level flag overriding account level was from my previous approach in a different PR which I closed, before I learnt about account level standard. That's updated now. 

Regarding account default configs, seems like that feature is not available in PBS-GO yet if I'm not mistaken https://github.com/prebid/prebid-server/blob/da478e319ea6c2c4a723fc752a150de401909535/config/config.go#L136

In PBS-GO, because `account.EventsEanbled` flag is a boolean, if it doesn't exist it evaluates to false therefore /event calls getting rejected https://github.com/prebid/prebid-server/blob/5cc5da91f517b79b6b7d2182994bb57d38813918/endpoints/events/event.go#L108

Hmm, but even if we have default account configs I'd assume by default we have to make events disabled based on the Prebid philosophy you mentioned and possible double-counting of wins. Or maybe we can come up with a solution to avoid that.
shahinrahbariasl: Hey @guscarreon is there any other feedback I can address for this? Please let me know, thanks!
shahinrahbariasl: > The `Events` field that represents the new `"accounts.events.enabled"` flag, also needs to be declared as a pointer.
> 
> ```
> 63   type Events struct {
> 64 -     Enabled    bool        `mapstructure:"enabled" json:"enabled"`
>    +     Enabled    *bool       `mapstructure:"enabled" json:"enabled"`
> 65       DefaultURL string      `mapstructure:"default_url" json:"default_url"`
> 66       VASTEvents []VASTEvent `mapstructure:"vast_events" json:"vast_events,omitempty"`
> 67   }
> config/events.go
> ```
> 
> If not, the legacy field `"events_enabled"` will always be ignored. Notice how the truth table changes if `"events.enabled"` is not a pointer:
> 
> r	`"events_enabled"`	`"events.enabled"`	output
> 1	`false`	`false`	`false`
> 2	`false`	`true`	`true`
> 3	`true`	`false`	`false`
> 4	`true`	`true`	`true`
> 5	not set	`false`	`false`
> 6	not set	`true`	`true`
> The output column above is identical to the `"events.enabled"` value everytime. The correct truth table is shown below (corrected as prer @bsardo's comment). Under this logic the only scenarios where the legacy field `"events_enabled"` is taken into account are rows 3 and 6 when `"events.enabled"` is not set. In other words, if the new `Events` field is not set, we should use the legacy value `"events_enabled"` instead as we expect some incoming requests to still use the deprecated `"events_enabled"` flag and not the new one. Also notice row 9: if neither flags are set (both pointers `nil`), we should default to `false`.
> 
> r	`"events_enabled"`	`"events.enabled"`	output
> 1	`false`	`false`	`false`
> 2	`false`	`true`	`true`
> 3	`false`	not set	`false`
> 4	`true`	`false`	`false`
> 5	`true`	`true`	`true`
> 6	`true`	not set	`true`
> 7	not set	`false`	`false`
> 8	not set	`true`	`true`
> 9	not set	not set	`false`
> I think we should declare `Events` as a `*bool` and modify `IsAccountEventEnabled` and `IsAccountDefaultsEventEnabled` acordingly.
> 
> Also: can `IsAccountEventEnabled` and `IsAccountDefaultsEventEnabled` be one function? They look, and after the changes will still look, very similar.

Hey @guscarreon, that's a good call out. Just to be clear, you mean the `Enabled` field in Events object to be a pointer right?
guscarreon: > > The `Events` field that represents the new `"accounts.events.enabled"` flag, also needs to be declared as a pointer.
> > ```
> > 63   type Events struct {
> > 64 -     Enabled    bool        `mapstructure:"enabled" json:"enabled"`
> >    +     Enabled    *bool       `mapstructure:"enabled" json:"enabled"`
> > 65       DefaultURL string      `mapstructure:"default_url" json:"default_url"`
> > 66       VASTEvents []VASTEvent `mapstructure:"vast_events" json:"vast_events,omitempty"`
> > 67   }
> > config/events.go
> > ```
> > 
> > 
> >     
> >       
> >     
> > 
> >       
> >     
> > 
> >     
> >   
> > If not, the legacy field `"events_enabled"` will always be ignored. Notice how the truth table changes if `"events.enabled"` is not a pointer:
> > r	`"events_enabled"`	`"events.enabled"`	output
> > 1	`false`	`false`	`false`
> > 2	`false`	`true`	`true`
> > 3	`true`	`false`	`false`
> > 4	`true`	`true`	`true`
> > 5	not set	`false`	`false`
> > 6	not set	`true`	`true`
> > The output column above is identical to the `"events.enabled"` value everytime. The correct truth table is shown below (corrected as prer @bsardo's comment). Under this logic the only scenarios where the legacy field `"events_enabled"` is taken into account are rows 3 and 6 when `"events.enabled"` is not set. In other words, if the new `Events` field is not set, we should use the legacy value `"events_enabled"` instead as we expect some incoming requests to still use the deprecated `"events_enabled"` flag and not the new one. Also notice row 9: if neither flags are set (both pointers `nil`), we should default to `false`.
> > r	`"events_enabled"`	`"events.enabled"`	output
> > 1	`false`	`false`	`false`
> > 2	`false`	`true`	`true`
> > 3	`false`	not set	`false`
> > 4	`true`	`false`	`false`
> > 5	`true`	`true`	`true`
> > 6	`true`	not set	`true`
> > 7	not set	`false`	`false`
> > 8	not set	`true`	`true`
> > 9	not set	not set	`false`
> > I think we should declare `Events` as a `*bool` and modify `IsAccountEventEnabled` and `IsAccountDefaultsEventEnabled` acordingly.
> > Also: can `IsAccountEventEnabled` and `IsAccountDefaultsEventEnabled` be one function? They look, and after the changes will still look, very similar.
> 
> Hey @guscarreon, that's a good call out. Just to be clear, you mean the `Enabled` field in Events object to be a pointer similar to `event_enabled` right?

  Yes @shahinrahbariasl 
shahinrahbariasl: > > > The `Events` field that represents the new `"accounts.events.enabled"` flag, also needs to be declared as a pointer.
> > > ```
> > > 63   type Events struct {
> > > 64 -     Enabled    bool        `mapstructure:"enabled" json:"enabled"`
> > >    +     Enabled    *bool       `mapstructure:"enabled" json:"enabled"`
> > > 65       DefaultURL string      `mapstructure:"default_url" json:"default_url"`
> > > 66       VASTEvents []VASTEvent `mapstructure:"vast_events" json:"vast_events,omitempty"`
> > > 67   }
> > > config/events.go
> > > ```
> > > 
> > > 
> > >     
> > >       
> > >     
> > > 
> > >       
> > >     
> > > 
> > >     
> > >   
> > > If not, the legacy field `"events_enabled"` will always be ignored. Notice how the truth table changes if `"events.enabled"` is not a pointer:
> > > r	`"events_enabled"`	`"events.enabled"`	output
> > > 1	`false`	`false`	`false`
> > > 2	`false`	`true`	`true`
> > > 3	`true`	`false`	`false`
> > > 4	`true`	`true`	`true`
> > > 5	not set	`false`	`false`
> > > 6	not set	`true`	`true`
> > > The output column above is identical to the `"events.enabled"` value everytime. The correct truth table is shown below (corrected as prer @bsardo's comment). Under this logic the only scenarios where the legacy field `"events_enabled"` is taken into account are rows 3 and 6 when `"events.enabled"` is not set. In other words, if the new `Events` field is not set, we should use the legacy value `"events_enabled"` instead as we expect some incoming requests to still use the deprecated `"events_enabled"` flag and not the new one. Also notice row 9: if neither flags are set (both pointers `nil`), we should default to `false`.
> > > r	`"events_enabled"`	`"events.enabled"`	output
> > > 1	`false`	`false`	`false`
> > > 2	`false`	`true`	`true`
> > > 3	`false`	not set	`false`
> > > 4	`true`	`false`	`false`
> > > 5	`true`	`true`	`true`
> > > 6	`true`	not set	`true`
> > > 7	not set	`false`	`false`
> > > 8	not set	`true`	`true`
> > > 9	not set	not set	`false`
> > > I think we should declare `Events` as a `*bool` and modify `IsAccountEventEnabled` and `IsAccountDefaultsEventEnabled` acordingly.
> > > Also: can `IsAccountEventEnabled` and `IsAccountDefaultsEventEnabled` be one function? They look, and after the changes will still look, very similar.
> > 
> > 
> > Hey @guscarreon, that's a good call out. Just to be clear, you mean the `Enabled` field in Events object to be a pointer similar to `event_enabled` right?
> 
> Yes @shahinrahbariasl

I think we can also remove this warning since we're now technically taking `cfg.AccountDefaults.Events.Enabled` into consideration as well
https://github.com/prebid/prebid-server/blob/master/config/config.go#L141
guscarreon: Clicked the wrong button and closed by accident. Sorry.
guscarreon: > I think we can also remove this warning since we're now technically taking `cfg.AccountDefaults.Events.Enabled` into consideration as well https://github.com/prebid/prebid-server/blob/master/config/config.go#L141

Good point we should remove that warning.
shahinrahbariasl: > > I think we can also remove this warning since we're now technically taking `cfg.AccountDefaults.Events.Enabled` into consideration as well https://github.com/prebid/prebid-server/blob/master/config/config.go#L141
> 
> Good point we should remove that warning.

Agreed and addressed.
shahinrahbariasl: > LGTM. @shahinrahbariasl thank you for addressing my feedback

No worries, thanks for reviewing. I wasn't fully familiar with the events code but now I am :D
guscarreon: Hi @shahinrahbariasl it seems that there is a conflic that must be resolved before we can merge your PR.

Patch:
diff --git a/account/account.go b/account/account.go
index a32423d1..052d0199 100644
--- a/account/account.go
+++ b/account/account.go
@@ -102,6 +102,10 @@ func GetAccount(ctx context.Context, cfg *config.Configuration, fetcher stored_r
 		})
 		return nil, errs
 	}
+
+	// set the value of events.enabled field based on deprecated events_enabled field and ensure backward compatibility
+	deprecateEventsEnabledField(account)
+
 	return account, nil
 }
 
@@ -252,3 +256,16 @@ func useGDPRChannelEnabled(account *config.Account) bool {
 func useCCPAChannelEnabled(account *config.Account) bool {
 	return account.CCPA.ChannelEnabled.IsSet() && !account.CCPA.IntegrationEnabled.IsSet()
 }
+
+// deprecateEventsEnabledField is responsible for ensuring backwards compatibility of "events_enabled" field.
+// This function favors "events.enabled" field over deprecated "events_enabled" field, if values for both are set.
+// If only deprecated "events_enabled" field is set then it sets the same value to "events.enabled" field.
+func deprecateEventsEnabledField(account *config.Account) {
+	if account != nil {
+		if account.Events.Enabled == nil {
+			account.Events.Enabled = account.EventsEnabled
+		}
+		// assign the old value to the new value so old and new are always the same even though the new value is what is used in the application code.
+		account.EventsEnabled = account.Events.Enabled
+	}
+}
diff --git a/adapters/frvradn/frvradn.go b/adapters/frvradn/frvradn.go
new file mode 100644
index 00000000..3954343e
--- /dev/null
+++ b/adapters/frvradn/frvradn.go
@@ -0,0 +1,151 @@
+package frvradn
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"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"
+	"github.com/prebid/prebid-server/util/httputil"
+	"strings"
+)
+
+type adapter struct {
+	uri string
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	if config.Endpoint == "" {
+		return nil, errors.New("missing endpoint adapter parameter")
+	}
+
+	bidder := &adapter{
+		uri: config.Endpoint,
+	}
+	return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	var requests []*adapters.RequestData
+	var errs []error
+
+	requestCopy := *request
+	for _, imp := range request.Imp {
+		frvrAdnExt, err := getImpressionExt(&imp)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+
+		if imp.BidFloor > 0 && imp.BidFloorCur != "" && strings.ToUpper(imp.BidFloorCur) != "USD" {
+			convertedValue, err := requestInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, "USD")
+			if err != nil {
+				errs = append(errs, err)
+				continue
+			}
+			imp.BidFloorCur = "USD"
+			imp.BidFloor = convertedValue
+		}
+
+		ext, err := json.Marshal(frvrAdnExt)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+		imp.Ext = ext
+
+		requestCopy.Imp = []openrtb2.Imp{imp}
+
+		requestJSON, err := json.Marshal(requestCopy)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+
+		requestData := &adapters.RequestData{
+			Method: "POST",
+			Uri:    a.uri,
+			Body:   requestJSON,
+		}
+		requests = append(requests, requestData)
+	}
+	return requests, errs
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if httputil.IsResponseStatusCodeNoContent(responseData) {
+		return nil, nil
+	}
+
+	if err := httputil.CheckResponseStatusCodeForErrors(responseData); err != nil {
+		return nil, []error{err}
+	}
+
+	if len(responseData.Body) == 0 {
+		return nil, nil
+	}
+
+	var response openrtb2.BidResponse
+	if err := json.Unmarshal(responseData.Body, &response); err != nil {
+		return nil, []error{err}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+	bidResponse.Currency = response.Cur
+	var errs []error
+	for _, seatBid := range response.SeatBid {
+		for i := range seatBid.Bid {
+			bidType, err := getBidMediaType(&seatBid.Bid[i])
+			if err != nil {
+				errs = append(errs, err)
+				continue
+			}
+
+			b := &adapters.TypedBid{
+				Bid:     &seatBid.Bid[i],
+				BidType: bidType,
+			}
+			bidResponse.Bids = append(bidResponse.Bids, b)
+		}
+	}
+	return bidResponse, errs
+}
+
+func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ImpExtFRVRAdn, error) {
+	var extImpBidder adapters.ExtImpBidder
+	if err := json.Unmarshal(imp.Ext, &extImpBidder); err != nil {
+		return nil, &errortypes.BadInput{
+			Message: "missing ext",
+		}
+	}
+	var frvrAdnExt openrtb_ext.ImpExtFRVRAdn
+	if err := json.Unmarshal(extImpBidder.Bidder, &frvrAdnExt); err != nil {
+		return nil, &errortypes.BadInput{
+			Message: "missing ext.bidder",
+		}
+	}
+
+	if len(frvrAdnExt.PublisherID) == 0 || len(frvrAdnExt.AdUnitID) == 0 {
+		return nil, &errortypes.BadInput{
+			Message: "publisher_id and ad_unit_id are required",
+		}
+	}
+	return &frvrAdnExt, nil
+}
+
+func getBidMediaType(bid *openrtb2.Bid) (openrtb_ext.BidType, error) {
+	var extBid openrtb_ext.ExtBid
+	err := json.Unmarshal(bid.Ext, &extBid)
+	if err != nil {
+		return "", fmt.Errorf("unable to deserialize imp %v bid.ext", bid.ImpID)
+	}
+
+	if extBid.Prebid == nil {
+		return "", fmt.Errorf("imp %v with unknown media type", bid.ImpID)
+	}
+
+	return extBid.Prebid.Type, nil
+}
diff --git a/adapters/richaudience/richaudience.go b/adapters/richaudience/richaudience.go
index 71356bf2..16fbe229 100644
--- a/adapters/richaudience/richaudience.go
+++ b/adapters/richaudience/richaudience.go
@@ -52,10 +52,16 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapte
 		}
 
 		if request.App != nil {
+			appCopy := *request.App
+			request.App = &appCopy
+
 			request.App.Keywords = "tagid=" + imp.TagID
 		}
 
 		if request.Site != nil {
+			siteCopy := *request.Site
+			request.Site = &siteCopy
+
 			request.Site.Keywords = "tagid=" + imp.TagID
 		}
 
@@ -65,6 +71,13 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapte
 			}
 
 			if raiExt.Test {
+				if request.Device != nil {
+					deviceCopy := *request.Device
+					request.Device = &deviceCopy
+				} else {
+					request.Device = &openrtb2.Device{}
+				}
+
 				request.Device.IP = "11.222.33.44"
 				request.Test = int8(1)
 			}
diff --git a/config/accounts.go b/config/accounts.go
index 77803309..ff69762a 100644
--- a/config/accounts.go
+++ b/config/accounts.go
@@ -26,7 +26,7 @@ type Account struct {
 	ID                      string                               `mapstructure:"id" json:"id"`
 	Disabled                bool                                 `mapstructure:"disabled" json:"disabled"`
 	CacheTTL                DefaultTTLs                          `mapstructure:"cache_ttl" json:"cache_ttl"`
-	EventsEnabled           bool                                 `mapstructure:"events_enabled" json:"events_enabled"`
+	EventsEnabled           *bool                                `mapstructure:"events_enabled" json:"events_enabled"` // Deprecated: Use events.enabled instead.
 	CCPA                    AccountCCPA                          `mapstructure:"ccpa" json:"ccpa"`
 	GDPR                    AccountGDPR                          `mapstructure:"gdpr" json:"gdpr"`
 	DebugAllow              bool                                 `mapstructure:"debug_allow" json:"debug_allow"`
diff --git a/config/config.go b/config/config.go
index e1d27064..d70afc21 100644
--- a/config/config.go
+++ b/config/config.go
@@ -17,6 +17,7 @@ import (
 	"github.com/prebid/go-gdpr/consentconstants"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/util/ptrutil"
 )
 
 // Configuration specifies the static application config.
@@ -138,14 +139,15 @@ func (cfg *Configuration) validate(v *viper.Viper) []error {
 	if cfg.AccountDefaults.Disabled {
 		glog.Warning(`With account_defaults.disabled=true, host-defined accounts must exist and have "disabled":false. All other requests will be rejected.`)
 	}
-	if cfg.AccountDefaults.Events.Enabled {
-		glog.Warning(`account_defaults.events will currently not do anything as the feature is still under development. Please follow https://github.com/prebid/prebid-server/issues/1725 for more updates`)
-	}
 
 	if cfg.PriceFloors.Enabled {
 		glog.Warning(`cfg.PriceFloors.Enabled will currently not do anything as price floors feature is still under development.`)
 	}
 
+	if len(cfg.AccountDefaults.Events.VASTEvents) > 0 {
+		errs = append(errs, errors.New("account_defaults.Events.VASTEvents has no effect as the feature is under development."))
+	}
+
 	errs = cfg.Experiment.validate(errs)
 	errs = cfg.BidderInfos.validate(errs)
 	return errs
@@ -688,6 +690,10 @@ func New(v *viper.Viper, bidderInfos BidderInfos, normalizeBidderName func(strin
 
 	// Update account defaults and generate base json for patch
 	c.AccountDefaults.CacheTTL = c.CacheURL.DefaultTTLs // comment this out to set explicitly in config
+
+	// Update the deprecated and new events enabled values for account defaults.
+	c.AccountDefaults.EventsEnabled, c.AccountDefaults.Events.Enabled = migrateConfigEventsEnabled(c.AccountDefaults.EventsEnabled, c.AccountDefaults.Events.Enabled)
+
 	if err := c.MarshalAccountDefaults(); err != nil {
 		return nil, err
 	}
@@ -1008,6 +1014,7 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) {
 	v.SetDefault("account_defaults.price_floors.use_dynamic_data", false)
 	v.SetDefault("account_defaults.price_floors.max_rules", 100)
 	v.SetDefault("account_defaults.price_floors.max_schema_dims", 3)
+	v.SetDefault("account_defaults.events_enabled", false)
 
 	v.SetDefault("certificates_file", "")
 	v.SetDefault("auto_gen_source_tid", true)
@@ -1371,6 +1378,28 @@ func migrateConfigDatabaseConnection(v *viper.Viper) {
 	}
 }
 
+// migrateConfigEventsEnabled is responsible for ensuring backward compatibility of events_enabled field.
+// This function copies the value of newField "events.enabled" and set it to the oldField "events_enabled".
+// This is necessary to achieve the desired order of precedence favoring the account values over the host values
+// given the account fetcher JSON merge mechanics.
+func migrateConfigEventsEnabled(oldFieldValue *bool, newFieldValue *bool) (updatedOldFieldValue, updatedNewFieldValue *bool) {
+	newField := "account_defaults.events.enabled"
+	oldField := "account_defaults.events_enabled"
+
+	updatedOldFieldValue = oldFieldValue
+	if oldFieldValue != nil {
+		glog.Warningf("%s is deprecated and should be changed to %s", oldField, newField)
+	}
+	if newFieldValue != nil {
+		if oldFieldValue != nil {
+			glog.Warningf("using %s and ignoring deprecated %s", newField, oldField)
+		}
+		updatedOldFieldValue = ptrutil.ToPtr(*newFieldValue)
+	}
+
+	return updatedOldFieldValue, nil
+}
+
 func isConfigInfoPresent(v *viper.Viper, prefix string, fields []string) bool {
 	prefix = prefix + "."
 	for _, field := range fields {
diff --git a/config/events.go b/config/events.go
index cf3139d8..83d2df4b 100644
--- a/config/events.go
+++ b/config/events.go
@@ -61,14 +61,14 @@ type VASTEvent struct {
 // within the VAST XML
 // Don't enable this feature. It is still under developmment. Please follow https://github.com/prebid/prebid-server/issues/1725 for more updates
 type Events struct {
-	Enabled    bool        `mapstructure:"enabled" json:"enabled"`
+	Enabled    *bool       `mapstructure:"enabled" json:"enabled"`
 	DefaultURL string      `mapstructure:"default_url" json:"default_url"`
 	VASTEvents []VASTEvent `mapstructure:"vast_events" json:"vast_events,omitempty"`
 }
 
 // validate verifies the events object  and returns error if at least one is invalid.
 func (e Events) validate(errs []error) []error {
-	if e.Enabled {
+	if e.IsEnabled() {
 		if !isValidURL(e.DefaultURL) {
 			return append(errs, errors.New("Invalid events.default_url"))
 		}
@@ -147,3 +147,8 @@ func isValidURL(eventURL string) bool {
 func (e VASTEvent) isTrackingEvent() bool {
 	return e.CreateElement == TrackingVASTElement
 }
+
+// IsEnabled function returns the value of events.enabled field
+func (e Events) IsEnabled() bool {
+	return e.Enabled != nil && *e.Enabled
+}
diff --git a/endpoints/events/event.go b/endpoints/events/event.go
index ca323a61..ba77f3ce 100644
--- a/endpoints/events/event.go
+++ b/endpoints/events/event.go
@@ -107,8 +107,8 @@ func (e *eventEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprou
 		return
 	}
 
-	// account does not support events
-	if !account.EventsEnabled {
+	// Check if events are enabled for the account
+	if !account.Events.IsEnabled() {
 		w.WriteHeader(http.StatusUnauthorized)
 		w.Write([]byte(fmt.Sprintf("Account '%s' doesn't support events", eventRequest.AccountID)))
 		return
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 4926cf2c..2811a9e7 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -1708,6 +1708,7 @@ func validateRegs(req *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) []er
 		for _, id := range req.BidRequest.Regs.GPPSID {
 			if id == int8(constants.SectionTCFEU2) {
 				gdpr = 1
+				break
 			}
 		}
 		if gdpr != *req.BidRequest.Regs.GDPR {
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/empty-gppsid-conflict.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/empty-gppsid-conflict.json
new file mode 100644
index 00000000..9f7c9231
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/empty-gppsid-conflict.json
@@ -0,0 +1,56 @@
+{
+  "description": "Well formed amp request with  conflicting (2.6 gdpr consent vs. GPP) gdpr signals",
+  "mockBidRequest": {
+    "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5",
+    "site": {
+      "page": "prebid.org",
+      "publisher": {
+        "id": "a3de7af2-a86a-4043-a77b-c7e86744155e"
+      }
+    },
+    "source": {
+      "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5"
+    },
+    "tmax": 1000,
+    "imp": [
+      {
+        "id": "/19968336/header-bid-tag-0",
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        },
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 300
+            }
+          ]
+        }
+      }
+    ],
+    "regs": {
+      "gpp": "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1NYN",
+      "gpp_sid": [ ],
+      "gdpr": 1,
+      "ext": {
+        "us_privacy": "1YYY"
+      }
+    },
+    "user": {
+      "consent": "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+      "ext": {}
+    }
+  },
+  "expectedBidResponse": {
+      "id":"b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5",
+      "bidid":"test bid id",
+      "nbr":0
+  },
+  "expectedReturnCode": 200
+}
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 784ae8ae..7a2ffd54 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -77,6 +77,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/eplanning"
 	"github.com/prebid/prebid-server/adapters/epom"
 	"github.com/prebid/prebid-server/adapters/freewheelssp"
+	"github.com/prebid/prebid-server/adapters/frvradn"
 	"github.com/prebid/prebid-server/adapters/gamma"
 	"github.com/prebid/prebid-server/adapters/gamoshi"
 	"github.com/prebid/prebid-server/adapters/globalsun"
@@ -254,6 +255,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderEvtech:            limelightDigital.Builder,
 		openrtb_ext.BidderFreewheelSSP:      freewheelssp.Builder,
 		openrtb_ext.BidderFreewheelSSPOld:   freewheelssp.Builder,
+		openrtb_ext.BidderFRVRAdNetwork:     frvradn.Builder,
 		openrtb_ext.BidderGamma:             gamma.Builder,
 		openrtb_ext.BidderGamoshi:           gamoshi.Builder,
 		openrtb_ext.BidderGlobalsun:         globalsun.Builder,
diff --git a/exchange/bidder.go b/exchange/bidder.go
index 58dc1789..8e6f133f 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -520,6 +520,7 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re
 		ctx = bidder.addClientTrace(ctx)
 	}
 	bidder.me.RecordOverheadTime(metrics.PreBidder, time.Since(pbsRequestStartTime))
+	httpCallStart := time.Now()
 	httpResp, err := ctxhttp.Do(ctx, bidder.Client, httpReq)
 	if err != nil {
 		if err == context.DeadlineExceeded {
@@ -560,6 +561,7 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re
 		}
 	}
 
+	bidder.me.RecordBidderServerResponseTime(time.Since(httpCallStart))
 	return &httpCallInfo{
 		request: req,
 		response: &adapters.ResponseData{
diff --git a/exchange/events.go b/exchange/events.go
index 73ca6ec9..fd52d6b6 100644
--- a/exchange/events.go
+++ b/exchange/events.go
@@ -28,7 +28,7 @@ type eventTracking struct {
 func getEventTracking(requestExtPrebid *openrtb_ext.ExtRequestPrebid, ts time.Time, account *config.Account, bidderInfos config.BidderInfos, externalURL string) *eventTracking {
 	return &eventTracking{
 		accountID:          account.ID,
-		enabledForAccount:  account.EventsEnabled,
+		enabledForAccount:  account.Events.IsEnabled(),
 		enabledForRequest:  requestExtPrebid != nil && requestExtPrebid.Events != nil,
 		auctionTimestampMs: ts.UnixNano() / 1e+6,
 		integrationType:    getIntegrationType(requestExtPrebid),
@@ -60,7 +60,7 @@ func (ev *eventTracking) modifyBidsForEvents(seatBids map[openrtb_ext.BidderName
 
 // isModifyingVASTXMLAllowed returns true if this bidder config allows modifying VAST XML for event tracking
 func (ev *eventTracking) isModifyingVASTXMLAllowed(bidderName string) bool {
-	return ev.bidderInfos[bidderName].ModifyingVastXmlAllowed && (ev.enabledForAccount || ev.enabledForRequest)
+	return ev.bidderInfos[bidderName].ModifyingVastXmlAllowed && ev.isEventAllowed()
 }
 
 // modifyBidVAST injects event Impression url if needed, otherwise returns original VAST string
@@ -81,7 +81,7 @@ func (ev *eventTracking) modifyBidVAST(pbsBid *entities.PbsOrtbBid, bidderName o
 
 // modifyBidJSON injects "wurl" (win) event url if needed, otherwise returns original json
 func (ev *eventTracking) modifyBidJSON(pbsBid *entities.PbsOrtbBid, bidderName openrtb_ext.BidderName, jsonBytes []byte) ([]byte, error) {
-	if !(ev.enabledForAccount || ev.enabledForRequest) || pbsBid.BidType == openrtb_ext.BidTypeVideo {
+	if !ev.isEventAllowed() || pbsBid.BidType == openrtb_ext.BidTypeVideo {
 		return jsonBytes, nil
 	}
 	var winEventURL string
@@ -104,7 +104,7 @@ func (ev *eventTracking) modifyBidJSON(pbsBid *entities.PbsOrtbBid, bidderName o
 
 // makeBidExtEvents make the data for bid.ext.prebid.events if needed, otherwise returns nil
 func (ev *eventTracking) makeBidExtEvents(pbsBid *entities.PbsOrtbBid, bidderName openrtb_ext.BidderName) *openrtb_ext.ExtBidPrebidEvents {
-	if !(ev.enabledForAccount || ev.enabledForRequest) || pbsBid.BidType == openrtb_ext.BidTypeVideo {
+	if !ev.isEventAllowed() || pbsBid.BidType == openrtb_ext.BidTypeVideo {
 		return nil
 	}
 	return &openrtb_ext.ExtBidPrebidEvents{
@@ -129,3 +129,8 @@ func (ev *eventTracking) makeEventURL(evType analytics.EventType, pbsBid *entiti
 			Integration: ev.integrationType,
 		})
 }
+
+// isEnabled checks if events are enabled by default or on account/request level
+func (ev *eventTracking) isEventAllowed() bool {
+	return ev.enabledForAccount || ev.enabledForRequest
+}
diff --git a/metrics/config/metrics.go b/metrics/config/metrics.go
index 95e2614d..0cf0533d 100644
--- a/metrics/config/metrics.go
+++ b/metrics/config/metrics.go
@@ -149,6 +149,12 @@ func (me *MultiMetricsEngine) RecordTLSHandshakeTime(tlsHandshakeTime time.Durat
 	}
 }
 
+func (me *MultiMetricsEngine) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) {
+	for _, thisME := range *me {
+		thisME.RecordBidderServerResponseTime(bidderServerResponseTime)
+	}
+}
+
 // RecordAdapterBidReceived across all engines
 func (me *MultiMetricsEngine) RecordAdapterBidReceived(labels metrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
 	for _, thisME := range *me {
@@ -425,6 +431,10 @@ func (me *NilMetricsEngine) RecordDNSTime(dnsLookupTime time.Duration) {
 func (me *NilMetricsEngine) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) {
 }
 
+// RecordBidderServerResponseTime as a noop
+func (me *NilMetricsEngine) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) {
+}
+
 // RecordAdapterBidReceived as a noop
 func (me *NilMetricsEngine) RecordAdapterBidReceived(labels metrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
 }
diff --git a/metrics/go_metrics.go b/metrics/go_metrics.go
index 44f2ecd7..64726e30 100644
--- a/metrics/go_metrics.go
+++ b/metrics/go_metrics.go
@@ -31,6 +31,7 @@ type Metrics struct {
 	AccountCacheMeter              map[CacheResult]metrics.Meter
 	DNSLookupTimer                 metrics.Timer
 	TLSHandshakeTimer              metrics.Timer
+	BidderServerResponseTimer      metrics.Timer
 	StoredResponsesMeter           metrics.Meter
 
 	// Metrics for OpenRTB requests specifically
@@ -219,7 +220,8 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 		exchanges: exchanges,
 		modules:   getModuleNames(moduleStageNames),
 
-		OverheadTimer: makeBlankOverheadTimerMetrics(),
+		OverheadTimer:             makeBlankOverheadTimerMetrics(),
+		BidderServerResponseTimer: blankTimer,
 	}
 
 	for _, a := range exchanges {
@@ -305,6 +307,7 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d
 	newMetrics.PrebidCacheRequestTimerError = metrics.GetOrRegisterTimer("prebid_cache_request_time.err", registry)
 	newMetrics.StoredResponsesMeter = metrics.GetOrRegisterMeter("stored_responses", registry)
 	newMetrics.OverheadTimer = makeOverheadTimerMetrics(registry)
+	newMetrics.BidderServerResponseTimer = metrics.GetOrRegisterTimer("bidder_server_response_time_seconds", registry)
 
 	for _, dt := range StoredDataTypes() {
 		for _, ft := range StoredDataFetchTypes() {
@@ -811,6 +814,10 @@ func (me *Metrics) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) {
 	me.TLSHandshakeTimer.Update(tlsHandshakeTime)
 }
 
+func (me *Metrics) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) {
+	me.BidderServerResponseTimer.Update(bidderServerResponseTime)
+}
+
 // RecordAdapterBidReceived implements a part of the MetricsEngine interface.
 // This tracks how many bids from each Bidder use `adm` vs. `nurl.
 func (me *Metrics) RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
diff --git a/metrics/metrics.go b/metrics/metrics.go
index 186e1ffa..214e7c1d 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -430,6 +430,7 @@ type MetricsEngine interface {
 	RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration)
 	RecordDNSTime(dnsLookupTime time.Duration)
 	RecordTLSHandshakeTime(tlsHandshakeTime time.Duration)
+	RecordBidderServerResponseTime(bidderServerResponseTime time.Duration)
 	RecordAdapterPanic(labels AdapterLabels)
 	RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool)
 	RecordAdapterPrice(labels AdapterLabels, cpm float64)
diff --git a/metrics/metrics_mock.go b/metrics/metrics_mock.go
index cbca8570..0ce238c5 100644
--- a/metrics/metrics_mock.go
+++ b/metrics/metrics_mock.go
@@ -71,6 +71,11 @@ func (me *MetricsEngineMock) RecordTLSHandshakeTime(tlsHandshakeTime time.Durati
 	me.Called(tlsHandshakeTime)
 }
 
+// RecordBidderServerResponseTime mock
+func (me *MetricsEngineMock) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) {
+	me.Called(bidderServerResponseTime)
+}
+
 // RecordAdapterBidReceived mock
 func (me *MetricsEngineMock) RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
 	me.Called(labels, bidType, hasAdm)
diff --git a/metrics/prometheus/prometheus.go b/metrics/prometheus/prometheus.go
index ee11dccf..eb387789 100644
--- a/metrics/prometheus/prometheus.go
+++ b/metrics/prometheus/prometheus.go
@@ -55,6 +55,7 @@ type Metrics struct {
 	storedResponsesErrors        *prometheus.CounterVec
 	adsCertRequests              *prometheus.CounterVec
 	adsCertSignTimer             prometheus.Histogram
+	bidderServerResponseTimer    prometheus.Histogram
 
 	// Adapter Metrics
 	adapterBids                           *prometheus.CounterVec
@@ -440,6 +441,11 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet
 		[]string{adapterLabel},
 		standardTimeBuckets)
 
+	metrics.bidderServerResponseTimer = newHistogram(cfg, reg,
+		"bidder_server_response_time_seconds",
+		"Duration needed to send HTTP request and receive response back from bidder server.",
+		standardTimeBuckets)
+
 	metrics.syncerRequests = newCounter(cfg, reg,
 		"syncer_requests",
 		"Count of cookie sync requests where a syncer is a candidate to be synced labeled by syncer key and status.",
@@ -893,6 +899,10 @@ func (m *Metrics) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) {
 	m.tlsHandhakeTimer.Observe(tlsHandshakeTime.Seconds())
 }
 
+func (m *Metrics) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) {
+	m.bidderServerResponseTimer.Observe(bidderServerResponseTime.Seconds())
+}
+
 func (m *Metrics) RecordAdapterPanic(labels metrics.AdapterLabels) {
 	m.adapterPanics.With(prometheus.Labels{
 		adapterLabel: string(labels.Adapter),
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 90f80d4f..a5a1ecff 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -164,6 +164,7 @@ const (
 	BidderEvtech            BidderName = "evtech"
 	BidderFreewheelSSP      BidderName = "freewheelssp"
 	BidderFreewheelSSPOld   BidderName = "freewheel-ssp"
+	BidderFRVRAdNetwork     BidderName = "frvradn"
 	BidderGamma             BidderName = "gamma"
 	BidderGamoshi           BidderName = "gamoshi"
 	BidderGlobalsun         BidderName = "globalsun"
@@ -350,6 +351,7 @@ func CoreBidderNames() []BidderName {
 		BidderEvtech,
 		BidderFreewheelSSP,
 		BidderFreewheelSSPOld,
+		BidderFRVRAdNetwork,
 		BidderGamma,
 		BidderGamoshi,
 		BidderGlobalsun,
diff --git a/openrtb_ext/imp_frvradn.go b/openrtb_ext/imp_frvradn.go
new file mode 100644
index 00000000..ac8f86b2
--- /dev/null
+++ b/openrtb_ext/imp_frvradn.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+type ImpExtFRVRAdn struct {
+	PublisherID string `json:"publisher_id"`
+	AdUnitID    string `json:"ad_unit_id"`
+}
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index 80a678fb..5e222ab7 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -5,6 +5,9 @@ import (
 	"fmt"
 
 	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/util/maputil"
+	"github.com/prebid/prebid-server/util/ptrutil"
+	"github.com/prebid/prebid-server/util/sliceutil"
 )
 
 // FirstPartyDataExtKey defines a field name within request.ext and request.imp.ext reserved for first party data.
@@ -314,3 +317,199 @@ func (m ExtMultiBid) String() string {
 	}
 	return fmt.Sprintf("{Bidder:%s, Bidders:%v, MaxBids:%s, TargetBidderCodePrefix:%s}", m.Bidder, m.Bidders, maxBid, m.TargetBidderCodePrefix)
 }
+
+func (erp *ExtRequestPrebid) Clone() *ExtRequestPrebid {
+	if erp == nil {
+		return nil
+	}
+
+	clone := *erp
+	clone.Aliases = maputil.Clone(erp.Aliases)
+	clone.AliasGVLIDs = maputil.Clone(erp.AliasGVLIDs)
+	clone.BidAdjustmentFactors = maputil.Clone(erp.BidAdjustmentFactors)
+
+	if erp.BidderConfigs != nil {
+		clone.BidderConfigs = make([]BidderConfig, len(erp.BidderConfigs))
+		for i, bc := range erp.BidderConfigs {
+			clonedBidderConfig := BidderConfig{Bidders: sliceutil.Clone(bc.Bidders)}
+			if bc.Config != nil {
+				config := &Config{}
+				if bc.Config.ORTB2 != nil {
+					config.ORTB2 = &ORTB2{
+						Site: maputil.Clone(bc.Config.ORTB2.Site),
+						App:  maputil.Clone(bc.Config.ORTB2.App),
+						User: maputil.Clone(bc.Config.ORTB2.User),
+					}
+				}
+				clonedBidderConfig.Config = config
+			}
+			clone.BidderConfigs[i] = clonedBidderConfig
+		}
+	}
+
+	if erp.Cache != nil {
+		clone.Cache = &ExtRequestPrebidCache{}
+		if erp.Cache.Bids != nil {
+			clone.Cache.Bids = &ExtRequestPrebidCacheBids{}
+			clone.Cache.Bids.ReturnCreative = ptrutil.Clone(erp.Cache.Bids.ReturnCreative)
+		}
+		if erp.Cache.VastXML != nil {
+			clone.Cache.VastXML = &ExtRequestPrebidCacheVAST{}
+			clone.Cache.VastXML.ReturnCreative = ptrutil.Clone(erp.Cache.VastXML.ReturnCreative)
+		}
+	}
+
+	if erp.Channel != nil {
+		channel := *erp.Channel
+		clone.Channel = &channel
+	}
+
+	if erp.CurrencyConversions != nil {
+		newConvRates := make(map[string]map[string]float64, len(erp.CurrencyConversions.ConversionRates))
+		for key, val := range erp.CurrencyConversions.ConversionRates {
+			newConvRates[key] = maputil.Clone(val)
+		}
+		clone.CurrencyConversions = &ExtRequestCurrency{ConversionRates: newConvRates}
+		if erp.CurrencyConversions.UsePBSRates != nil {
+			clone.CurrencyConversions.UsePBSRates = ptrutil.ToPtr(*erp.CurrencyConversions.UsePBSRates)
+		}
+	}
+
+	if erp.Data != nil {
+		clone.Data = &ExtRequestPrebidData{Bidders: sliceutil.Clone(erp.Data.Bidders)}
+		if erp.Data.EidPermissions != nil {
+			newEidPermissions := make([]ExtRequestPrebidDataEidPermission, len(erp.Data.EidPermissions))
+			for i, eidp := range erp.Data.EidPermissions {
+				newEidPermissions[i] = ExtRequestPrebidDataEidPermission{
+					Source:  eidp.Source,
+					Bidders: sliceutil.Clone(eidp.Bidders),
+				}
+			}
+			clone.Data.EidPermissions = newEidPermissions
+		}
+	}
+
+	if erp.Experiment != nil {
+		clone.Experiment = &Experiment{}
+		if erp.Experiment.AdsCert != nil {
+			clone.Experiment.AdsCert = ptrutil.ToPtr(*erp.Experiment.AdsCert)
+		}
+	}
+
+	if erp.MultiBid != nil {
+		clone.MultiBid = make([]*ExtMultiBid, len(erp.MultiBid))
+		for i, mulBid := range erp.MultiBid {
+			newMulBid := &ExtMultiBid{
+				Bidder:                 mulBid.Bidder,
+				Bidders:                sliceutil.Clone(mulBid.Bidders),
+				TargetBidderCodePrefix: mulBid.TargetBidderCodePrefix,
+			}
+			if mulBid.MaxBids != nil {
+				newMulBid.MaxBids = ptrutil.ToPtr(*mulBid.MaxBids)
+			}
+			clone.MultiBid[i] = newMulBid
+		}
+	}
+
+	if erp.SChains != nil {
+		clone.SChains = make([]*ExtRequestPrebidSChain, len(erp.SChains))
+		for i, schain := range erp.SChains {
+			newChain := *schain
+			newNodes := sliceutil.Clone(schain.SChain.Nodes)
+			for j, node := range newNodes {
+				if node.HP != nil {
+					newNodes[j].HP = ptrutil.ToPtr(*newNodes[j].HP)
+				}
+			}
+			newChain.SChain.Nodes = newNodes
+			clone.SChains[i] = &newChain
+		}
+	}
+
+	clone.Server = ptrutil.Clone(erp.Server)
+
+	clone.StoredRequest = ptrutil.Clone(erp.StoredRequest)
+
+	if erp.Targeting != nil {
+		newTargeting := &ExtRequestTargeting{
+			IncludeFormat:     erp.Targeting.IncludeFormat,
+			DurationRangeSec:  sliceutil.Clone(erp.Targeting.DurationRangeSec),
+			PreferDeals:       erp.Targeting.PreferDeals,
+			AppendBidderNames: erp.Targeting.AppendBidderNames,
+		}
+		if erp.Targeting.PriceGranularity != nil {
+			newPriceGranularity := &PriceGranularity{
+				Ranges: sliceutil.Clone(erp.Targeting.PriceGranularity.Ranges),
+			}
+			newPriceGranularity.Precision = ptrutil.Clone(erp.Targeting.PriceGranularity.Precision)
+			newTargeting.PriceGranularity = newPriceGranularity
+		}
+		newTargeting.IncludeWinners = ptrutil.Clone(erp.Targeting.IncludeWinners)
+		newTargeting.IncludeBidderKeys = ptrutil.Clone(erp.Targeting.IncludeBidderKeys)
+		if erp.Targeting.IncludeBrandCategory != nil {
+			newIncludeBrandCategory := *erp.Targeting.IncludeBrandCategory
+			newIncludeBrandCategory.TranslateCategories = ptrutil.Clone(erp.Targeting.IncludeBrandCategory.TranslateCategories)
+			newTargeting.IncludeBrandCategory = &newIncludeBrandCategory
+		}
+		clone.Targeting = newTargeting
+	}
+
+	clone.NoSale = sliceutil.Clone(erp.NoSale)
+
+	if erp.AlternateBidderCodes != nil {
+		newAlternateBidderCodes := ExtAlternateBidderCodes{Enabled: erp.AlternateBidderCodes.Enabled}
+		if erp.AlternateBidderCodes.Bidders != nil {
+			newBidders := make(map[string]ExtAdapterAlternateBidderCodes, len(erp.AlternateBidderCodes.Bidders))
+			for key, val := range erp.AlternateBidderCodes.Bidders {
+				newBidders[key] = ExtAdapterAlternateBidderCodes{
+					Enabled:            val.Enabled,
+					AllowedBidderCodes: sliceutil.Clone(val.AllowedBidderCodes),
+				}
+			}
+			newAlternateBidderCodes.Bidders = newBidders
+		}
+		clone.AlternateBidderCodes = &newAlternateBidderCodes
+	}
+
+	if erp.Floors != nil {
+		clonedFloors := *erp.Floors
+		clonedFloors.Location = ptrutil.Clone(erp.Floors.Location)
+		if erp.Floors.Data != nil {
+			clonedData := *erp.Floors.Data
+			if erp.Floors.Data.ModelGroups != nil {
+				clonedData.ModelGroups = make([]PriceFloorModelGroup, len(erp.Floors.Data.ModelGroups))
+				for i, pfmg := range erp.Floors.Data.ModelGroups {
+					clonedData.ModelGroups[i] = pfmg
+					clonedData.ModelGroups[i].ModelWeight = ptrutil.Clone(pfmg.ModelWeight)
+					clonedData.ModelGroups[i].Schema.Fields = sliceutil.Clone(pfmg.Schema.Fields)
+					clonedData.ModelGroups[i].Values = maputil.Clone(pfmg.Values)
+				}
+			}
+			clonedFloors.Data = &clonedData
+		}
+		if erp.Floors.Enforcement != nil {
+			clonedFloors.Enforcement = &PriceFloorEnforcement{
+				EnforceJS:     ptrutil.Clone(erp.Floors.Enforcement.EnforceJS),
+				EnforcePBS:    ptrutil.Clone(erp.Floors.Enforcement.EnforcePBS),
+				FloorDeals:    ptrutil.Clone(erp.Floors.Enforcement.FloorDeals),
+				BidAdjustment: ptrutil.Clone(erp.Floors.Enforcement.BidAdjustment),
+				EnforceRate:   erp.Floors.Enforcement.EnforceRate,
+			}
+		}
+		clonedFloors.Enabled = ptrutil.Clone(erp.Floors.Enabled)
+		clonedFloors.Skipped = ptrutil.Clone(erp.Floors.Skipped)
+		clone.Floors = &clonedFloors
+	}
+	if erp.MultiBidMap != nil {
+		clone.MultiBidMap = make(map[string]ExtMultiBid, len(erp.MultiBidMap))
+		for k, v := range erp.MultiBidMap {
+			// Make v a deep copy of the ExtMultiBid struct
+			v.Bidders = sliceutil.Clone(v.Bidders)
+			v.MaxBids = ptrutil.Clone(v.MaxBids)
+			clone.MultiBidMap[k] = v
+		}
+	}
+	clone.AdServerTargeting = sliceutil.Clone(erp.AdServerTargeting)
+
+	return &clone
+}
diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go
index 8fe33fff..5f5636d6 100644
--- a/openrtb_ext/request_wrapper.go
+++ b/openrtb_ext/request_wrapper.go
@@ -5,6 +5,9 @@ import (
 	"errors"
 
 	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/util/maputil"
+	"github.com/prebid/prebid-server/util/ptrutil"
+	"github.com/prebid/prebid-server/util/sliceutil"
 )
 
 // RequestWrapper wraps the OpenRTB request to provide a storage location for unmarshalled ext fields, so they
@@ -351,6 +354,27 @@ func (rw *RequestWrapper) rebuildSourceExt() error {
 	return nil
 }
 
+func (rw *RequestWrapper) Clone() *RequestWrapper {
+	if rw == nil {
+		return nil
+	}
+	clone := *rw
+	newImpWrappers := make([]*ImpWrapper, len(rw.impWrappers))
+	for i, iw := range rw.impWrappers {
+		newImpWrappers[i] = iw.Clone()
+	}
+	clone.impWrappers = newImpWrappers
+	clone.userExt = rw.userExt.Clone()
+	clone.deviceExt = rw.deviceExt.Clone()
+	clone.requestExt = rw.requestExt.Clone()
+	clone.appExt = rw.appExt.Clone()
+	clone.regExt = rw.regExt.Clone()
+	clone.siteExt = rw.siteExt.Clone()
+	clone.sourceExt = rw.sourceExt.Clone()
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // UserExt provides an interface for request.user.ext
 // ---------------------------------------------------------------
@@ -606,6 +630,43 @@ func (ue *UserExt) SetEid(eid *[]openrtb2.EID) {
 	ue.eidsDirty = true
 }
 
+func (ue *UserExt) Clone() *UserExt {
+	if ue == nil {
+		return nil
+	}
+	clone := *ue
+	clone.ext = maputil.Clone(ue.ext)
+
+	if ue.consent != nil {
+		clonedConsent := *ue.consent
+		clone.consent = &clonedConsent
+	}
+
+	if ue.prebid != nil {
+		clone.prebid = &ExtUserPrebid{}
+		clone.prebid.BuyerUIDs = maputil.Clone(ue.prebid.BuyerUIDs)
+	}
+
+	if ue.eids != nil {
+		clonedEids := make([]openrtb2.EID, len(*ue.eids))
+		for i, eid := range *ue.eids {
+			newEid := eid
+			newEid.UIDs = sliceutil.Clone(eid.UIDs)
+			clonedEids[i] = newEid
+		}
+		clone.eids = &clonedEids
+	}
+
+	if ue.consentedProvidersSettingsIn != nil {
+		clone.consentedProvidersSettingsIn = &ConsentedProvidersSettingsIn{ConsentedProvidersString: ue.consentedProvidersSettingsIn.ConsentedProvidersString}
+	}
+	if ue.consentedProvidersSettingsOut != nil {
+		clone.consentedProvidersSettingsOut = &ConsentedProvidersSettingsOut{ConsentedProvidersList: sliceutil.Clone(ue.consentedProvidersSettingsOut.ConsentedProvidersList)}
+	}
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // RequestExt provides an interface for request.ext
 // ---------------------------------------------------------------
@@ -741,6 +802,23 @@ func (re *RequestExt) SetSChain(schain *openrtb2.SupplyChain) {
 	re.schainDirty = true
 }
 
+func (re *RequestExt) Clone() *RequestExt {
+	if re == nil {
+		return nil
+	}
+
+	clone := *re
+	clone.ext = maputil.Clone(re.ext)
+
+	if re.prebid != nil {
+		clone.prebid = re.prebid.Clone()
+	}
+
+	clone.schain = cloneSupplyChain(re.schain)
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // DeviceExt provides an interface for request.device.ext
 // ---------------------------------------------------------------
@@ -837,6 +915,26 @@ func (de *DeviceExt) SetPrebid(prebid *ExtDevicePrebid) {
 	de.prebidDirty = true
 }
 
+func (de *DeviceExt) Clone() *DeviceExt {
+	if de == nil {
+		return nil
+	}
+
+	clone := *de
+	clone.ext = maputil.Clone(de.ext)
+
+	if de.prebid != nil {
+		clonedPrebid := *de.prebid
+		if clonedPrebid.Interstitial != nil {
+			clonedInterstitial := *de.prebid.Interstitial
+			clonedPrebid.Interstitial = &clonedInterstitial
+		}
+		clone.prebid = &clonedPrebid
+	}
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // AppExt provides an interface for request.app.ext
 // ---------------------------------------------------------------
@@ -929,6 +1027,19 @@ func (ae *AppExt) SetPrebid(prebid *ExtAppPrebid) {
 	ae.prebidDirty = true
 }
 
+func (ae *AppExt) Clone() *AppExt {
+	if ae == nil {
+		return nil
+	}
+
+	clone := *ae
+	clone.ext = maputil.Clone(ae.ext)
+
+	clone.prebid = ptrutil.Clone(ae.prebid)
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // RegExt provides an interface for request.regs.ext
 // ---------------------------------------------------------------
@@ -1045,6 +1156,19 @@ func (re *RegExt) SetUSPrivacy(usPrivacy string) {
 	re.usPrivacyDirty = true
 }
 
+func (re *RegExt) Clone() *RegExt {
+	if re == nil {
+		return nil
+	}
+
+	clone := *re
+	clone.ext = maputil.Clone(re.ext)
+
+	clone.gdpr = ptrutil.Clone(re.gdpr)
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // SiteExt provides an interface for request.site.ext
 // ---------------------------------------------------------------
@@ -1128,6 +1252,18 @@ func (se *SiteExt) SetAmp(amp *int8) {
 	se.ampDirty = true
 }
 
+func (se *SiteExt) Clone() *SiteExt {
+	if se == nil {
+		return nil
+	}
+
+	clone := *se
+	clone.ext = maputil.Clone(se.ext)
+	clone.amp = ptrutil.Clone(se.amp)
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // SourceExt provides an interface for request.source.ext
 // ---------------------------------------------------------------
@@ -1219,6 +1355,19 @@ func (se *SourceExt) SetSChain(schain *openrtb2.SupplyChain) {
 	se.schainDirty = true
 }
 
+func (se *SourceExt) Clone() *SourceExt {
+	if se == nil {
+		return nil
+	}
+
+	clone := *se
+	clone.ext = maputil.Clone(se.ext)
+
+	clone.schain = cloneSupplyChain(se.schain)
+
+	return &clone
+}
+
 // ImpWrapper wraps an OpenRTB impression object to provide storage for unmarshalled ext fields, so they
 // will not need to be unmarshalled multiple times. It is intended to use the ImpWrapper via the RequestWrapper
 // and follow the same usage conventions.
@@ -1265,6 +1414,17 @@ func (w *ImpWrapper) rebuildImpExt() error {
 	return nil
 }
 
+func (w *ImpWrapper) Clone() *ImpWrapper {
+	if w == nil {
+		return nil
+	}
+
+	clone := *w
+	clone.impExt = w.impExt.Clone()
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // ImpExt provides an interface for imp.ext
 // ---------------------------------------------------------------
@@ -1429,3 +1589,32 @@ func (e *ImpExt) GetGpId() string {
 func CreateImpExtForTesting(ext map[string]json.RawMessage, prebid *ExtImpPrebid) ImpExt {
 	return ImpExt{ext: ext, prebid: prebid}
 }
+
+func (e *ImpExt) Clone() *ImpExt {
+	if e == nil {
+		return nil
+	}
+
+	clone := *e
+	clone.ext = maputil.Clone(e.ext)
+
+	if e.prebid != nil {
+		clonedPrebid := *e.prebid
+		clonedPrebid.StoredRequest = ptrutil.Clone(e.prebid.StoredRequest)
+		clonedPrebid.StoredAuctionResponse = ptrutil.Clone(e.prebid.StoredAuctionResponse)
+		if e.prebid.StoredBidResponse != nil {
+			clonedPrebid.StoredBidResponse = make([]ExtStoredBidResponse, len(e.prebid.StoredBidResponse))
+			for i, sbr := range e.prebid.StoredBidResponse {
+				clonedPrebid.StoredBidResponse[i] = sbr
+				clonedPrebid.StoredBidResponse[i].ReplaceImpId = ptrutil.Clone(sbr.ReplaceImpId)
+			}
+		}
+		clonedPrebid.IsRewardedInventory = ptrutil.Clone(e.prebid.IsRewardedInventory)
+		clonedPrebid.Bidder = maputil.Clone(e.prebid.Bidder)
+		clonedPrebid.Options = ptrutil.Clone(e.prebid.Options)
+		clonedPrebid.Floors = ptrutil.Clone(e.prebid.Floors)
+		clone.prebid = &clonedPrebid
+	}
+
+	return &clone
+}
diff --git a/openrtb_ext/supplyChain.go b/openrtb_ext/supplyChain.go
new file mode 100644
index 00000000..6f023542
--- /dev/null
+++ b/openrtb_ext/supplyChain.go
@@ -0,0 +1,21 @@
+package openrtb_ext
+
+import (
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/util/ptrutil"
+)
+
+func cloneSupplyChain(schain *openrtb2.SupplyChain) *openrtb2.SupplyChain {
+	if schain == nil {
+		return nil
+	}
+	clone := *schain
+	clone.Nodes = make([]openrtb2.SupplyChainNode, len(schain.Nodes))
+	for i, node := range schain.Nodes {
+		clone.Nodes[i] = node
+		clone.Nodes[i].HP = ptrutil.Clone(schain.Nodes[i].HP)
+	}
+
+	return &clone
+
+}
diff --git a/static/bidder-info/adview.yaml b/static/bidder-info/adview.yaml
index 53158cf4..2871653f 100644
--- a/static/bidder-info/adview.yaml
+++ b/static/bidder-info/adview.yaml
@@ -2,6 +2,8 @@ endpoint: "https://bid.adview.com/agent/thirdAdxService/{{.AccountID}}"
 maintainer:
   email: "partner@adview.com"
 gvlVendorID: 1022
+endpointCompression: gzip
+modifyingVastXmlAllowed: true
 capabilities:
   app:
     mediaTypes:
diff --git a/static/bidder-info/frvradn.yaml b/static/bidder-info/frvradn.yaml
new file mode 100644
index 00000000..b5b2e7f2
--- /dev/null
+++ b/static/bidder-info/frvradn.yaml
@@ -0,0 +1,18 @@
+endpoint: "https://fran.frvr.com/api/v1/openrtb"
+maintainer:
+  email: "info@frvr.com"
+gvlVendorID: 1107
+modifyingVastXmlAllowed: false
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+  site:
+    mediaTypes:
+      - banner
+      - video
+userSync:
+  iframe:
+    url: "https://fran.frvr.com/api/v1/sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect_uri={{.RedirectURL}}"
+    userMacro: "{{UID}}"
\ No newline at end of file
diff --git a/static/bidder-params/frvradn.json b/static/bidder-params/frvradn.json
new file mode 100644
index 00000000..09edaba1
--- /dev/null
+++ b/static/bidder-params/frvradn.json
@@ -0,0 +1,17 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "FRVR Ad Network Adapter Params",
+  "description": "A schema which validates params accepted by the FRVR Ad Network adapter",
+  "type": "object",
+  "properties": {
+    "publisher_id": {
+      "type": ["string"],
+      "description": "Publisher ID"
+    },
+    "ad_unit_id": {
+      "type": ["string"],
+      "description": "Ad Unit ID"
+    }
+  },
+  "required": ["publisher_id", "ad_unit_id"]
+}
diff --git a/util/maputil/maputil.go b/util/maputil/maputil.go
index 49df0772..c33740d4 100644
--- a/util/maputil/maputil.go
+++ b/util/maputil/maputil.go
@@ -48,3 +48,16 @@ func HasElement(m map[string]interface{}, k ...string) bool {
 
 	return exists
 }
+
+// Clone creates an indepent copy of a map,
+func Clone[K comparable, V any](m map[K]V) map[K]V {
+	if m == nil {
+		return nil
+	}
+	clone := make(map[K]V, len(m))
+	for key, value := range m {
+		clone[key] = value
+	}
+
+	return clone
+}


Test patch:
diff --git a/account/account_test.go b/account/account_test.go
index 95fa8ada..4bac0894 100644
--- a/account/account_test.go
+++ b/account/account_test.go
@@ -12,6 +12,7 @@ import (
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/stored_requests"
+	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 )
@@ -579,3 +580,66 @@ func TestAccountUpgradeStatusGetAccount(t *testing.T) {
 		})
 	}
 }
+
+func TestDeprecateEventsEnabledField(t *testing.T) {
+	testCases := []struct {
+		name    string
+		account *config.Account
+		want    *bool
+	}{
+		{
+			name:    "account is nil",
+			account: nil,
+			want:    nil,
+		},
+		{
+			name: "account.EventsEnabled is nil, account.Events.Enabled is nil",
+			account: &config.Account{
+				EventsEnabled: nil,
+				Events: config.Events{
+					Enabled: nil,
+				},
+			},
+			want: nil,
+		},
+		{
+			name: "account.EventsEnabled is nil, account.Events.Enabled is non-nil",
+			account: &config.Account{
+				EventsEnabled: nil,
+				Events: config.Events{
+					Enabled: ptrutil.ToPtr(true),
+				},
+			},
+			want: ptrutil.ToPtr(true),
+		},
+		{
+			name: "account.EventsEnabled is non-nil, account.Events.Enabled is nil",
+			account: &config.Account{
+				EventsEnabled: ptrutil.ToPtr(true),
+				Events: config.Events{
+					Enabled: nil,
+				},
+			},
+			want: ptrutil.ToPtr(true),
+		},
+		{
+			name: "account.EventsEnabled is non-nil, account.Events.Enabled is non-nil",
+			account: &config.Account{
+				EventsEnabled: ptrutil.ToPtr(false),
+				Events: config.Events{
+					Enabled: ptrutil.ToPtr(true),
+				},
+			},
+			want: ptrutil.ToPtr(true),
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			deprecateEventsEnabledField(test.account)
+			if test.account != nil {
+				assert.Equal(t, test.want, test.account.Events.Enabled)
+			}
+		})
+	}
+}
diff --git a/adapters/frvradn/frvradn_test.go b/adapters/frvradn/frvradn_test.go
new file mode 100644
index 00000000..f50f319b
--- /dev/null
+++ b/adapters/frvradn/frvradn_test.go
@@ -0,0 +1,28 @@
+package frvradn
+
+import (
+	"github.com/stretchr/testify/assert"
+	"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.BidderFRVRAdNetwork, config.Adapter{
+		Endpoint: "https://fran.frvr.com/api/v1/openrtb",
+	}, config.Server{ExternalUrl: "https://host.com", GvlID: 1, DataCenter: "2"})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "frvradntest", bidder)
+}
+
+func TestInvalidEndpoint(t *testing.T) {
+	_, buildErr := Builder(openrtb_ext.BidderFRVRAdNetwork, config.Adapter{Endpoint: ""}, config.Server{})
+
+	assert.Error(t, buildErr)
+}
diff --git a/adapters/frvradn/frvradntest/exemplary/banner.json b/adapters/frvradn/frvradntest/exemplary/banner.json
new file mode 100644
index 00000000..1c9866aa
--- /dev/null
+++ b/adapters/frvradn/frvradntest/exemplary/banner.json
@@ -0,0 +1,105 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "4fd8fadb-b218-41cd-a99f-8ae932b8c529",
+            "publisher_id": "7de498f7-f955-4696-a337-348b026c5e42"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "4fd8fadb-b218-41cd-a99f-8ae932b8c529",
+                "publisher_id": "7de498f7-f955-4696-a337-348b026c5e42"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "test-seat",
+              "bid": [
+                {
+                  "id": "d4916d88-c0f4-4340-87c0-bc9054696d3a",
+                  "impid": "test-banner-id",
+                  "price": 0.55,
+                  "adm": "a creative",
+                  "adid": "123123",
+                  "adomain": ["advertiser.com"],
+                  "crid": "9999",
+                  "w": 300,
+                  "h": 600,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "d4916d88-c0f4-4340-87c0-bc9054696d3a",
+            "impid": "test-banner-id",
+            "price": 0.55,
+            "adm": "a creative",
+            "adid": "123123",
+            "adomain": ["advertiser.com"],
+            "crid": "9999",
+            "w": 300,
+            "h": 600,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/exemplary/native.json b/adapters/frvradn/frvradntest/exemplary/native.json
new file mode 100644
index 00000000..9b350547
--- /dev/null
+++ b/adapters/frvradn/frvradntest/exemplary/native.json
@@ -0,0 +1,95 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-native-id",
+        "native": {
+          "request": "test-native-request"
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "38cab549-569e-4362-a90f-f7c1d09d3e4c",
+            "publisher_id": "90f55919-525e-4e2f-99cb-fd4a59fb75a0"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-native-id",
+              "native": {
+                "request": "test-native-request"
+              },
+              "ext": {
+                "ad_unit_id": "38cab549-569e-4362-a90f-f7c1d09d3e4c",
+                "publisher_id": "90f55919-525e-4e2f-99cb-fd4a59fb75a0"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "test-seat",
+              "bid": [
+                {
+                  "id": "9244e776-e0c7-4d76-8c20-88971909114b",
+                  "impid": "test-native-id",
+                  "price": 2.50,
+                  "adm": "a creative",
+                  "adid": "9999",
+                  "adomain": ["advertiser.com"],
+                  "crid": "9999",
+                  "w": 300,
+                  "h": 600,
+                  "ext": {
+                    "prebid": {
+                      "type": "native"
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "9244e776-e0c7-4d76-8c20-88971909114b",
+            "impid": "test-native-id",
+            "price": 2.50,
+            "adm": "a creative",
+            "adid": "9999",
+            "adomain": ["advertiser.com"],
+            "crid": "9999",
+            "w": 300,
+            "h": 600,
+            "ext": {
+              "prebid": {
+                "type": "native"
+              }
+            }
+          },
+          "type": "native"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/exemplary/video.json b/adapters/frvradn/frvradntest/exemplary/video.json
new file mode 100644
index 00000000..9ff1bb31
--- /dev/null
+++ b/adapters/frvradn/frvradntest/exemplary/video.json
@@ -0,0 +1,101 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-video-id",
+        "video": {
+          "mimes": ["video/mp4"],
+          "w": 300,
+          "h": 600
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "5da02f4f-96de-4a89-824a-6e8e1dfc495c",
+            "publisher_id": "9d66baf8-a80f-4378-9bd2-06ec09eaa86c"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-video-id",
+              "video": {
+                "mimes": ["video/mp4"],
+                "w": 300,
+                "h": 600
+              },
+              "ext": {
+                "ad_unit_id": "5da02f4f-96de-4a89-824a-6e8e1dfc495c",
+                "publisher_id": "9d66baf8-a80f-4378-9bd2-06ec09eaa86c"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "test-seat",
+              "bid": [
+                {
+                  "id": "42d72858-e071-40b2-ab74-27848e4d3336",
+                  "impid": "test-video-id",
+                  "price": 6.50,
+                  "nurl": "notification",
+                  "adm": "a creative",
+                  "adid": "9999",
+                  "adomain": ["advertiser.com"],
+                  "crid": "9999",
+                  "w": 300,
+                  "h": 600,
+                  "ext": {
+                    "prebid": {
+                      "type": "video"
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "42d72858-e071-40b2-ab74-27848e4d3336",
+            "impid": "test-video-id",
+            "price": 6.50,
+            "nurl": "notification",
+            "adm": "a creative",
+            "adid": "9999",
+            "adomain": ["advertiser.com"],
+            "crid": "9999",
+            "w": 300,
+            "h": 600,
+            "ext": {
+              "prebid": {
+                "type": "video"
+              }
+            }
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/204.json b/adapters/frvradn/frvradntest/supplemental/204.json
new file mode 100644
index 00000000..48fe8a03
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/204.json
@@ -0,0 +1,55 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "aae06006-2c41-499c-a432-7b74bc8d9b28",
+            "publisher_id": "0af78b6e-45ad-4bad-8332-4b5e6a87b222"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "aae06006-2c41-499c-a432-7b74bc8d9b28",
+                "publisher_id": "0af78b6e-45ad-4bad-8332-4b5e6a87b222"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 204
+      }
+    }
+  ],
+  "expectedBidResponses": []
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/400.json b/adapters/frvradn/frvradntest/supplemental/400.json
new file mode 100644
index 00000000..a7e5f40d
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/400.json
@@ -0,0 +1,60 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "7fc326d4-243d-41ef-bbb4-4c76c4ad69cd",
+            "publisher_id": "4552e5bd-5ece-4f7d-a256-9069a33260a6"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "7fc326d4-243d-41ef-bbb4-4c76c4ad69cd",
+                "publisher_id": "4552e5bd-5ece-4f7d-a256-9069a33260a6"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 400
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 400. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/503.json b/adapters/frvradn/frvradntest/supplemental/503.json
new file mode 100644
index 00000000..11b9ce2d
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/503.json
@@ -0,0 +1,60 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "7fc326d4-243d-41ef-bbb4-4c76c4ad69cd",
+            "publisher_id": "4552e5bd-5ece-4f7d-a256-9069a33260a6"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "7fc326d4-243d-41ef-bbb4-4c76c4ad69cd",
+                "publisher_id": "4552e5bd-5ece-4f7d-a256-9069a33260a6"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 503
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 503. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/currency_converter.json b/adapters/frvradn/frvradntest/supplemental/currency_converter.json
new file mode 100644
index 00000000..45561d0b
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/currency_converter.json
@@ -0,0 +1,133 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "bidfloor": 5,
+        "bidfloorcur": "EUR",
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+            "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "currency": {
+          "rates": {
+            "EUR": {
+              "USD": 1.5
+            }
+          },
+          "usepbsrates": false
+        }
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "bidfloor": 7.5,
+              "bidfloorcur": "USD",
+              "ext": {
+                "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+                "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+              }
+            }
+          ],
+          "ext": {
+            "prebid": {
+              "currency": {
+                "rates": {
+                  "EUR": {
+                    "USD": 1.5
+                  }
+                },
+                "usepbsrates": false
+              }
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "test-seat",
+              "bid": [
+                {
+                  "id": "ed03a73b-3c9d-4883-a672-7919cb607c15",
+                  "impid": "test-banner-id",
+                  "price": 7.5,
+                  "adm": "a creative",
+                  "adid": "9999",
+                  "adomain": ["advertiser.com"],
+                  "crid": "9999",
+                  "w": 300,
+                  "h": 600,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "ed03a73b-3c9d-4883-a672-7919cb607c15",
+            "impid": "test-banner-id",
+            "price": 7.5,
+            "adm": "a creative",
+            "adid": "9999",
+            "adomain": ["advertiser.com"],
+            "crid": "9999",
+            "w": 300,
+            "h": 600,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/empty_object_response.json b/adapters/frvradn/frvradntest/supplemental/empty_object_response.json
new file mode 100644
index 00000000..e0e8959e
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/empty_object_response.json
@@ -0,0 +1,56 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+            "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+                "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {}
+      }
+    }
+  ],
+  "expectedBidResponses": []
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/empty_reponse.json b/adapters/frvradn/frvradntest/supplemental/empty_reponse.json
new file mode 100644
index 00000000..d6e2b059
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/empty_reponse.json
@@ -0,0 +1,55 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+            "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+                "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200
+      }
+    }
+  ],
+  "expectedBidResponses": []
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/missing_bid_ext.json b/adapters/frvradn/frvradntest/supplemental/missing_bid_ext.json
new file mode 100644
index 00000000..32b54f20
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/missing_bid_ext.json
@@ -0,0 +1,81 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "56f94390-e632-46e3-8e0f-e399d7d6e10d",
+            "publisher_id": "96ce87c5-1c9d-4456-a093-9d8162e54b44"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "56f94390-e632-46e3-8e0f-e399d7d6e10d",
+                "publisher_id": "96ce87c5-1c9d-4456-a093-9d8162e54b44"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "test-seat",
+              "bid": [
+                {
+                  "id": "ec174d08-4c15-4ea0-95cc-e55bb5bfa0f9\n\n",
+                  "impid": "random-imp",
+                  "price": 0.50,
+                  "adm": "a creative",
+                  "adid": "9999",
+                  "adomain": ["advertiser.com"],
+                  "crid": "9999",
+                  "w": 300,
+                  "h": 600
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "unable to deserialize imp random-imp bid.ext",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/missing_ext.json b/adapters/frvradn/frvradntest/supplemental/missing_ext.json
new file mode 100644
index 00000000..4c22142b
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/missing_ext.json
@@ -0,0 +1,24 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        }
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "missing ext",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/missing_ext_bidder.json b/adapters/frvradn/frvradntest/supplemental/missing_ext_bidder.json
new file mode 100644
index 00000000..b88f989a
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/missing_ext_bidder.json
@@ -0,0 +1,25 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {}
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "missing ext.bidder",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/missing_params.json b/adapters/frvradn/frvradntest/supplemental/missing_params.json
new file mode 100644
index 00000000..4aba47d2
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/missing_params.json
@@ -0,0 +1,27 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {}
+        }
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "publisher_id and ad_unit_id are required",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/unknown_currency.json b/adapters/frvradn/frvradntest/supplemental/unknown_currency.json
new file mode 100644
index 00000000..74332ad6
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/unknown_currency.json
@@ -0,0 +1,44 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "bidfloor": 1,
+        "bidfloorcur": "EUR",
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+            "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "currency": {
+          "rates": {
+            "GBP": {
+              "USD": 1.5
+            }
+          },
+          "usepbsrates": false
+        }
+      }
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Currency conversion rate not found: 'EUR' => 'USD'",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/unknown_imp_media_type.json b/adapters/frvradn/frvradntest/supplemental/unknown_imp_media_type.json
new file mode 100644
index 00000000..7ed869d7
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/unknown_imp_media_type.json
@@ -0,0 +1,66 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "cdb3a029-ce0a-4b4b-b404-1d5f9ff86d7d",
+            "publisher_id": "dc4c0f5e-8637-4017-8aa8-af59d4bc03af"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "ext": {
+                "ad_unit_id": "cdb3a029-ce0a-4b4b-b404-1d5f9ff86d7d",
+                "publisher_id": "dc4c0f5e-8637-4017-8aa8-af59d4bc03af"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "test-seat",
+              "bid": [
+                {
+                  "id": "e2710817-83dd-421f-9cd6-55bed1ca8a41",
+                  "impid": "test-banner-id",
+                  "price": 5.50,
+                  "adm": "test-banner",
+                  "adid": "9999",
+                  "adomain": ["advertiser.com"],
+                  "crid": "9999",
+                  "w": 300,
+                  "h": 600,
+                  "ext": {}
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "imp test-banner-id with unknown media type",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/params_test.go b/adapters/frvradn/params_test.go
new file mode 100644
index 00000000..2d4836c1
--- /dev/null
+++ b/adapters/frvradn/params_test.go
@@ -0,0 +1,48 @@
+package frvradn
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderFRVRAdNetwork, json.RawMessage(p)); err != nil {
+			t.Errorf("Schema rejected valid params: %s", p)
+		}
+	}
+}
+
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderFRVRAdNetwork, json.RawMessage(p)); err == nil {
+			t.Errorf("Schema allowed invalid params: %s", p)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"publisher_id": "247f36ed-bda5-4159-86f1-e383849e7810", "ad_unit_id": "63c36c82-3246-4931-97f9-4f16a9639ba9"}`,
+}
+
+var invalidParams = []string{
+	`null`,
+	`nil`,
+	``,
+	`[]`,
+	`{}`,
+	`{"publisher_id": "247f36ed-bda5-4159-86f1-e383849e7810"}`,
+	`{"ad_unit_id": "247f36ed-bda5-4159-86f1-e383849e7810"}`,
+}
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-app.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-app.json
index 4b760494..672d8ab0 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-app.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-app.json
@@ -13,6 +13,7 @@
           },
           "bidfloor": 1e-05,
           "bidfloorcur": "USD",
+          "tagid": "testTag",
           "ext": {
            "bidder": {
               "pid": "OsNsyeF68q",
@@ -24,7 +25,6 @@
       ],
       "app": {
         "id": "12345678",
-        "keywords": "tagid=",
         "name": "Richaudience TV",
         "bundle": "R12345678901011",
         "publisher": {
@@ -41,7 +41,7 @@
         }
     },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
     },
       "user": {
@@ -86,7 +86,7 @@
             ],
             "app": {
               "id": "12345678",
-              "keywords": "tagid=",
+              "keywords": "tagid=testTag",
               "name": "Richaudience TV",
               "bundle": "R12345678901011",
               "publisher": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-defaultCurrency.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-defaultCurrency.json
index 7583c13f..0ddec0a4 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-defaultCurrency.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-defaultCurrency.json
@@ -14,6 +14,7 @@
           ]
         },
         "bidfloor": 1e-05,
+        "tagid": "testTag",
         "ext": {
           "bidder": {
             "pid": "OsNsyeF68q",
@@ -24,12 +25,11 @@
       }
     ],
     "site": {
-      "keywords": "tagid=",
       "domain": "bridge.richmediastudio.com",
       "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
     },
     "device": {
-      "ip": "11.222.33.44",
+      "ip": "10.20.30.40",
       "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
     "user": {
@@ -80,7 +80,7 @@
             }
           ],
           "site": {
-            "keywords": "tagid=",
+            "keywords": "tagid=testTag",
             "domain": "bridge.richmediastudio.com",
             "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
           },
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-deviceConfig.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-deviceConfig.json
index e4bc3327..5ccc8f14 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-deviceConfig.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-deviceConfig.json
@@ -13,6 +13,7 @@
           },
           "bidfloor": 1e-05,
           "bidfloorcur": "USD",
+          "tagid": "testTag",
           "ext": {
            "bidder": {
               "pid": "OsNsyeF68q",
@@ -23,14 +24,13 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
           "dnt": 1,
           "lmt": 1,
-        "ip": "11.222.33.44"
+        "ip": "10.20.30.40"
     },
       "user": {
         "buyeruid": "189f4055-78a3-46eb-b7fd-0915a1a43bd2a",
@@ -73,7 +73,7 @@
               }
             ],
             "site": {
-              "keywords": "tagid=",
+              "keywords": "tagid=testTag",
               "domain": "bridge.richmediastudio.com",
               "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
             },
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-extUser.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-extUser.json
index d22e1cf6..b6227d37 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-extUser.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-extUser.json
@@ -25,12 +25,11 @@
       }
     ],
     "site": {
-      "keywords": "tagid=",
       "domain": "bridge.richmediastudio.com",
       "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
     },
     "device": {
-      "ip": "11.222.33.44",
+      "ip": "10.20.30.40",
       "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
     "user": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-floorPrice.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-floorPrice.json
index 7d76a12b..9fec4d46 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-floorPrice.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-floorPrice.json
@@ -24,12 +24,11 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
       "user": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-iPv6.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-iPv6.json
index 93fabba3..b40e9f65 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-iPv6.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-iPv6.json
@@ -25,12 +25,11 @@
       }
     ],
     "site": {
-      "keywords": "tagid=",
       "domain": "bridge.richmediastudio.com",
       "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
     },
     "device": {
-      "ip": "11.222.33.44",
+      "ip": "10.20.30.40",
       "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245",
       "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-nil-device.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-nil-device.json
new file mode 100644
index 00000000..f5e38991
--- /dev/null
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-nil-device.json
@@ -0,0 +1,168 @@
+{
+  "mockBidRequest": {
+    "id": "4d3f84eb-787b-42fb-a0cf-062690dadce3",
+    "test": 0,
+    "imp": [
+      {
+        "id": "div-gpt-ad-1460505748561-0",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "bidfloor": 1e-05,
+        "bidfloorcur": "USD",
+        "tagid": "testTag",
+        "ext": {
+          "bidder": {
+            "pid": "OsNsyeF68q",
+            "supplyType": "site",
+            "test": true
+          }
+        }
+      }
+    ],
+    "app": {
+      "id": "12345678",
+      "name": "Richaudience TV",
+      "bundle": "R12345678901011",
+      "publisher": {
+        "id": "1234567",
+        "ext": {
+          "prebid": {
+            "parentAccount": "891011"
+          }
+        }
+      },
+      "content": {
+        "title": "Richaudience TV",
+        "series": "Richaudience TV"
+      }
+    },
+    "user": {
+      "buyeruid": "189f4055-78a3-46eb-b7fd-0915a1a43bd2a",
+      "ext": {}
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ortb.richaudience.com/ortb/?bidder=pbs",
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "X-Openrtb-Version": [
+            "2.5"
+          ]
+        },
+        "body": {
+          "id": "4d3f84eb-787b-42fb-a0cf-062690dadce3",
+          "test": 1,
+          "imp": [
+            {
+              "id": "div-gpt-ad-1460505748561-0",
+              "tagid": "OsNsyeF68q",
+              "secure": 0,
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              },
+              "bidfloor": 1e-05,
+              "bidfloorcur": "USD",
+              "ext": {
+                "bidder": {
+                  "pid": "OsNsyeF68q",
+                  "supplyType": "site",
+                  "test": true
+                }
+              }
+            }
+          ],
+          "app": {
+            "id": "12345678",
+            "keywords": "tagid=testTag",
+            "name": "Richaudience TV",
+            "bundle": "R12345678901011",
+            "publisher": {
+              "id": "1234567",
+              "ext": {
+                "prebid": {
+                  "parentAccount": "891011"
+                }
+              }
+            },
+            "content": {
+              "title": "Richaudience TV",
+              "series": "Richaudience TV"
+            }
+          },
+          "device": {
+            "ip": "11.222.33.44"
+          },
+          "user": {
+            "buyeruid": "189f4055-78a3-46eb-b7fd-0915a1a43bd2a",
+            "ext": {
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "ff935bea-4661-40bf-95b7-80c354cf0cdc",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "47286888",
+                  "impid": "div-gpt-ad-1460505748561-0",
+                  "price": 99,
+                  "crid": "999999",
+                  "adm": "<!-- creative -->",
+                  "adomain": [
+                    "richaudience.com"
+                  ],
+                  "h": 250,
+                  "w": 300
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "47286888",
+            "impid": "div-gpt-ad-1460505748561-0",
+            "price": 99,
+            "adm": "<!-- creative -->",
+            "adomain": [
+              "richaudience.com"
+            ],
+            "crid": "999999",
+            "w": 300,
+            "h": 250
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-nosecure.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-nosecure.json
index 68cc0af6..78886f38 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-nosecure.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-nosecure.json
@@ -23,12 +23,11 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "http://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
       "user": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-setCurrency.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-setCurrency.json
index 70662052..5490197d 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-setCurrency.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-setCurrency.json
@@ -23,12 +23,11 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
       "user": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-sitePage.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-sitePage.json
index 901af4a8..e6a00cbf 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-sitePage.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-sitePage.json
@@ -23,12 +23,11 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
       "user": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner.json
index 901af4a8..e6a00cbf 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner.json
@@ -23,12 +23,11 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
       "user": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-video.json b/adapters/richaudience/richaudiencetest/exemplary/single-video.json
index 2d18c01a..0219906f 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-video.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-video.json
@@ -23,12 +23,11 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
       "user": {
diff --git a/config/config_test.go b/config/config_test.go
index f45d7fcd..fbd96d3f 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/prebid/go-gdpr/consentconstants"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
 )
@@ -185,6 +186,8 @@ func TestDefaults(t *testing.T) {
 	cmpBools(t, "account_defaults.price_floors.use_dynamic_data", false, cfg.AccountDefaults.PriceFloors.UseDynamicData)
 	cmpInts(t, "account_defaults.price_floors.max_rules", 100, cfg.AccountDefaults.PriceFloors.MaxRule)
 	cmpInts(t, "account_defaults.price_floors.max_schema_dims", 3, cfg.AccountDefaults.PriceFloors.MaxSchemaDims)
+	cmpBools(t, "account_defaults.events_enabled", *cfg.AccountDefaults.EventsEnabled, false)
+	cmpNils(t, "account_defaults.events.enabled", cfg.AccountDefaults.Events.Enabled)
 
 	cmpBools(t, "hooks.enabled", false, cfg.Hooks.Enabled)
 	cmpStrings(t, "validations.banner_creative_max_size", "skip", cfg.Validations.BannerCreativeMaxSize)
@@ -311,6 +314,7 @@ func TestDefaults(t *testing.T) {
 	assert.Equal(t, expectedTCF2, cfg.GDPR.TCF2, "gdpr.tcf2")
 }
 
+// When adding a new field, make sure the indentations are spaces not tabs otherwise read config may fail to parse the new field value.
 var fullConfig = []byte(`
 gdpr:
   host_vendor_id: 15
@@ -446,6 +450,9 @@ hooks:
 price_floors:
     enabled: true
 account_defaults:
+    events_enabled: false
+    events:
+        enabled: true
     price_floors:
         enabled: true
         enforce_floors_rate: 50
@@ -543,6 +550,8 @@ func TestFullConfig(t *testing.T) {
 	cmpBools(t, "account_defaults.price_floors.use_dynamic_data", true, cfg.AccountDefaults.PriceFloors.UseDynamicData)
 	cmpInts(t, "account_defaults.price_floors.max_rules", 120, cfg.AccountDefaults.PriceFloors.MaxRule)
 	cmpInts(t, "account_defaults.price_floors.max_schema_dims", 5, cfg.AccountDefaults.PriceFloors.MaxSchemaDims)
+	cmpBools(t, "account_defaults.events_enabled", *cfg.AccountDefaults.EventsEnabled, true)
+	cmpNils(t, "account_defaults.events.enabled", cfg.AccountDefaults.Events.Enabled)
 
 	//Assert the NonStandardPublishers was correctly unmarshalled
 	assert.Equal(t, []string{"pub1", "pub2"}, cfg.GDPR.NonStandardPublishers, "gdpr.non_standard_publishers")
@@ -3191,3 +3200,52 @@ func TestTCF2FeatureOneVendorException(t *testing.T) {
 		assert.Equal(t, tt.wantIsVendorException, value, tt.description)
 	}
 }
+
+func TestMigrateConfigEventsEnabled(t *testing.T) {
+	testCases := []struct {
+		name                  string
+		oldFieldValue         *bool
+		newFieldValue         *bool
+		expectedOldFieldValue *bool
+		expectedNewFieldValue *bool
+	}{
+		{
+			name:                  "Both old and new fields are nil",
+			oldFieldValue:         nil,
+			newFieldValue:         nil,
+			expectedOldFieldValue: nil,
+			expectedNewFieldValue: nil,
+		},
+		{
+			name:                  "Only old field is set",
+			oldFieldValue:         ptrutil.ToPtr(true),
+			newFieldValue:         nil,
+			expectedOldFieldValue: ptrutil.ToPtr(true),
+			expectedNewFieldValue: nil,
+		},
+		{
+			name:                  "Only new field is set",
+			oldFieldValue:         nil,
+			newFieldValue:         ptrutil.ToPtr(true),
+			expectedOldFieldValue: ptrutil.ToPtr(true),
+			expectedNewFieldValue: nil,
+		},
+		{
+			name:                  "Both old and new fields are set, override old field with new field value",
+			oldFieldValue:         ptrutil.ToPtr(false),
+			newFieldValue:         ptrutil.ToPtr(true),
+			expectedOldFieldValue: ptrutil.ToPtr(true),
+			expectedNewFieldValue: nil,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			updatedOldFieldValue, updatedNewFieldValue := migrateConfigEventsEnabled(tc.oldFieldValue, tc.newFieldValue)
+
+			assert.Equal(t, tc.expectedOldFieldValue, updatedOldFieldValue)
+			assert.Nil(t, updatedNewFieldValue)
+			assert.Nil(t, tc.expectedNewFieldValue)
+		})
+	}
+}
diff --git a/config/events_test.go b/config/events_test.go
index b4c196e9..4baa9066 100644
--- a/config/events_test.go
+++ b/config/events_test.go
@@ -3,6 +3,7 @@ package config
 import (
 	"testing"
 
+	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -261,22 +262,29 @@ func TestValidate(t *testing.T) {
 		{
 			description: "Empty default URL",
 			events: Events{
-				Enabled: true,
+				Enabled: ptrutil.ToPtr(true),
 			},
 			expectErr: true,
 		},
 		{
 			description: "Events are disabled. Skips validations",
 			events: Events{
-				Enabled:    false,
+				Enabled:    ptrutil.ToPtr(false),
 				DefaultURL: "",
 			},
 			expectErr: false,
 		},
+		{
+			description: "Events are nil. Skip validations",
+			events: Events{
+				Enabled: nil,
+			},
+			expectErr: false,
+		},
 		{
 			description: "No VAST Events and default URL present",
 			events: Events{
-				Enabled:    true,
+				Enabled:    ptrutil.ToPtr(true),
 				DefaultURL: "http://prebid.org",
 			},
 			expectErr: false,
@@ -284,7 +292,7 @@ func TestValidate(t *testing.T) {
 		{
 			description: "Invalid VAST Event",
 			events: Events{
-				Enabled:    true,
+				Enabled:    ptrutil.ToPtr(true),
 				DefaultURL: "http://prebid.org",
 				VASTEvents: []VASTEvent{
 					{},
@@ -327,3 +335,34 @@ func TestValidateVASTEvents(t *testing.T) {
 		assert.Equal(t, !test.expectErr, err == nil, test.description)
 	}
 }
+
+func TestIsEnabled(t *testing.T) {
+	testCases := []struct {
+		name     string
+		events   Events
+		expected bool
+	}{
+		{
+			name:     "nil pointer",
+			events:   Events{},
+			expected: false,
+		},
+		{
+			name:     "event false",
+			events:   Events{Enabled: ptrutil.ToPtr(false)},
+			expected: false,
+		},
+		{
+			name:     "event true",
+			events:   Events{Enabled: ptrutil.ToPtr(true)},
+			expected: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			actual := tc.events.IsEnabled()
+			assert.Equal(t, tc.expected, actual)
+		})
+	}
+}
diff --git a/endpoints/events/event_test.go b/endpoints/events/event_test.go
index d4bbc991..95711ba1 100644
--- a/endpoints/events/event_test.go
+++ b/endpoints/events/event_test.go
@@ -73,9 +73,9 @@ func (e *eventsMockAnalyticsModule) LogNotificationEventObject(ne *analytics.Not
 
 // Mock Account fetcher
 var mockAccountData = map[string]json.RawMessage{
-	"events_enabled":  json.RawMessage(`{"events_enabled":true}`),
-	"events_disabled": json.RawMessage(`{"events_enabled":false}`),
-	"malformed_acct":  json.RawMessage(`{"events_enabled":"invalid type"}`),
+	"events_enabled":  json.RawMessage(`{"events": {"enabled":true}}`),
+	"events_disabled": json.RawMessage(`{"events": {"enabled":false}}`),
+	"malformed_acct":  json.RawMessage(`{"events": {"enabled":"invalid type"}}`),
 }
 
 type mockAccountsFetcher struct {
@@ -359,7 +359,7 @@ func TestShouldReturnBadRequestWhenAnalyticsValueIsInvalid(t *testing.T) {
 	assert.Equal(t, "invalid request: unknown analytics: '4'\n", string(d))
 }
 
-func TestShouldNotPassEventToAnalyticsReporterWhenAccountNotFound(t *testing.T) {
+func TestShouldNotPassEventToAnalyticsReporterWhenAccountNotFoundAndDefaultIsFalse(t *testing.T) {
 
 	// mock AccountsFetcher
 	mockAccountsFetcher := &mockAccountsFetcher{
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 00c6d7f7..f291d2e8 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -3590,6 +3590,11 @@ func TestAuctionWarnings(t *testing.T) {
 			file:            "us-privacy-conflict.json",
 			expectedWarning: "regs.us_privacy consent does not match uspv1 in GPP, using regs.gpp",
 		},
+		{
+			name:            "empty-gppsid-array-conflicts-with-regs-gdpr", // gdpr set to 1, an empty non-nil gpp_sid array doesn't match
+			file:            "empty-gppsid-conflict.json",
+			expectedWarning: "regs.gdpr signal conflicts with GPP (regs.gpp_sid) and will be ignored",
+		},
 		{
 			name:            "gdpr-signals-conflict", // gdpr signals do not match
 			file:            "gdpr-conflict.json",
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index 2aa03573..4e367ebe 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -2026,6 +2026,7 @@ func TestCallRecordAdapterConnections(t *testing.T) {
 
 	mockMetricEngine.On("RecordAdapterConnections", expectedAdapterName, false, mock.MatchedBy(compareConnWaitTime)).Once()
 	mockMetricEngine.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once()
+	mockMetricEngine.On("RecordBidderServerResponseTime", mock.Anything).Once()
 
 	// Run requestBid using an http.Client with a mock handler
 	bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, mockMetricEngine, openrtb_ext.BidderAppnexus, nil, "")
@@ -2087,6 +2088,7 @@ func TestCallRecordDNSTime(t *testing.T) {
 	metricsMock := &metrics.MetricsEngineMock{}
 	metricsMock.Mock.On("RecordDNSTime", mock.Anything).Return()
 	metricsMock.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once()
+	metricsMock.On("RecordBidderServerResponseTime", mock.Anything).Once()
 
 	// Instantiate the bidder that will send the request. We'll make sure to use an
 	// http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{})
@@ -2109,6 +2111,7 @@ func TestCallRecordTLSHandshakeTime(t *testing.T) {
 	metricsMock := &metrics.MetricsEngineMock{}
 	metricsMock.Mock.On("RecordTLSHandshakeTime", mock.Anything).Return()
 	metricsMock.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once()
+	metricsMock.On("RecordBidderServerResponseTime", mock.Anything).Once()
 
 	// Instantiate the bidder that will send the request. We'll make sure to use an
 	// http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{})
diff --git a/exchange/events_test.go b/exchange/events_test.go
index a808173e..24dedf1a 100644
--- a/exchange/events_test.go
+++ b/exchange/events_test.go
@@ -121,7 +121,7 @@ func Test_eventsData_modifyBidJSON(t *testing.T) {
 		},
 		{
 			name:      "banner: broken json expected to fail patching",
-			args:      args{enabledForAccount: true, enabledForRequest: true, bidType: openrtb_ext.BidTypeBanner, generatedBidId: ""},
+			args:      args{enabledForAccount: false, enabledForRequest: true, bidType: openrtb_ext.BidTypeBanner, generatedBidId: ""},
 			jsonBytes: []byte(`broken json`),
 			want:      nil,
 		},
@@ -153,3 +153,42 @@ func Test_eventsData_modifyBidJSON(t *testing.T) {
 		})
 	}
 }
+
+func Test_isEventAllowed(t *testing.T) {
+	type args struct {
+		enabledForAccount bool
+		enabledForRequest bool
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "enabled for account",
+			args: args{enabledForAccount: true, enabledForRequest: false},
+			want: true,
+		},
+		{
+			name: "enabled for request",
+			args: args{enabledForAccount: false, enabledForRequest: true},
+			want: true,
+		},
+		{
+			name: "disabled for account and request",
+			args: args{enabledForAccount: false, enabledForRequest: false},
+			want: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			evData := &eventTracking{
+				enabledForAccount: tt.args.enabledForAccount,
+				enabledForRequest: tt.args.enabledForRequest,
+			}
+			isEventAllowed := evData.isEventAllowed()
+			assert.Equal(t, tt.want, isEventAllowed)
+		})
+	}
+}
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index bef48be1..323b299e 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -2398,10 +2398,12 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 	auctionRequest := &AuctionRequest{
 		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &spec.IncomingRequest.OrtbRequest},
 		Account: config.Account{
-			ID:            "testaccount",
-			EventsEnabled: spec.EventsEnabled,
-			DebugAllow:    true,
-			Validations:   spec.AccountConfigBidValidation,
+			ID: "testaccount",
+			Events: config.Events{
+				Enabled: &spec.EventsEnabled,
+			},
+			DebugAllow:  true,
+			Validations: spec.AccountConfigBidValidation,
 		},
 		UserSyncs:     mockIdFetcher(spec.IncomingRequest.Usersyncs),
 		ImpExtInfoMap: impExtInfoMap,
diff --git a/metrics/go_metrics_test.go b/metrics/go_metrics_test.go
index 1b0b20e4..1ab22ac1 100644
--- a/metrics/go_metrics_test.go
+++ b/metrics/go_metrics_test.go
@@ -83,6 +83,7 @@ func TestNewMetrics(t *testing.T) {
 	ensureContains(t, registry, "request_over_head_time.pre-bidder", m.OverheadTimer[PreBidder])
 	ensureContains(t, registry, "request_over_head_time.make-auction-response", m.OverheadTimer[MakeAuctionResponse])
 	ensureContains(t, registry, "request_over_head_time.make-bidder-requests", m.OverheadTimer[MakeBidderRequests])
+	ensureContains(t, registry, "bidder_server_response_time_seconds", m.BidderServerResponseTimer)
 
 	for module, stages := range moduleStageNames {
 		for _, stage := range stages {
@@ -420,6 +421,36 @@ func TestRecordTLSHandshakeTime(t *testing.T) {
 	}
 }
 
+func TestRecordBidderServerResponseTime(t *testing.T) {
+	testCases := []struct {
+		name          string
+		time          time.Duration
+		expectedCount int64
+		expectedSum   int64
+	}{
+		{
+			name:          "record-bidder-server-response-time-1",
+			time:          time.Duration(500),
+			expectedCount: 1,
+			expectedSum:   500,
+		},
+		{
+			name:          "record-bidder-server-response-time-2",
+			time:          time.Duration(500),
+			expectedCount: 2,
+			expectedSum:   1000,
+		},
+	}
+	for _, test := range testCases {
+		registry := metrics.NewRegistry()
+		m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AccountAdapterDetails: true}, nil, nil)
+
+		m.RecordBidderServerResponseTime(test.time)
+
+		assert.Equal(t, test.time.Nanoseconds(), m.BidderServerResponseTimer.Sum(), test.name)
+	}
+}
+
 func TestRecordAdapterConnections(t *testing.T) {
 	var fakeBidder openrtb_ext.BidderName = "fooAdvertising"
 
diff --git a/metrics/prometheus/prometheus_test.go b/metrics/prometheus/prometheus_test.go
index d2ede844..32f7848c 100644
--- a/metrics/prometheus/prometheus_test.go
+++ b/metrics/prometheus/prometheus_test.go
@@ -1475,6 +1475,39 @@ func TestRecordTLSHandshakeTime(t *testing.T) {
 	}
 }
 
+func TestRecordBidderServerResponseTime(t *testing.T) {
+	testCases := []struct {
+		description   string
+		timeInMs      float64
+		expectedCount uint64
+		expectedSum   float64
+	}{
+		{
+			description:   "record-bidder-server-response-time-1",
+			timeInMs:      500,
+			expectedCount: 1,
+			expectedSum:   0.5,
+		},
+		{
+			description:   "record-bidder-server-response-time-2",
+			timeInMs:      400,
+			expectedCount: 1,
+			expectedSum:   0.4,
+		},
+	}
+	for _, test := range testCases {
+		pm := createMetricsForTesting()
+		pm.RecordBidderServerResponseTime(time.Duration(test.timeInMs) * time.Millisecond)
+
+		m := dto.Metric{}
+		pm.bidderServerResponseTimer.Write(&m)
+		histogram := *m.GetHistogram()
+
+		assert.Equal(t, test.expectedCount, histogram.GetSampleCount())
+		assert.Equal(t, test.expectedSum, histogram.GetSampleSum())
+	}
+}
+
 func TestRecordAdapterConnections(t *testing.T) {
 
 	type testIn struct {
diff --git a/openrtb_ext/request_test.go b/openrtb_ext/request_test.go
index 8d1ecee2..94c042db 100644
--- a/openrtb_ext/request_test.go
+++ b/openrtb_ext/request_test.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/prebid/openrtb/v19/openrtb2"
 	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 )
@@ -141,3 +142,628 @@ var validGranularityTests []granularityTestData = []granularityTestData{
 		},
 	},
 }
+
+func TestCloneExtRequestPrebid(t *testing.T) {
+	testCases := []struct {
+		name       string
+		prebid     *ExtRequestPrebid
+		prebidCopy *ExtRequestPrebid                            // manual copy of above prebid object to verify against
+		mutator    func(t *testing.T, prebid *ExtRequestPrebid) // function to modify the prebid object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			prebid:     nil,
+			prebidCopy: nil,
+			mutator:    func(t *testing.T, prebid *ExtRequestPrebid) {},
+		},
+		{
+			name: "NoMutateTest",
+			prebid: &ExtRequestPrebid{
+				Aliases:              map[string]string{"alias1": "bidder1"},
+				BidAdjustmentFactors: map[string]float64{"bidder5": 1.2},
+				BidderParams:         json.RawMessage(`{}`),
+				Channel: &ExtRequestPrebidChannel{
+					Name:    "ABC",
+					Version: "1.0",
+				},
+				Debug: true,
+				Experiment: &Experiment{
+					AdsCert: &AdsCert{
+						Enabled: false,
+					},
+				},
+				Server: &ExtRequestPrebidServer{
+					ExternalUrl: "http://www.example.com",
+					GvlID:       5,
+					DataCenter:  "Universe Central",
+				},
+				SupportDeals: true,
+			},
+			prebidCopy: &ExtRequestPrebid{
+				Aliases:              map[string]string{"alias1": "bidder1"},
+				BidAdjustmentFactors: map[string]float64{"bidder5": 1.2},
+				BidderParams:         json.RawMessage(`{}`),
+				Channel: &ExtRequestPrebidChannel{
+					Name:    "ABC",
+					Version: "1.0",
+				},
+				Debug: true,
+				Experiment: &Experiment{
+					AdsCert: &AdsCert{
+						Enabled: false,
+					},
+				},
+				Server: &ExtRequestPrebidServer{
+					ExternalUrl: "http://www.example.com",
+					GvlID:       5,
+					DataCenter:  "Universe Central",
+				},
+				SupportDeals: true,
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {},
+		},
+		{
+			name: "GeneralTest",
+			prebid: &ExtRequestPrebid{
+				Aliases:              map[string]string{"alias1": "bidder1"},
+				BidAdjustmentFactors: map[string]float64{"bidder5": 1.2},
+				BidderParams:         json.RawMessage(`{}`),
+				Channel: &ExtRequestPrebidChannel{
+					Name:    "ABC",
+					Version: "1.0",
+				},
+				Debug: true,
+				Experiment: &Experiment{
+					AdsCert: &AdsCert{
+						Enabled: false,
+					},
+				},
+				Server: &ExtRequestPrebidServer{
+					ExternalUrl: "http://www.example.com",
+					GvlID:       5,
+					DataCenter:  "Universe Central",
+				},
+				SupportDeals: true,
+			},
+			prebidCopy: &ExtRequestPrebid{
+				Aliases:              map[string]string{"alias1": "bidder1"},
+				BidAdjustmentFactors: map[string]float64{"bidder5": 1.2},
+				BidderParams:         json.RawMessage(`{}`),
+				Channel: &ExtRequestPrebidChannel{
+					Name:    "ABC",
+					Version: "1.0",
+				},
+				Debug: true,
+				Experiment: &Experiment{
+					AdsCert: &AdsCert{
+						Enabled: false,
+					},
+				},
+				Server: &ExtRequestPrebidServer{
+					ExternalUrl: "http://www.example.com",
+					GvlID:       5,
+					DataCenter:  "Universe Central",
+				},
+				SupportDeals: true,
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.Aliases["alias2"] = "bidder52"
+				prebid.Aliases["alias1"] = "some other"
+				prebid.AliasGVLIDs = map[string]uint16{"alias2": 42}
+				prebid.BidAdjustmentFactors["alias2"] = 1.1
+				delete(prebid.BidAdjustmentFactors, "bidder5")
+				prebid.BidderParams = json.RawMessage(`{"someJSON": true}`)
+				prebid.Channel = nil
+				prebid.Data = &ExtRequestPrebidData{
+					EidPermissions: []ExtRequestPrebidDataEidPermission{{Source: "mySource", Bidders: []string{"sauceBidder"}}},
+				}
+				prebid.Events = json.RawMessage(`{}`)
+				prebid.Server.GvlID = 7
+				prebid.SupportDeals = false
+			},
+		},
+		{
+			name: "BidderConfig",
+			prebid: &ExtRequestPrebid{
+				BidderConfigs: []BidderConfig{
+					{
+						Bidders: []string{"Bidder1", "bidder2"},
+						Config:  &Config{&ORTB2{Site: map[string]json.RawMessage{"test": json.RawMessage(`{}`)}}},
+					},
+					{
+						Bidders: []string{"Bidder5", "bidder17"},
+						Config:  &Config{&ORTB2{Site: map[string]json.RawMessage{"test": json.RawMessage(`{"foo":"bar"}`), "more": json.RawMessage(`{}`)}}},
+					},
+					{
+						Bidders: []string{"foo"},
+						Config:  &Config{&ORTB2{User: map[string]json.RawMessage{"abc": json.RawMessage(`123`)}}},
+					},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				BidderConfigs: []BidderConfig{
+					{
+						Bidders: []string{"Bidder1", "bidder2"},
+						Config:  &Config{&ORTB2{Site: map[string]json.RawMessage{"test": json.RawMessage(`{}`)}}},
+					},
+					{
+						Bidders: []string{"Bidder5", "bidder17"},
+						Config:  &Config{&ORTB2{Site: map[string]json.RawMessage{"test": json.RawMessage(`{"foo":"bar"}`), "more": json.RawMessage(`{}`)}}},
+					},
+					{
+						Bidders: []string{"foo"},
+						Config:  &Config{&ORTB2{User: map[string]json.RawMessage{"abc": json.RawMessage(`123`)}}},
+					},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.BidderConfigs[0].Bidders = append(prebid.BidderConfigs[0].Bidders, "bidder4")
+				prebid.BidderConfigs[1] = BidderConfig{
+					Bidders: []string{"george"},
+					Config:  &Config{nil},
+				}
+				prebid.BidderConfigs[2].Config.ORTB2.User["abc"] = json.RawMessage(`{"id": 345}`)
+				prebid.BidderConfigs = append(prebid.BidderConfigs, BidderConfig{
+					Bidders: []string{"bidder2"},
+					Config:  &Config{&ORTB2{}},
+				})
+			},
+		},
+		{
+			name: "Cache",
+			prebid: &ExtRequestPrebid{
+				Cache: &ExtRequestPrebidCache{
+					Bids: &ExtRequestPrebidCacheBids{
+						ReturnCreative: ptrutil.ToPtr(true),
+					},
+					VastXML: &ExtRequestPrebidCacheVAST{
+						ReturnCreative: ptrutil.ToPtr(false),
+					},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				Cache: &ExtRequestPrebidCache{
+					Bids: &ExtRequestPrebidCacheBids{
+						ReturnCreative: ptrutil.ToPtr(true),
+					},
+					VastXML: &ExtRequestPrebidCacheVAST{
+						ReturnCreative: ptrutil.ToPtr(false),
+					},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.Cache.Bids.ReturnCreative = ptrutil.ToPtr(false)
+				prebid.Cache.Bids = nil
+				prebid.Cache.VastXML = &ExtRequestPrebidCacheVAST{
+					ReturnCreative: ptrutil.ToPtr(true),
+				}
+			},
+		},
+		{
+			name: "Currency",
+			prebid: &ExtRequestPrebid{
+				CurrencyConversions: &ExtRequestCurrency{
+					ConversionRates: map[string]map[string]float64{"A": {"X": 5.4}},
+					UsePBSRates:     ptrutil.ToPtr(false),
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				CurrencyConversions: &ExtRequestCurrency{
+					ConversionRates: map[string]map[string]float64{"A": {"X": 5.4}},
+					UsePBSRates:     ptrutil.ToPtr(false),
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.CurrencyConversions.ConversionRates["A"]["X"] = 3.4
+				prebid.CurrencyConversions.ConversionRates["B"] = make(map[string]float64)
+				prebid.CurrencyConversions.ConversionRates["B"]["Y"] = 0.76
+				prebid.CurrencyConversions.UsePBSRates = ptrutil.ToPtr(true)
+			},
+		},
+		{
+			name: "Data",
+			prebid: &ExtRequestPrebid{
+				Data: &ExtRequestPrebidData{
+					EidPermissions: []ExtRequestPrebidDataEidPermission{
+						{
+							Source:  "Sauce",
+							Bidders: []string{"G", "H"},
+						},
+						{
+							Source:  "Black Hole",
+							Bidders: []string{"Q", "P"},
+						},
+					},
+					Bidders: []string{"A", "B", "C"},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				Data: &ExtRequestPrebidData{
+					EidPermissions: []ExtRequestPrebidDataEidPermission{
+						{
+							Source:  "Sauce",
+							Bidders: []string{"G", "H"},
+						},
+						{
+							Source:  "Black Hole",
+							Bidders: []string{"Q", "P"},
+						},
+					},
+					Bidders: []string{"A", "B", "C"},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.Data.EidPermissions[0].Source = "Fuzzy Bunnies"
+				prebid.Data.EidPermissions[1].Bidders[0] = "X"
+				prebid.Data.EidPermissions[0].Bidders = append(prebid.Data.EidPermissions[0].Bidders, "R")
+				prebid.Data.EidPermissions = append(prebid.Data.EidPermissions, ExtRequestPrebidDataEidPermission{Source: "Harry"})
+				prebid.Data.Bidders[1] = "D"
+				prebid.Data.Bidders = append(prebid.Data.Bidders, "E")
+			},
+		},
+		{
+			name: "Multibid",
+			prebid: &ExtRequestPrebid{
+				MultiBid: []*ExtMultiBid{
+					{Bidder: "somebidder", MaxBids: ptrutil.ToPtr(3), TargetBidderCodePrefix: "SB"},
+					{Bidders: []string{"A", "B", "C"}, MaxBids: ptrutil.ToPtr(4)},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				MultiBid: []*ExtMultiBid{
+					{Bidder: "somebidder", MaxBids: ptrutil.ToPtr(3), TargetBidderCodePrefix: "SB"},
+					{Bidders: []string{"A", "B", "C"}, MaxBids: ptrutil.ToPtr(4)},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.MultiBid[0].MaxBids = ptrutil.ToPtr(2)
+				prebid.MultiBid[1].Bidders = []string{"C", "D", "E", "F"}
+				prebid.MultiBid = []*ExtMultiBid{
+					{Bidder: "otherbid"},
+				}
+			},
+		},
+		{
+			name: "PassthroughSChains",
+			prebid: &ExtRequestPrebid{
+				SChains: []*ExtRequestPrebidSChain{
+					{
+						Bidders: []string{"A", "B", "C"},
+						SChain: openrtb2.SupplyChain{
+							Complete: 1,
+							Ver:      "2.2",
+							Ext:      json.RawMessage(`{"foo": "bar"}`),
+							Nodes: []openrtb2.SupplyChainNode{
+								{
+									ASI:    "something",
+									Domain: "example.com",
+									HP:     openrtb2.Int8Ptr(1),
+								},
+							},
+						},
+					},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				SChains: []*ExtRequestPrebidSChain{
+					{
+						Bidders: []string{"A", "B", "C"},
+						SChain: openrtb2.SupplyChain{
+							Complete: 1,
+							Ver:      "2.2",
+							Ext:      json.RawMessage(`{"foo": "bar"}`),
+							Nodes: []openrtb2.SupplyChainNode{
+								{
+									ASI:    "something",
+									Domain: "example.com",
+									HP:     openrtb2.Int8Ptr(1),
+								},
+							},
+						},
+					},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.Passthrough = json.RawMessage(`{"bar": "foo"}`)
+				prebid.SChains[0].Bidders = append(prebid.SChains[0].Bidders, "D")
+				prebid.SChains[0].SChain.Ver = "2.3"
+				prebid.SChains[0].SChain.Complete = 0
+				prebid.SChains[0].SChain.Nodes[0].Name = "Alice"
+				prebid.SChains[0].SChain.Nodes[0].ASI = "New ASI"
+				prebid.SChains[0].SChain.Nodes[0].HP = openrtb2.Int8Ptr(0)
+				prebid.SChains[0].SChain.Nodes = append(prebid.SChains[0].SChain.Nodes, prebid.SChains[0].SChain.Nodes[0])
+				prebid.SChains = append(prebid.SChains, prebid.SChains[0])
+			},
+		},
+		{
+			name: "StoredRequest",
+			prebid: &ExtRequestPrebid{
+				StoredRequest: &ExtStoredRequest{
+					ID: "abc123",
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				StoredRequest: &ExtStoredRequest{
+					ID: "abc123",
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.StoredRequest.ID = "nada"
+				prebid.StoredRequest = &ExtStoredRequest{ID: "ID"}
+			},
+		},
+		{
+			name: "Targeting",
+			prebid: &ExtRequestPrebid{
+				Targeting: &ExtRequestTargeting{
+					PriceGranularity: &PriceGranularity{
+						Precision: ptrutil.ToPtr(2),
+						Ranges: []GranularityRange{
+							{Max: 2.0, Increment: 0.1},
+							{Max: 10.0, Increment: 0.5},
+							{Max: 20.0, Increment: 1.0},
+						},
+					},
+					IncludeWinners: ptrutil.ToPtr(true),
+					IncludeBrandCategory: &ExtIncludeBrandCategory{
+						PrimaryAdServer:     1,
+						Publisher:           "Bob",
+						TranslateCategories: ptrutil.ToPtr(true),
+					},
+					DurationRangeSec: []int{1, 2, 3},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				Targeting: &ExtRequestTargeting{
+					PriceGranularity: &PriceGranularity{
+						Precision: ptrutil.ToPtr(2),
+						Ranges: []GranularityRange{
+							{Max: 2.0, Increment: 0.1},
+							{Max: 10.0, Increment: 0.5},
+							{Max: 20.0, Increment: 1.0},
+						},
+					},
+					IncludeWinners: ptrutil.ToPtr(true),
+					IncludeBrandCategory: &ExtIncludeBrandCategory{
+						PrimaryAdServer:     1,
+						Publisher:           "Bob",
+						TranslateCategories: ptrutil.ToPtr(true),
+					},
+					DurationRangeSec: []int{1, 2, 3},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.Targeting.PriceGranularity.Ranges[1].Max = 12
+				prebid.Targeting.PriceGranularity.Ranges[1].Min = 2.0
+				prebid.Targeting.PriceGranularity.Ranges = append(prebid.Targeting.PriceGranularity.Ranges, GranularityRange{Max: 50, Increment: 2.0})
+				prebid.Targeting.IncludeWinners = nil
+				prebid.Targeting.IncludeBidderKeys = ptrutil.ToPtr(true)
+				prebid.Targeting.IncludeBrandCategory.TranslateCategories = ptrutil.ToPtr(false)
+				prebid.Targeting.IncludeBrandCategory = nil
+				prebid.Targeting.DurationRangeSec[1] = 5
+				prebid.Targeting.DurationRangeSec = append(prebid.Targeting.DurationRangeSec, 1)
+				prebid.Targeting.AppendBidderNames = true
+			},
+		},
+		{
+			name: "NoSale",
+			prebid: &ExtRequestPrebid{
+				NoSale: []string{"A", "B", "C"},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				NoSale: []string{"A", "B", "C"},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.NoSale[1] = "G"
+				prebid.NoSale = append(prebid.NoSale, "D")
+			},
+		},
+		{
+			name: "AlternateBidderCodes",
+			prebid: &ExtRequestPrebid{
+				AlternateBidderCodes: &ExtAlternateBidderCodes{
+					Enabled: true,
+					Bidders: map[string]ExtAdapterAlternateBidderCodes{
+						"X": {Enabled: true, AllowedBidderCodes: []string{"A", "B", "C"}},
+						"Y": {Enabled: false, AllowedBidderCodes: []string{"C", "B", "G"}},
+					},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				AlternateBidderCodes: &ExtAlternateBidderCodes{
+					Enabled: true,
+					Bidders: map[string]ExtAdapterAlternateBidderCodes{
+						"X": {Enabled: true, AllowedBidderCodes: []string{"A", "B", "C"}},
+						"Y": {Enabled: false, AllowedBidderCodes: []string{"C", "B", "G"}},
+					},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				newAABC := prebid.AlternateBidderCodes.Bidders["X"]
+				newAABC.Enabled = false
+				newAABC.AllowedBidderCodes[1] = "F"
+				newAABC.AllowedBidderCodes = append(newAABC.AllowedBidderCodes, "Z")
+				prebid.AlternateBidderCodes.Bidders["X"] = newAABC
+				prebid.AlternateBidderCodes.Bidders["Z"] = ExtAdapterAlternateBidderCodes{Enabled: true, AllowedBidderCodes: []string{"G", "Z"}}
+				prebid.AlternateBidderCodes.Enabled = false
+				prebid.AlternateBidderCodes = nil
+			},
+		},
+		{
+			name: "Floors",
+			prebid: &ExtRequestPrebid{
+				Floors: &PriceFloorRules{
+					FloorMin:    0.25,
+					FloorMinCur: "EUR",
+					Location: &PriceFloorEndpoint{
+						URL: "http://www.example.com",
+					},
+					Data: &PriceFloorData{
+						Currency: "USD",
+						SkipRate: 3,
+						ModelGroups: []PriceFloorModelGroup{
+							{
+								Currency:    "USD",
+								ModelWeight: ptrutil.ToPtr(0),
+								SkipRate:    2,
+								Schema: PriceFloorSchema{
+									Fields:    []string{"A", "B"},
+									Delimiter: "^",
+								},
+								Values: map[string]float64{"A": 2, "B": 1},
+							},
+						},
+					},
+					Enforcement: &PriceFloorEnforcement{
+						EnforceJS:   ptrutil.ToPtr(true),
+						FloorDeals:  ptrutil.ToPtr(false),
+						EnforceRate: 5,
+					},
+					Enabled:       ptrutil.ToPtr(true),
+					FloorProvider: "Someone",
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				Floors: &PriceFloorRules{
+					FloorMin:    0.25,
+					FloorMinCur: "EUR",
+					Location: &PriceFloorEndpoint{
+						URL: "http://www.example.com",
+					},
+					Data: &PriceFloorData{
+						Currency: "USD",
+						SkipRate: 3,
+						ModelGroups: []PriceFloorModelGroup{
+							{
+								Currency:    "USD",
+								ModelWeight: ptrutil.ToPtr(0),
+								SkipRate:    2,
+								Schema: PriceFloorSchema{
+									Fields:    []string{"A", "B"},
+									Delimiter: "^",
+								},
+								Values: map[string]float64{"A": 2, "B": 1},
+							},
+						},
+					},
+					Enforcement: &PriceFloorEnforcement{
+						EnforceJS:   ptrutil.ToPtr(true),
+						FloorDeals:  ptrutil.ToPtr(false),
+						EnforceRate: 5,
+					},
+					Enabled:       ptrutil.ToPtr(true),
+					FloorProvider: "Someone",
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.Floors.Data.ModelGroups[0].Schema.Fields[1] = "C"
+				prebid.Floors.Data.ModelGroups[0].Schema.Fields = append(prebid.Floors.Data.ModelGroups[0].Schema.Fields, "D")
+				prebid.Floors.Data.ModelGroups[0].Schema.Delimiter = ","
+				prebid.Floors.Data.ModelGroups[0].Currency = "CRO"
+				prebid.Floors.Data.ModelGroups[0].ModelWeight = ptrutil.ToPtr(8)
+				prebid.Floors.Data.ModelGroups[0].Values["A"] = 0
+				prebid.Floors.Data.ModelGroups[0].Values["C"] = 7
+				prebid.Floors.Data.ModelGroups = append(prebid.Floors.Data.ModelGroups, PriceFloorModelGroup{})
+				prebid.Floors.FloorMin = 0.3
+				prebid.Floors.FetchStatus = "arf"
+				prebid.Floors.Location.URL = "www.nothere.com"
+				prebid.Floors.Location = nil
+				prebid.Floors.Enabled = nil
+				prebid.Floors.Skipped = ptrutil.ToPtr(true)
+				prebid.Floors.Enforcement.BidAdjustment = ptrutil.ToPtr(true)
+				prebid.Floors.Enforcement.EnforceJS = ptrutil.ToPtr(false)
+				prebid.Floors.Enforcement.FloorDeals = nil
+				prebid.Floors.FloorProvider = ""
+			},
+		},
+		{
+			name: "MultiBidMap",
+			prebid: &ExtRequestPrebid{
+				MultiBidMap: map[string]ExtMultiBid{
+					"A": {
+						Bidder:                 "J",
+						Bidders:                []string{"X", "Y", "Z"},
+						MaxBids:                ptrutil.ToPtr(5),
+						TargetBidderCodePrefix: ">>",
+					},
+					"B": {
+						Bidder:  "J",
+						Bidders: []string{"One", "Two", "Three"},
+					},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				MultiBidMap: map[string]ExtMultiBid{
+					"A": {
+						Bidder:                 "J",
+						Bidders:                []string{"X", "Y", "Z"},
+						MaxBids:                ptrutil.ToPtr(5),
+						TargetBidderCodePrefix: ">>",
+					},
+					"B": {
+						Bidder:  "J",
+						Bidders: []string{"One", "Two", "Three"},
+					},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				mulbidB := prebid.MultiBidMap["B"]
+				mulbidB.TargetBidderCodePrefix = "|"
+				mulbidB.Bidders[1] = "Five"
+				mulbidB.Bidders = append(mulbidB.Bidders, "Six")
+				mulbidB.MaxBids = ptrutil.ToPtr(2)
+				prebid.MultiBidMap["B"] = mulbidB
+				prebid.MultiBidMap["C"] = ExtMultiBid{Bidder: "alpha", MaxBids: ptrutil.ToPtr(3)}
+			},
+		},
+		{
+			name: "MultiBidMap",
+			prebid: &ExtRequestPrebid{
+				AdServerTargeting: []AdServerTarget{
+					{
+						Key:    "A",
+						Source: "Sauce",
+						Value:  "Gold",
+					},
+					{
+						Key:    "B",
+						Source: "Omega",
+						Value:  "Dirt",
+					},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				AdServerTargeting: []AdServerTarget{
+					{
+						Key:    "A",
+						Source: "Sauce",
+						Value:  "Gold",
+					},
+					{
+						Key:    "B",
+						Source: "Omega",
+						Value:  "Dirt",
+					},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.AdServerTargeting[0].Key = "Five"
+				prebid.AdServerTargeting[1].Value = "Dust"
+				prebid.AdServerTargeting = append(prebid.AdServerTargeting, AdServerTarget{Key: "Val"})
+				prebid.AdServerTargeting = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.prebid.Clone()
+			if test.prebid != nil {
+				assert.NotSame(t, test.prebid, clone)
+			}
+			test.mutator(t, test.prebid)
+			assert.Equal(t, test.prebidCopy, clone)
+		})
+	}
+
+}
diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go
index 2986840b..e02dd741 100644
--- a/openrtb_ext/request_wrapper_test.go
+++ b/openrtb_ext/request_wrapper_test.go
@@ -5,9 +5,130 @@ import (
 	"testing"
 
 	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 )
 
+func TestCloneRequestWrapper(t *testing.T) {
+	testCases := []struct {
+		name        string
+		reqWrap     *RequestWrapper
+		reqWrapCopy *RequestWrapper                             // manual copy of above ext object to verify against
+		mutator     func(t *testing.T, reqWrap *RequestWrapper) // function to modify the Ext object
+	}{
+		{
+			name:        "Nil", // Verify the nil case
+			reqWrap:     nil,
+			reqWrapCopy: nil,
+			mutator:     func(t *testing.T, reqWrap *RequestWrapper) {},
+		},
+		{
+			name: "NoMutate",
+			reqWrap: &RequestWrapper{
+				impWrappers: []*ImpWrapper{
+					{
+						impExt: &ImpExt{prebid: &ExtImpPrebid{Options: &Options{EchoVideoAttrs: true}}, prebidDirty: true, tid: "fun"},
+					},
+					{
+						impExt: &ImpExt{tid: "star"},
+					},
+				},
+				userExt:   &UserExt{consentDirty: true},
+				deviceExt: &DeviceExt{extDirty: true},
+				requestExt: &RequestExt{
+					prebid: &ExtRequestPrebid{Integration: "derivative"},
+				},
+				appExt:    &AppExt{prebidDirty: true},
+				regExt:    &RegExt{usPrivacy: "foo"},
+				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				sourceExt: &SourceExt{schainDirty: true},
+			},
+			reqWrapCopy: &RequestWrapper{
+				impWrappers: []*ImpWrapper{
+					{
+						impExt: &ImpExt{prebid: &ExtImpPrebid{Options: &Options{EchoVideoAttrs: true}}, prebidDirty: true, tid: "fun"},
+					},
+					{
+						impExt: &ImpExt{tid: "star"},
+					},
+				},
+				userExt:   &UserExt{consentDirty: true},
+				deviceExt: &DeviceExt{extDirty: true},
+				requestExt: &RequestExt{
+					prebid: &ExtRequestPrebid{Integration: "derivative"},
+				},
+				appExt:    &AppExt{prebidDirty: true},
+				regExt:    &RegExt{usPrivacy: "foo"},
+				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				sourceExt: &SourceExt{schainDirty: true},
+			},
+			mutator: func(t *testing.T, reqWrap *RequestWrapper) {},
+		},
+		{
+			name: "General",
+			reqWrap: &RequestWrapper{
+				impWrappers: []*ImpWrapper{
+					{
+						impExt: &ImpExt{prebid: &ExtImpPrebid{Options: &Options{EchoVideoAttrs: true}}, prebidDirty: true, tid: "fun"},
+					},
+					{
+						impExt: &ImpExt{tid: "star"},
+					},
+				},
+				userExt:   &UserExt{consentDirty: true},
+				deviceExt: &DeviceExt{extDirty: true},
+				requestExt: &RequestExt{
+					prebid: &ExtRequestPrebid{Integration: "derivative"},
+				},
+				appExt:    &AppExt{prebidDirty: true},
+				regExt:    &RegExt{usPrivacy: "foo"},
+				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				sourceExt: &SourceExt{schainDirty: true},
+			},
+			reqWrapCopy: &RequestWrapper{
+				impWrappers: []*ImpWrapper{
+					{
+						impExt: &ImpExt{prebid: &ExtImpPrebid{Options: &Options{EchoVideoAttrs: true}}, prebidDirty: true, tid: "fun"},
+					},
+					{
+						impExt: &ImpExt{tid: "star"},
+					},
+				},
+				userExt:   &UserExt{consentDirty: true},
+				deviceExt: &DeviceExt{extDirty: true},
+				requestExt: &RequestExt{
+					prebid: &ExtRequestPrebid{Integration: "derivative"},
+				},
+				appExt:    &AppExt{prebidDirty: true},
+				regExt:    &RegExt{usPrivacy: "foo"},
+				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				sourceExt: &SourceExt{schainDirty: true},
+			},
+			mutator: func(t *testing.T, reqWrap *RequestWrapper) {
+				reqWrap.impWrappers[1].impExt.prebidDirty = true
+				reqWrap.impWrappers[0] = nil
+				reqWrap.impWrappers = append(reqWrap.impWrappers, &ImpWrapper{impExt: &ImpExt{tid: "star"}})
+				reqWrap.impWrappers = nil
+				reqWrap.userExt = nil
+				reqWrap.deviceExt = nil
+				reqWrap.requestExt = nil
+				reqWrap.appExt = nil
+				reqWrap.regExt = nil
+				reqWrap.siteExt = nil
+				reqWrap.sourceExt = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.reqWrap.Clone()
+			test.mutator(t, test.reqWrap)
+			assert.Equal(t, test.reqWrapCopy, clone)
+		})
+	}
+}
+
 func TestUserExt(t *testing.T) {
 	userExt := &UserExt{}
 
@@ -465,6 +586,162 @@ func TestUserExtUnmarshal(t *testing.T) {
 	}
 }
 
+func TestCloneUserExt(t *testing.T) {
+	testCases := []struct {
+		name        string
+		userExt     *UserExt
+		userExtCopy *UserExt                             // manual copy of above ext object to verify against
+		mutator     func(t *testing.T, userExt *UserExt) // function to modify the Ext object
+	}{
+		{
+			name:        "Nil", // Verify the nil case
+			userExt:     nil,
+			userExtCopy: nil,
+			mutator:     func(t *testing.T, user *UserExt) {},
+		},
+		{
+			name: "NoMutate",
+			userExt: &UserExt{
+				ext:          map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				consent:      ptrutil.ToPtr("Myconsent"),
+				consentDirty: true,
+				prebid: &ExtUserPrebid{
+					BuyerUIDs: map[string]string{"A": "X", "B": "Y"},
+				},
+				prebidDirty: true,
+				eids:        &[]openrtb2.EID{},
+			},
+			userExtCopy: &UserExt{
+				ext:          map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				consent:      ptrutil.ToPtr("Myconsent"),
+				consentDirty: true,
+				prebid: &ExtUserPrebid{
+					BuyerUIDs: map[string]string{"A": "X", "B": "Y"},
+				},
+				prebidDirty: true,
+				eids:        &[]openrtb2.EID{},
+			},
+			mutator: func(t *testing.T, user *UserExt) {},
+		},
+		{
+			name: "General",
+			userExt: &UserExt{
+				ext:          map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				consent:      ptrutil.ToPtr("Myconsent"),
+				consentDirty: true,
+				prebid: &ExtUserPrebid{
+					BuyerUIDs: map[string]string{"A": "X", "B": "Y"},
+				},
+				prebidDirty: true,
+				eids:        &[]openrtb2.EID{},
+			},
+			userExtCopy: &UserExt{
+				ext:          map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				consent:      ptrutil.ToPtr("Myconsent"),
+				consentDirty: true,
+				prebid: &ExtUserPrebid{
+					BuyerUIDs: map[string]string{"A": "X", "B": "Y"},
+				},
+				prebidDirty: true,
+				eids:        &[]openrtb2.EID{},
+			},
+			mutator: func(t *testing.T, user *UserExt) {
+				user.ext["A"] = json.RawMessage(`G`)
+				user.ext["C"] = json.RawMessage(`L`)
+				user.extDirty = true
+				user.consent = nil
+				user.consentDirty = false
+				user.prebid.BuyerUIDs["A"] = "C"
+				user.prebid.BuyerUIDs["C"] = "A"
+				user.prebid = nil
+			},
+		},
+		{
+			name: "EIDs",
+			userExt: &UserExt{
+				eids: &[]openrtb2.EID{
+					{
+						Source: "Sauce",
+						UIDs: []openrtb2.UID{
+							{ID: "A", AType: 5, Ext: json.RawMessage(`{}`)},
+							{ID: "B", AType: 1, Ext: json.RawMessage(`{"extra": "stuff"}`)},
+						},
+					},
+					{
+						Source: "Moon",
+						UIDs: []openrtb2.UID{
+							{ID: "G", AType: 3, Ext: json.RawMessage(`{}`)},
+							{ID: "D", AType: 1},
+						},
+					},
+				},
+			},
+			userExtCopy: &UserExt{
+				eids: &[]openrtb2.EID{
+					{
+						Source: "Sauce",
+						UIDs: []openrtb2.UID{
+							{ID: "A", AType: 5, Ext: json.RawMessage(`{}`)},
+							{ID: "B", AType: 1, Ext: json.RawMessage(`{"extra": "stuff"}`)},
+						},
+					},
+					{
+						Source: "Moon",
+						UIDs: []openrtb2.UID{
+							{ID: "G", AType: 3, Ext: json.RawMessage(`{}`)},
+							{ID: "D", AType: 1},
+						},
+					},
+				},
+			},
+			mutator: func(t *testing.T, userExt *UserExt) {
+				eids := *userExt.eids
+				eids[0].UIDs[1].ID = "G2"
+				eids[1].UIDs[0].AType = 0
+				eids[0].UIDs = append(eids[0].UIDs, openrtb2.UID{ID: "Z", AType: 2})
+				eids = append(eids, openrtb2.EID{Source: "Blank"})
+				userExt.eids = nil
+			},
+		},
+		{
+			name: "ConsentedProviders",
+			userExt: &UserExt{
+				consentedProvidersSettingsIn: &ConsentedProvidersSettingsIn{
+					ConsentedProvidersString: "A,B,C",
+				},
+				consentedProvidersSettingsOut: &ConsentedProvidersSettingsOut{
+					ConsentedProvidersList: []int{1, 2, 3, 4},
+				},
+			},
+			userExtCopy: &UserExt{
+				consentedProvidersSettingsIn: &ConsentedProvidersSettingsIn{
+					ConsentedProvidersString: "A,B,C",
+				},
+				consentedProvidersSettingsOut: &ConsentedProvidersSettingsOut{
+					ConsentedProvidersList: []int{1, 2, 3, 4},
+				},
+			},
+			mutator: func(t *testing.T, userExt *UserExt) {
+				userExt.consentedProvidersSettingsIn.ConsentedProvidersString = "B,C,D"
+				userExt.consentedProvidersSettingsIn = &ConsentedProvidersSettingsIn{
+					ConsentedProvidersString: "G,H,I",
+				}
+				userExt.consentedProvidersSettingsOut.ConsentedProvidersList[1] = 5
+				userExt.consentedProvidersSettingsOut.ConsentedProvidersList = append(userExt.consentedProvidersSettingsOut.ConsentedProvidersList, 7)
+				userExt.consentedProvidersSettingsOut = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.userExt.Clone()
+			test.mutator(t, test.userExt)
+			assert.Equal(t, test.userExtCopy, clone)
+		})
+	}
+}
+
 func TestRebuildDeviceExt(t *testing.T) {
 	prebidContent1 := ExtDevicePrebid{Interstitial: &ExtDeviceInt{MinWidthPerc: 1}}
 	prebidContent2 := ExtDevicePrebid{Interstitial: &ExtDeviceInt{MinWidthPerc: 2}}
@@ -611,6 +888,172 @@ func TestRebuildRequestExt(t *testing.T) {
 	}
 }
 
+func TestCloneRequestExt(t *testing.T) {
+	testCases := []struct {
+		name       string
+		reqExt     *RequestExt
+		reqExtCopy *RequestExt                            // manual copy of above ext object to verify against
+		mutator    func(t *testing.T, reqExt *RequestExt) // function to modify the Ext object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			reqExt:     nil,
+			reqExtCopy: nil,
+			mutator:    func(t *testing.T, reqExt *RequestExt) {},
+		},
+		{
+			name: "NoMutate", // Verify the nil case
+			reqExt: &RequestExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtRequestPrebid{
+					BidderParams: json.RawMessage(`{}`),
+				},
+			},
+			reqExtCopy: &RequestExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtRequestPrebid{
+					BidderParams: json.RawMessage(`{}`),
+				},
+			},
+			mutator: func(t *testing.T, reqExt *RequestExt) {},
+		},
+		{
+			name: "General", // Verify the nil case
+			reqExt: &RequestExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtRequestPrebid{
+					BidderParams: json.RawMessage(`{}`),
+				},
+			},
+			reqExtCopy: &RequestExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtRequestPrebid{
+					BidderParams: json.RawMessage(`{}`),
+				},
+			},
+			mutator: func(t *testing.T, reqExt *RequestExt) {
+				reqExt.ext["A"] = json.RawMessage(`"string"`)
+				reqExt.ext["C"] = json.RawMessage(`{}`)
+				reqExt.extDirty = false
+				reqExt.prebid.Channel = &ExtRequestPrebidChannel{Name: "Bob"}
+				reqExt.prebid.BidderParams = nil
+				reqExt.prebid = nil
+			},
+		},
+		{
+			name: "SChain", // Verify the nil case
+			reqExt: &RequestExt{
+				schain: &openrtb2.SupplyChain{
+					Complete: 1,
+					Ver:      "1.1",
+					Nodes: []openrtb2.SupplyChainNode{
+						{ASI: "Is a", RID: "off", HP: ptrutil.ToPtr[int8](1)},
+						{ASI: "other", RID: "drift", HP: ptrutil.ToPtr[int8](0)},
+					},
+				},
+			},
+			reqExtCopy: &RequestExt{
+				schain: &openrtb2.SupplyChain{
+					Complete: 1,
+					Ver:      "1.1",
+					Nodes: []openrtb2.SupplyChainNode{
+						{ASI: "Is a", RID: "off", HP: ptrutil.ToPtr[int8](1)},
+						{ASI: "other", RID: "drift", HP: ptrutil.ToPtr[int8](0)},
+					},
+				},
+			},
+			mutator: func(t *testing.T, reqExt *RequestExt) {
+				reqExt.schain.Complete = 0
+				reqExt.schain.Ver = "1.2"
+				reqExt.schain.Nodes[0].ASI = "some"
+				reqExt.schain.Nodes[1].HP = nil
+				reqExt.schain.Nodes = append(reqExt.schain.Nodes, openrtb2.SupplyChainNode{ASI: "added"})
+				reqExt.schain = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.reqExt.Clone()
+			test.mutator(t, test.reqExt)
+			assert.Equal(t, test.reqExtCopy, clone)
+		})
+	}
+
+}
+
+func TestCloneDeviceExt(t *testing.T) {
+	testCases := []struct {
+		name       string
+		devExt     *DeviceExt
+		devExtCopy *DeviceExt                            // manual copy of above ext object to verify against
+		mutator    func(t *testing.T, devExt *DeviceExt) // function to modify the Ext object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			devExt:     nil,
+			devExtCopy: nil,
+			mutator:    func(t *testing.T, devExt *DeviceExt) {},
+		},
+		{
+			name: "NoMutate",
+			devExt: &DeviceExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtDevicePrebid{
+					Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0},
+				},
+			},
+			devExtCopy: &DeviceExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtDevicePrebid{
+					Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0},
+				},
+			},
+			mutator: func(t *testing.T, devExt *DeviceExt) {},
+		},
+		{
+			name: "General",
+			devExt: &DeviceExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtDevicePrebid{
+					Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0},
+				},
+			},
+			devExtCopy: &DeviceExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtDevicePrebid{
+					Interstitial: &ExtDeviceInt{MinWidthPerc: 65, MinHeightPerc: 75},
+				},
+			},
+			mutator: func(t *testing.T, devExt *DeviceExt) {
+				devExt.ext["A"] = json.RawMessage(`"string"`)
+				devExt.ext["C"] = json.RawMessage(`{}`)
+				devExt.extDirty = false
+				devExt.prebid.Interstitial.MinHeightPerc = 55
+				devExt.prebid.Interstitial = &ExtDeviceInt{MinWidthPerc: 80}
+				devExt.prebid = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.devExt.Clone()
+			test.mutator(t, test.devExt)
+			assert.Equal(t, test.devExtCopy, clone)
+		})
+	}
+}
+
 func TestRebuildAppExt(t *testing.T) {
 	prebidContent1 := ExtAppPrebid{Source: "1"}
 	prebidContent2 := ExtAppPrebid{Source: "2"}
@@ -693,6 +1136,143 @@ func TestRebuildAppExt(t *testing.T) {
 	}
 }
 
+func TestCloneAppExt(t *testing.T) {
+	testCases := []struct {
+		name       string
+		appExt     *AppExt
+		appExtCopy *AppExt                            // manual copy of above ext object to verify against
+		mutator    func(t *testing.T, appExt *AppExt) // function to modify the Ext object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			appExt:     nil,
+			appExtCopy: nil,
+			mutator:    func(t *testing.T, appExt *AppExt) {},
+		},
+		{
+			name: "NoMutate",
+			appExt: &AppExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				prebid: &ExtAppPrebid{
+					Source:  "Sauce",
+					Version: "2.2",
+				},
+			},
+			appExtCopy: &AppExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				prebid: &ExtAppPrebid{
+					Source:  "Sauce",
+					Version: "2.2",
+				},
+			},
+			mutator: func(t *testing.T, appExt *AppExt) {},
+		},
+		{
+			name: "General",
+			appExt: &AppExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				prebid: &ExtAppPrebid{
+					Source:  "Sauce",
+					Version: "2.2",
+				},
+			},
+			appExtCopy: &AppExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				prebid: &ExtAppPrebid{
+					Source:  "Sauce",
+					Version: "2.2",
+				},
+			},
+			mutator: func(t *testing.T, appExt *AppExt) {
+				appExt.ext["A"] = json.RawMessage(`"string"`)
+				appExt.ext["C"] = json.RawMessage(`{}`)
+				appExt.extDirty = false
+				appExt.prebid.Source = "foobar"
+				appExt.prebid = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.appExt.Clone()
+			test.mutator(t, test.appExt)
+			assert.Equal(t, test.appExtCopy, clone)
+		})
+	}
+}
+
+func TestCloneRegExt(t *testing.T) {
+	testCases := []struct {
+		name       string
+		regExt     *RegExt
+		regExtCopy *RegExt                            // manual copy of above ext object to verify against
+		mutator    func(t *testing.T, regExt *RegExt) // function to modify the Ext object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			regExt:     nil,
+			regExtCopy: nil,
+			mutator:    func(t *testing.T, appExt *RegExt) {},
+		},
+		{
+			name: "NoMutate",
+			regExt: &RegExt{
+				ext:            map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty:       true,
+				gdpr:           ptrutil.ToPtr[int8](1),
+				usPrivacy:      "priv",
+				usPrivacyDirty: true,
+			},
+			regExtCopy: &RegExt{
+				ext:            map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty:       true,
+				gdpr:           ptrutil.ToPtr[int8](1),
+				usPrivacy:      "priv",
+				usPrivacyDirty: true,
+			},
+			mutator: func(t *testing.T, appExt *RegExt) {},
+		},
+		{
+			name: "General",
+			regExt: &RegExt{
+				ext:            map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty:       true,
+				gdpr:           ptrutil.ToPtr[int8](1),
+				usPrivacy:      "priv",
+				usPrivacyDirty: true,
+			},
+			regExtCopy: &RegExt{
+				ext:            map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty:       true,
+				gdpr:           ptrutil.ToPtr[int8](1),
+				usPrivacy:      "priv",
+				usPrivacyDirty: true,
+			},
+			mutator: func(t *testing.T, appExt *RegExt) {
+				appExt.ext["A"] = json.RawMessage(`"string"`)
+				appExt.ext["C"] = json.RawMessage(`{}`)
+				appExt.extDirty = false
+				appExt.gdpr = nil
+				appExt.gdprDirty = true
+				appExt.usPrivacy = "Other"
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.regExt.Clone()
+			test.mutator(t, test.regExt)
+			assert.Equal(t, test.regExtCopy, clone)
+		})
+	}
+}
+
 func TestRebuildSiteExt(t *testing.T) {
 	int1 := int8(1)
 	int2 := int8(2)
@@ -775,6 +1355,64 @@ func TestRebuildSiteExt(t *testing.T) {
 	}
 }
 
+func TestCloneSiteExt(t *testing.T) {
+	testCases := []struct {
+		name        string
+		siteExt     *SiteExt
+		siteExtCopy *SiteExt                             // manual copy of above ext object to verify against
+		mutator     func(t *testing.T, siteExt *SiteExt) // function to modify the Ext object
+	}{
+		{
+			name:        "Nil", // Verify the nil case
+			siteExt:     nil,
+			siteExtCopy: nil,
+			mutator:     func(t *testing.T, siteExt *SiteExt) {},
+		},
+		{
+			name: "NoMutate",
+			siteExt: &SiteExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				amp:      ptrutil.ToPtr[int8](1),
+			},
+			siteExtCopy: &SiteExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				amp:      ptrutil.ToPtr[int8](1),
+			},
+			mutator: func(t *testing.T, siteExt *SiteExt) {},
+		},
+		{
+			name: "General",
+			siteExt: &SiteExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				amp:      ptrutil.ToPtr[int8](1),
+			},
+			siteExtCopy: &SiteExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				amp:      ptrutil.ToPtr[int8](1),
+			},
+			mutator: func(t *testing.T, siteExt *SiteExt) {
+				siteExt.ext["A"] = json.RawMessage(`"string"`)
+				siteExt.ext["C"] = json.RawMessage(`{}`)
+				siteExt.extDirty = false
+				siteExt.amp = nil
+				siteExt.ampDirty = true
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.siteExt.Clone()
+			test.mutator(t, test.siteExt)
+			assert.Equal(t, test.siteExtCopy, clone)
+		})
+	}
+}
+
 func TestRebuildSourceExt(t *testing.T) {
 	schainContent1 := openrtb2.SupplyChain{Ver: "1"}
 	schainContent2 := openrtb2.SupplyChain{Ver: "2"}
@@ -857,6 +1495,69 @@ func TestRebuildSourceExt(t *testing.T) {
 	}
 }
 
+func TestCloneSourceExt(t *testing.T) {
+	testCases := []struct {
+		name          string
+		sourceExt     *SourceExt
+		sourceExtCopy *SourceExt                               // manual copy of above ext object to verify against
+		mutator       func(t *testing.T, sourceExt *SourceExt) // function to modify the Ext object
+	}{
+		{
+			name:          "Nil", // Verify the nil case
+			sourceExt:     nil,
+			sourceExtCopy: nil,
+			mutator:       func(t *testing.T, sourceExt *SourceExt) {},
+		},
+		{
+			name: "NoMutate",
+			sourceExt: &SourceExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				schain: &openrtb2.SupplyChain{
+					Complete: 1,
+					Ver:      "1.1",
+					Nodes: []openrtb2.SupplyChainNode{
+						{ASI: "Is a", RID: "off", HP: ptrutil.ToPtr[int8](1)},
+						{ASI: "other", RID: "drift", HP: ptrutil.ToPtr[int8](0)},
+					},
+				},
+			},
+			sourceExtCopy: &SourceExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				schain: &openrtb2.SupplyChain{
+					Complete: 1,
+					Ver:      "1.1",
+					Nodes: []openrtb2.SupplyChainNode{
+						{ASI: "Is a", RID: "off", HP: ptrutil.ToPtr[int8](1)},
+						{ASI: "other", RID: "drift", HP: ptrutil.ToPtr[int8](0)},
+					},
+				},
+			},
+			mutator: func(t *testing.T, sourceExt *SourceExt) {
+				sourceExt.ext["A"] = json.RawMessage(`"string"`)
+				sourceExt.ext["C"] = json.RawMessage(`{}`)
+				sourceExt.extDirty = false
+				sourceExt.schain.Complete = 0
+				sourceExt.schain.Ver = "1.2"
+				sourceExt.schain.Nodes[0].ASI = "some"
+				sourceExt.schain.Nodes[1].HP = nil
+				sourceExt.schain.Nodes = append(sourceExt.schain.Nodes, openrtb2.SupplyChainNode{ASI: "added"})
+				sourceExt.schain = nil
+
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.sourceExt.Clone()
+			test.mutator(t, test.sourceExt)
+			assert.Equal(t, test.sourceExtCopy, clone)
+		})
+	}
+}
+
 func TestImpWrapperRebuildImp(t *testing.T) {
 	var (
 		isRewardedInventoryOne int8 = 1
@@ -1026,3 +1727,184 @@ func TestImpExtTid(t *testing.T) {
 	assert.Equal(t, "tid", impExt.GetTid(), "ImpExt tid is incorrect")
 	assert.Equal(t, true, impExt.Dirty(), "New impext should be dirty.")
 }
+
+func TestCloneImpWrapper(t *testing.T) {
+	testCases := []struct {
+		name           string
+		impWrapper     *ImpWrapper
+		impWrapperCopy *ImpWrapper                                // manual copy of above ext object to verify against
+		mutator        func(t *testing.T, impWrapper *ImpWrapper) // function to modify the Ext object
+	}{
+		{
+			name:           "Nil", // Verify the nil case
+			impWrapper:     nil,
+			impWrapperCopy: nil,
+			mutator:        func(t *testing.T, impWrapper *ImpWrapper) {},
+		},
+		{
+			name: "NoMutate",
+			impWrapper: &ImpWrapper{
+				impExt: &ImpExt{
+					tid: "occupied",
+				},
+			},
+			impWrapperCopy: &ImpWrapper{
+				impExt: &ImpExt{
+					tid: "occupied",
+				},
+			},
+			mutator: func(t *testing.T, impWrapper *ImpWrapper) {},
+		},
+		{
+			name: "General",
+			impWrapper: &ImpWrapper{
+				impExt: &ImpExt{
+					tid: "occupied",
+				},
+			},
+			impWrapperCopy: &ImpWrapper{
+				impExt: &ImpExt{
+					tid: "occupied",
+				},
+			},
+			mutator: func(t *testing.T, impWrapper *ImpWrapper) {
+				impWrapper.impExt.extDirty = true
+				impWrapper.impExt.tid = "Something"
+				impWrapper.impExt = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.impWrapper.Clone()
+			test.mutator(t, test.impWrapper)
+			assert.Equal(t, test.impWrapperCopy, clone)
+		})
+	}
+}
+
+func TestCloneImpExt(t *testing.T) {
+	testCases := []struct {
+		name       string
+		impExt     *ImpExt
+		impExtCopy *ImpExt                            // manual copy of above ext object to verify against
+		mutator    func(t *testing.T, impExt *ImpExt) // function to modify the Ext object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			impExt:     nil,
+			impExtCopy: nil,
+			mutator:    func(t *testing.T, impExt *ImpExt) {},
+		},
+		{
+			name: "NoMutate",
+			impExt: &ImpExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				tid:      "TID",
+			},
+			impExtCopy: &ImpExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				tid:      "TID",
+			},
+			mutator: func(t *testing.T, impExt *ImpExt) {},
+		},
+		{
+			name: "General",
+			impExt: &ImpExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				tid:      "TID",
+			},
+			impExtCopy: &ImpExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				tid:      "TID",
+			},
+			mutator: func(t *testing.T, impExt *ImpExt) {
+				impExt.ext["A"] = json.RawMessage(`"string"`)
+				impExt.ext["C"] = json.RawMessage(`{}`)
+				impExt.extDirty = false
+				impExt.tid = "other"
+				impExt.tidDirty = true
+			},
+		},
+		{
+			name: "Prebid",
+			impExt: &ImpExt{
+				prebid: &ExtImpPrebid{
+					StoredRequest:         &ExtStoredRequest{ID: "abc123"},
+					StoredAuctionResponse: &ExtStoredAuctionResponse{ID: "123abc"},
+					StoredBidResponse: []ExtStoredBidResponse{
+						{ID: "foo", Bidder: "bar", ReplaceImpId: ptrutil.ToPtr(true)},
+						{ID: "def", Bidder: "xyz", ReplaceImpId: ptrutil.ToPtr(false)},
+					},
+					IsRewardedInventory: ptrutil.ToPtr[int8](1),
+					Bidder: map[string]json.RawMessage{
+						"abc": json.RawMessage(`{}`),
+						"def": json.RawMessage(`{"alpha":"beta"}`),
+					},
+					Options:     &Options{EchoVideoAttrs: true},
+					Passthrough: json.RawMessage(`{"foo":"bar"}`),
+					Floors: &ExtImpPrebidFloors{
+						FloorRule:      "Rule 16",
+						FloorRuleValue: 16.17,
+						FloorValue:     6.7,
+					},
+				},
+			},
+			impExtCopy: &ImpExt{
+				prebid: &ExtImpPrebid{
+					StoredRequest:         &ExtStoredRequest{ID: "abc123"},
+					StoredAuctionResponse: &ExtStoredAuctionResponse{ID: "123abc"},
+					StoredBidResponse: []ExtStoredBidResponse{
+						{ID: "foo", Bidder: "bar", ReplaceImpId: ptrutil.ToPtr(true)},
+						{ID: "def", Bidder: "xyz", ReplaceImpId: ptrutil.ToPtr(false)},
+					},
+					IsRewardedInventory: ptrutil.ToPtr[int8](1),
+					Bidder: map[string]json.RawMessage{
+						"abc": json.RawMessage(`{}`),
+						"def": json.RawMessage(`{"alpha":"beta"}`),
+					},
+					Options:     &Options{EchoVideoAttrs: true},
+					Passthrough: json.RawMessage(`{"foo":"bar"}`),
+					Floors: &ExtImpPrebidFloors{
+						FloorRule:      "Rule 16",
+						FloorRuleValue: 16.17,
+						FloorValue:     6.7,
+					},
+				},
+			},
+			mutator: func(t *testing.T, impExt *ImpExt) {
+				impExt.prebid.StoredRequest.ID = "seventy"
+				impExt.prebid.StoredRequest = nil
+				impExt.prebid.StoredAuctionResponse.ID = "xyz"
+				impExt.prebid.StoredAuctionResponse = nil
+				impExt.prebid.StoredBidResponse[0].ID = "alpha"
+				impExt.prebid.StoredBidResponse[1].ReplaceImpId = nil
+				impExt.prebid.StoredBidResponse[0] = ExtStoredBidResponse{ID: "o", Bidder: "k", ReplaceImpId: ptrutil.ToPtr(false)}
+				impExt.prebid.StoredBidResponse = append(impExt.prebid.StoredBidResponse, ExtStoredBidResponse{ID: "jay", Bidder: "walk"})
+				impExt.prebid.IsRewardedInventory = nil
+				impExt.prebid.Bidder["def"] = json.RawMessage(``)
+				delete(impExt.prebid.Bidder, "abc")
+				impExt.prebid.Bidder["xyz"] = json.RawMessage(`{"jar":5}`)
+				impExt.prebid.Options.EchoVideoAttrs = false
+				impExt.prebid.Options = nil
+				impExt.prebid.Passthrough = json.RawMessage(`{}`)
+				impExt.prebid.Floors.FloorRule = "Friday"
+				impExt.prebid.Floors.FloorMinCur = "EUR"
+				impExt.prebid.Floors = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.impExt.Clone()
+			test.mutator(t, test.impExt)
+			assert.Equal(t, test.impExtCopy, clone)
+		})
+	}
+}
diff --git a/openrtb_ext/supplyChain_test.go b/openrtb_ext/supplyChain_test.go
new file mode 100644
index 00000000..12fd5c33
--- /dev/null
+++ b/openrtb_ext/supplyChain_test.go
@@ -0,0 +1,83 @@
+package openrtb_ext
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/util/ptrutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCloneSupplyChain(t *testing.T) {
+	testCases := []struct {
+		name       string
+		schain     *openrtb2.SupplyChain
+		schainCopy *openrtb2.SupplyChain                            // manual copy of above prebid object to verify against
+		mutator    func(t *testing.T, schain *openrtb2.SupplyChain) // function to modify the prebid object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			schain:     nil,
+			schainCopy: nil,
+			mutator:    func(t *testing.T, schain *openrtb2.SupplyChain) {},
+		},
+		{
+			name: "General",
+			schain: &openrtb2.SupplyChain{
+				Complete: 2,
+				Nodes: []openrtb2.SupplyChainNode{
+					{
+						SID:  "alpha",
+						Name: "Johnny",
+						HP:   ptrutil.ToPtr[int8](5),
+						Ext:  json.RawMessage(`{}`),
+					},
+					{
+						ASI:  "Oh my",
+						Name: "Johnny",
+						HP:   ptrutil.ToPtr[int8](5),
+						Ext:  json.RawMessage(`{"samson"}`),
+					},
+				},
+				Ver: "v2.5",
+				Ext: json.RawMessage(`{"foo": "bar"}`),
+			},
+			schainCopy: &openrtb2.SupplyChain{
+				Complete: 2,
+				Nodes: []openrtb2.SupplyChainNode{
+					{
+						SID:  "alpha",
+						Name: "Johnny",
+						HP:   ptrutil.ToPtr[int8](5),
+						Ext:  json.RawMessage(`{}`),
+					},
+					{
+						ASI:  "Oh my",
+						Name: "Johnny",
+						HP:   ptrutil.ToPtr[int8](5),
+						Ext:  json.RawMessage(`{"samson"}`),
+					},
+				},
+				Ver: "v2.5",
+				Ext: json.RawMessage(`{"foo": "bar"}`),
+			},
+			mutator: func(t *testing.T, schain *openrtb2.SupplyChain) {
+				schain.Nodes[0].SID = "beta"
+				schain.Nodes[1].HP = nil
+				schain.Nodes[0].Ext = nil
+				schain.Nodes = append(schain.Nodes, openrtb2.SupplyChainNode{SID: "Gamma"})
+				schain.Complete = 0
+				schain.Ext = json.RawMessage(`{}`)
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := cloneSupplyChain(test.schain)
+			test.mutator(t, test.schain)
+			assert.Equal(t, test.schainCopy, clone)
+		})
+	}
+}
diff --git a/util/maputil/maputil_test.go b/util/maputil/maputil_test.go
index 114c2332..e7c06722 100644
--- a/util/maputil/maputil_test.go
+++ b/util/maputil/maputil_test.go
@@ -245,3 +245,29 @@ func TestHasElement(t *testing.T) {
 		assert.Equal(t, test.expected, result, test.description)
 	}
 }
+
+func TestCloneMap(t *testing.T) {
+	// Test we handle nils properly
+	t.Run("NilMap", func(t *testing.T) {
+		var testMap, copyMap map[string]string = nil, nil // copyMap is a manual copy of testMap
+		clone := Clone(testMap)
+		testMap = map[string]string{"foo": "bar"}
+		assert.Equal(t, copyMap, clone)
+	})
+	// Test a simple string map
+	t.Run("StringMap", func(t *testing.T) {
+		var testMap, copyMap map[string]string = map[string]string{"foo": "bar", "first": "one"}, map[string]string{"foo": "bar", "first": "one"}
+		clone := Clone(testMap)
+		testMap["foo"] = "baz"
+		testMap["bozo"] = "the clown"
+		assert.Equal(t, copyMap, clone)
+	})
+	// Test a simple map[string]int
+	t.Run("StringInt", func(t *testing.T) {
+		var testMap, copyMap map[string]int = map[string]int{"foo": 1, "first": 2}, map[string]int{"foo": 1, "first": 2}
+		clone := Clone(testMap)
+		testMap["foo"] = 7
+		testMap["bozo"] = 13
+		assert.Equal(t, copyMap, clone)
+	})
+}