diff --git a/adapters/bidder.go b/adapters/bidder.go
index c18dde7c120..b659f969f71 100644
--- a/adapters/bidder.go
+++ b/adapters/bidder.go
@@ -156,6 +156,8 @@ type ExtraRequestInfo struct {
PbsEntryPoint metrics.RequestType
GlobalPrivacyControlHeader string
CurrencyConversions currency.Conversions
+
+ BidderCoreName openrtb_ext.BidderName // OW specific: required for oRTB bidder
}
func NewExtraRequestInfo(c currency.Conversions) ExtraRequestInfo {
diff --git a/adapters/ortbbidder/bidderparams/config.go b/adapters/ortbbidder/bidderparams/config.go
new file mode 100644
index 00000000000..1b75da9bf25
--- /dev/null
+++ b/adapters/ortbbidder/bidderparams/config.go
@@ -0,0 +1,54 @@
+package bidderparams
+
+// BidderParamMapper contains property details like location
+type BidderParamMapper struct {
+ location []string
+}
+
+// GetLocation returns the location of bidderParam
+func (bpm *BidderParamMapper) GetLocation() []string {
+ return bpm.location
+}
+
+// SetLocation sets the location in BidderParamMapper
+// Do not modify the location of bidderParam unless you are writing unit test case
+func (bpm *BidderParamMapper) SetLocation(location []string) {
+ bpm.location = location
+}
+
+// config contains mappings requestParams and responseParams
+type config struct {
+ requestParams map[string]BidderParamMapper
+ responseParams map[string]BidderParamMapper
+}
+
+// BidderConfig contains map of bidderName to its requestParams and responseParams
+type BidderConfig struct {
+ bidderConfigMap map[string]*config
+}
+
+// setRequestParams sets the bidder specific requestParams
+func (bcfg *BidderConfig) setRequestParams(bidderName string, requestParams map[string]BidderParamMapper) {
+ if bcfg == nil {
+ return
+ }
+ if bcfg.bidderConfigMap == nil {
+ bcfg.bidderConfigMap = make(map[string]*config)
+ }
+ if _, found := bcfg.bidderConfigMap[bidderName]; !found {
+ bcfg.bidderConfigMap[bidderName] = &config{}
+ }
+ bcfg.bidderConfigMap[bidderName].requestParams = requestParams
+}
+
+// GetRequestParams returns bidder specific requestParams
+func (bcfg *BidderConfig) GetRequestParams(bidderName string) (map[string]BidderParamMapper, bool) {
+ if bcfg == nil || len(bcfg.bidderConfigMap) == 0 {
+ return nil, false
+ }
+ bidderConfig, _ := bcfg.bidderConfigMap[bidderName]
+ if bidderConfig == nil {
+ return nil, false
+ }
+ return bidderConfig.requestParams, true
+}
diff --git a/adapters/ortbbidder/bidderparams/config_test.go b/adapters/ortbbidder/bidderparams/config_test.go
new file mode 100644
index 00000000000..580689e6bf4
--- /dev/null
+++ b/adapters/ortbbidder/bidderparams/config_test.go
@@ -0,0 +1,321 @@
+package bidderparams
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSetRequestParams(t *testing.T) {
+ type fields struct {
+ bidderConfig *BidderConfig
+ }
+ type args struct {
+ bidderName string
+ requestParams map[string]BidderParamMapper
+ }
+
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want *BidderConfig
+ }{
+ {
+ name: "bidderConfig_is_nil",
+ fields: fields{
+ bidderConfig: nil,
+ },
+ args: args{
+ bidderName: "test",
+ requestParams: map[string]BidderParamMapper{
+ "adunit": {
+ location: []string{"ext", "adunit"},
+ },
+ },
+ },
+ want: nil,
+ },
+ {
+ name: "bidderConfigMap_is_nil",
+ fields: fields{
+ bidderConfig: &BidderConfig{
+ bidderConfigMap: nil,
+ },
+ },
+ args: args{
+ bidderName: "test",
+ requestParams: map[string]BidderParamMapper{
+ "adunit": {
+ location: []string{"ext", "adunit"},
+ },
+ },
+ },
+ want: &BidderConfig{
+ bidderConfigMap: map[string]*config{
+ "test": {
+ requestParams: map[string]BidderParamMapper{
+ "adunit": {
+ location: []string{"ext", "adunit"},
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "bidderName_not_found",
+ fields: fields{
+ bidderConfig: &BidderConfig{
+ bidderConfigMap: map[string]*config{},
+ },
+ },
+ args: args{
+ bidderName: "test",
+ requestParams: map[string]BidderParamMapper{
+ "param-1": {
+ location: []string{"path"},
+ },
+ },
+ },
+ want: &BidderConfig{
+ bidderConfigMap: map[string]*config{
+ "test": {
+ requestParams: map[string]BidderParamMapper{
+ "param-1": {
+ location: []string{"path"},
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "bidderName_found",
+ fields: fields{
+ bidderConfig: &BidderConfig{
+ bidderConfigMap: map[string]*config{
+ "test": {
+ requestParams: map[string]BidderParamMapper{
+ "param-1": {
+ location: []string{"path-1"},
+ },
+ },
+ },
+ },
+ },
+ },
+ args: args{
+ bidderName: "test",
+ requestParams: map[string]BidderParamMapper{
+ "param-2": {
+ location: []string{"path-2"},
+ },
+ },
+ },
+ want: &BidderConfig{
+ bidderConfigMap: map[string]*config{
+ "test": {
+ requestParams: map[string]BidderParamMapper{
+ "param-2": {
+ location: []string{"path-2"},
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.fields.bidderConfig.setRequestParams(tt.args.bidderName, tt.args.requestParams)
+ assert.Equal(t, tt.want, tt.fields.bidderConfig, "mismatched bidderConfig")
+ })
+ }
+}
+
+func TestGetBidderRequestProperties(t *testing.T) {
+ type fields struct {
+ biddersConfig *BidderConfig
+ }
+ type args struct {
+ bidderName string
+ }
+ type want struct {
+ requestParams map[string]BidderParamMapper
+ found bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want want
+ }{
+ {
+ name: "BidderConfig_is_nil",
+ fields: fields{
+ biddersConfig: nil,
+ },
+ args: args{
+ bidderName: "test",
+ },
+ want: want{
+ requestParams: nil,
+ found: false,
+ },
+ },
+ {
+ name: "BidderConfigMap_is_nil",
+ fields: fields{
+ biddersConfig: &BidderConfig{
+ bidderConfigMap: nil,
+ },
+ },
+ args: args{
+ bidderName: "test",
+ },
+ want: want{
+ requestParams: nil,
+ found: false,
+ },
+ },
+ {
+ name: "BidderName_absent_in_biddersConfigMap",
+ fields: fields{
+ biddersConfig: &BidderConfig{
+ bidderConfigMap: map[string]*config{
+ "ortb": {},
+ },
+ },
+ },
+ args: args{
+ bidderName: "test",
+ },
+ want: want{
+ requestParams: nil,
+ found: false,
+ },
+ },
+ {
+ name: "BidderName_present_but_config_is_nil",
+ fields: fields{
+ biddersConfig: &BidderConfig{
+ bidderConfigMap: map[string]*config{
+ "ortb": nil,
+ },
+ },
+ },
+ args: args{
+ bidderName: "test",
+ },
+ want: want{
+ requestParams: nil,
+ found: false,
+ },
+ },
+ {
+ name: "BidderName_present_in_biddersConfigMap",
+ fields: fields{
+ biddersConfig: &BidderConfig{
+ bidderConfigMap: map[string]*config{
+ "test": {
+ requestParams: map[string]BidderParamMapper{
+ "param-1": {
+ location: []string{"value-1"},
+ },
+ },
+ },
+ },
+ },
+ },
+ args: args{
+ bidderName: "test",
+ },
+ want: want{
+ requestParams: map[string]BidderParamMapper{
+ "param-1": {
+ location: []string{"value-1"},
+ },
+ },
+ found: true,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ params, found := tt.fields.biddersConfig.GetRequestParams(tt.args.bidderName)
+ assert.Equal(t, tt.want.requestParams, params, "mismatched requestParams")
+ assert.Equal(t, tt.want.found, found, "mismatched found value")
+ })
+ }
+}
+
+func TestBidderParamMapperGetLocation(t *testing.T) {
+ tests := []struct {
+ name string
+ bpm BidderParamMapper
+ want []string
+ }{
+ {
+ name: "location_is_nil",
+ bpm: BidderParamMapper{
+ location: nil,
+ },
+ want: nil,
+ },
+ {
+ name: "location_is_non_empty",
+ bpm: BidderParamMapper{
+ location: []string{"req", "ext"},
+ },
+ want: []string{"req", "ext"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := tt.bpm.GetLocation()
+ assert.Equal(t, tt.want, got, "mismatched location")
+ })
+ }
+}
+
+func TestBidderParamMapperSetLocation(t *testing.T) {
+ type args struct {
+ location []string
+ }
+ tests := []struct {
+ name string
+ bpm BidderParamMapper
+ args args
+ want BidderParamMapper
+ }{
+ {
+ name: "set_location",
+ bpm: BidderParamMapper{},
+ args: args{
+ location: []string{"req", "ext"},
+ },
+ want: BidderParamMapper{
+ location: []string{"req", "ext"},
+ },
+ },
+ {
+ name: "override_location",
+ bpm: BidderParamMapper{
+ location: []string{"imp", "ext"},
+ },
+ args: args{
+ location: []string{"req", "ext"},
+ },
+ want: BidderParamMapper{
+ location: []string{"req", "ext"},
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.bpm.SetLocation(tt.args.location)
+ assert.Equal(t, tt.want, tt.bpm, "mismatched location")
+ })
+ }
+}
diff --git a/adapters/ortbbidder/bidderparams/parser.go b/adapters/ortbbidder/bidderparams/parser.go
new file mode 100644
index 00000000000..8d198464fcb
--- /dev/null
+++ b/adapters/ortbbidder/bidderparams/parser.go
@@ -0,0 +1,85 @@
+package bidderparams
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+const (
+ propertiesKey = "properties"
+ locationKey = "location"
+)
+
+// LoadBidderConfig creates a bidderConfig from JSON files specified in dirPath directory.
+func LoadBidderConfig(dirPath string, isBidderAllowed func(string) bool) (*BidderConfig, error) {
+ files, err := os.ReadDir(dirPath)
+ if err != nil {
+ return nil, fmt.Errorf("error:[%s] dirPath:[%s]", err.Error(), dirPath)
+ }
+ bidderConfigMap := &BidderConfig{bidderConfigMap: make(map[string]*config)}
+ for _, file := range files {
+ bidderName, ok := strings.CutSuffix(file.Name(), ".json")
+ if !ok {
+ return nil, fmt.Errorf("error:[invalid_json_file_name] filename:[%s]", file.Name())
+ }
+ if !isBidderAllowed(bidderName) {
+ continue
+ }
+ requestParamsConfig, err := readFile(dirPath, file.Name())
+ if err != nil {
+ return nil, fmt.Errorf("error:[fail_to_read_file] dir:[%s] filename:[%s] err:[%s]", dirPath, file.Name(), err.Error())
+ }
+ requestParams, err := prepareRequestParams(bidderName, requestParamsConfig)
+ if err != nil {
+ return nil, err
+ }
+ bidderConfigMap.setRequestParams(bidderName, requestParams)
+ }
+ return bidderConfigMap, nil
+}
+
+// readFile reads the file from directory and unmarshals it into the map[string]any
+func readFile(dirPath, file string) (map[string]any, error) {
+ filePath := filepath.Join(dirPath, file)
+ content, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil, err
+ }
+ var contentMap map[string]any
+ err = json.Unmarshal(content, &contentMap)
+ return contentMap, err
+}
+
+// prepareRequestParams parse the requestParamsConfig and returns the requestParams
+func prepareRequestParams(bidderName string, requestParamsConfig map[string]any) (map[string]BidderParamMapper, error) {
+ params, found := requestParamsConfig[propertiesKey]
+ if !found {
+ return nil, nil
+ }
+ paramsMap, ok := params.(map[string]any)
+ if !ok {
+ return nil, fmt.Errorf("error:[invalid_json_file_content_malformed_properties] bidderName:[%s]", bidderName)
+ }
+ requestParams := make(map[string]BidderParamMapper, len(paramsMap))
+ for paramName, paramValue := range paramsMap {
+ paramValueMap, ok := paramValue.(map[string]any)
+ if !ok {
+ return nil, fmt.Errorf("error:[invalid_json_file_content] bidder:[%s] bidderParam:[%s]", bidderName, paramName)
+ }
+ location, found := paramValueMap[locationKey]
+ if !found {
+ continue
+ }
+ locationStr, ok := location.(string)
+ if !ok {
+ return nil, fmt.Errorf("error:[incorrect_location_in_bidderparam] bidder:[%s] bidderParam:[%s]", bidderName, paramName)
+ }
+ requestParams[paramName] = BidderParamMapper{
+ location: strings.Split(locationStr, "."),
+ }
+ }
+ return requestParams, nil
+}
diff --git a/adapters/ortbbidder/bidderparams/parser_test.go b/adapters/ortbbidder/bidderparams/parser_test.go
new file mode 100644
index 00000000000..54ae74deea0
--- /dev/null
+++ b/adapters/ortbbidder/bidderparams/parser_test.go
@@ -0,0 +1,365 @@
+package bidderparams
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPrepareRequestParams(t *testing.T) {
+ type args struct {
+ requestParamCfg map[string]any
+ bidderName string
+ }
+ type want struct {
+ requestParams map[string]BidderParamMapper
+ err error
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "properties_missing_from_fileContents",
+ args: args{
+ requestParamCfg: map[string]any{
+ "title": "test bidder parameters",
+ },
+ bidderName: "testbidder",
+ },
+ want: want{
+ requestParams: nil,
+ err: nil,
+ },
+ },
+ {
+ name: "properties_data_type_invalid",
+ args: args{
+ requestParamCfg: map[string]any{
+ "title": "test bidder parameters",
+ "properties": "type invalid",
+ },
+ bidderName: "testbidder",
+ },
+ want: want{
+ requestParams: nil,
+ err: fmt.Errorf("error:[invalid_json_file_content_malformed_properties] bidderName:[testbidder]"),
+ },
+ },
+ {
+ name: "bidder-params_data_type_invalid",
+ args: args{
+ requestParamCfg: map[string]any{
+ "title": "test bidder parameters",
+ "properties": map[string]any{
+ "adunitid": "invalid-type",
+ },
+ },
+ bidderName: "testbidder",
+ },
+ want: want{
+ requestParams: nil,
+ err: fmt.Errorf("error:[invalid_json_file_content] bidder:[testbidder] bidderParam:[adunitid]"),
+ },
+ },
+ {
+ name: "bidder-params_properties_is_not_provided",
+ args: args{
+ requestParamCfg: map[string]any{
+ "title": "test bidder parameters",
+ "properties": map[string]any{
+ "adunitid": map[string]any{
+ "type": "string",
+ },
+ },
+ },
+ bidderName: "testbidder",
+ },
+ want: want{
+ requestParams: map[string]BidderParamMapper{},
+ err: nil,
+ },
+ },
+ {
+ name: "bidder-params_location_is_not_in_string",
+ args: args{
+ requestParamCfg: map[string]any{
+ "title": "test bidder parameters",
+ "properties": map[string]any{
+ "adunitid": map[string]any{
+ "type": "string",
+ "location": 100,
+ },
+ },
+ },
+ bidderName: "testbidder",
+ },
+ want: want{
+ requestParams: nil,
+ err: fmt.Errorf("error:[incorrect_location_in_bidderparam] bidder:[testbidder] bidderParam:[adunitid]"),
+ },
+ },
+ {
+ name: "set_bidder-params_location_in_mapper",
+ args: args{
+ requestParamCfg: map[string]any{
+ "title": "test bidder parameters",
+ "properties": map[string]any{
+ "adunitid": map[string]any{
+ "type": "string",
+ "location": "app.adunitid",
+ },
+ },
+ },
+ bidderName: "testbidder",
+ },
+ want: want{
+ requestParams: map[string]BidderParamMapper{
+ "adunitid": {location: []string{"app", "adunitid"}},
+ },
+ err: nil,
+ },
+ },
+ {
+ name: "set_multiple_bidder-params_and_locations_in_mapper",
+ args: args{
+ requestParamCfg: map[string]any{
+ "title": "test bidder parameters",
+ "properties": map[string]any{
+ "adunitid": map[string]any{
+ "type": "string",
+ "location": "app.adunitid",
+ },
+ "slotname": map[string]any{
+ "type": "string",
+ "location": "ext.slot",
+ },
+ },
+ },
+ bidderName: "testbidder",
+ },
+ want: want{
+ requestParams: map[string]BidderParamMapper{
+ "adunitid": {location: []string{"app", "adunitid"}},
+ "slotname": {location: []string{"ext", "slot"}},
+ },
+ err: nil,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ requestParams, err := prepareRequestParams(tt.args.bidderName, tt.args.requestParamCfg)
+ assert.Equalf(t, tt.want.err, err, "updateBidderParamsMapper returned unexpected error")
+ assert.Equalf(t, tt.want.requestParams, requestParams, "updateBidderParamsMapper returned unexpected mapper")
+ })
+ }
+}
+
+func TestLoadBidderConfig(t *testing.T) {
+ type want struct {
+ biddersConfigMap *BidderConfig
+ err string
+ }
+ tests := []struct {
+ name string
+ want want
+ setup func() (string, error)
+ }{
+ {
+ name: "read_directory_fail",
+ want: want{
+ biddersConfigMap: nil,
+ err: "error:[open invalid-path: no such file or directory] dirPath:[invalid-path]",
+ },
+ setup: func() (string, error) { return "invalid-path", nil },
+ },
+ {
+ name: "found_file_without_.json_extension",
+ want: want{
+ biddersConfigMap: nil,
+ err: "error:[invalid_json_file_name] filename:[example.txt]",
+ },
+ setup: func() (string, error) {
+ dirPath := t.TempDir()
+ err := os.WriteFile(dirPath+"/example.txt", []byte("anything"), 0644)
+ return dirPath, err
+ },
+ },
+ {
+ name: "oRTB_bidder_not_found",
+ want: want{
+ biddersConfigMap: &BidderConfig{bidderConfigMap: make(map[string]*config)},
+ err: "",
+ },
+ setup: func() (string, error) {
+ dirPath := t.TempDir()
+ err := os.WriteFile(dirPath+"/example.json", []byte("anything"), 0644)
+ return dirPath, err
+ },
+ },
+ {
+ name: "oRTB_bidder_found_but_invalid_json_present",
+ want: want{
+ biddersConfigMap: nil,
+ err: "error:[fail_to_read_file]",
+ },
+ setup: func() (string, error) {
+ dirPath := t.TempDir()
+ err := os.WriteFile(dirPath+"/owortb_test.json", []byte("anything"), 0644)
+ return dirPath, err
+ },
+ },
+ {
+ name: "oRTB_bidder_found_but_bidder-params_are_absent",
+ want: want{
+ biddersConfigMap: &BidderConfig{bidderConfigMap: map[string]*config{
+ "owortb_test": {
+ requestParams: nil,
+ },
+ }},
+ err: "",
+ },
+ setup: func() (string, error) {
+ dirPath := t.TempDir()
+ err := os.WriteFile(dirPath+"/owortb_test.json", []byte("{}"), 0644)
+ return dirPath, err
+ },
+ },
+ {
+ name: "oRTB_bidder_found_but_prepareBidderRequestProperties_returns_error",
+ want: want{
+ biddersConfigMap: nil,
+ err: "error:[invalid_json_file_content_malformed_properties] bidderName:[owortb_test]",
+ },
+ setup: func() (string, error) {
+ dirPath := t.TempDir()
+ err := os.WriteFile(dirPath+"/owortb_test.json", []byte(`{"properties":"invalid-properties"}`), 0644)
+ return dirPath, err
+ },
+ },
+ {
+ name: "oRTB_bidder_found_and_valid_json_contents_present",
+ want: want{
+ biddersConfigMap: &BidderConfig{
+ bidderConfigMap: map[string]*config{
+ "owortb_test": {
+ requestParams: map[string]BidderParamMapper{
+ "adunitid": {location: []string{"app", "adunit", "id"}},
+ "slotname": {location: []string{"ext", "slotname"}},
+ },
+ },
+ }},
+ err: "",
+ },
+ setup: func() (string, error) {
+ dirPath := t.TempDir()
+ err := os.WriteFile(dirPath+"/owortb_test.json", []byte(`
+ {
+ "title":"ortb bidder",
+ "properties": {
+ "adunitid": {
+ "type": "string",
+ "location": "app.adunit.id"
+ },
+ "slotname": {
+ "type": "string",
+ "location": "ext.slotname"
+ }
+ }
+ }
+ `), 0644)
+ return dirPath, err
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ dirPath, err := tt.setup()
+ assert.NoError(t, err, "setup returned unexpected error")
+ got, err := LoadBidderConfig(dirPath, func(bidderName string) bool {
+ if strings.HasPrefix(bidderName, "owortb_") {
+ return true
+ }
+ return false
+ })
+ assert.Equal(t, tt.want.biddersConfigMap, got, "found incorrect mapper")
+ assert.Equal(t, len(tt.want.err) == 0, err == nil, "mismatched error")
+ if err != nil {
+ assert.ErrorContains(t, err, tt.want.err, "found incorrect error message")
+ }
+ })
+ }
+}
+
+func TestReadFile(t *testing.T) {
+ var setup = func() (string, error) {
+ dir := t.TempDir()
+ err := os.WriteFile(dir+"/owortb.json", []byte(`
+ {
+ "title":"ortb bidder",
+ "properties": {
+ "adunitid": {
+ "type": "string",
+ "location": "req.app.adunit.id"
+ }
+ }
+ }
+ `), 0644)
+ return dir, err
+ }
+ type args struct {
+ file string
+ }
+ type want struct {
+ err bool
+ node map[string]any
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "successful_readfile",
+ args: args{
+ file: "owortb.json",
+ },
+ want: want{
+ err: false,
+ node: map[string]any{
+ "title": "ortb bidder",
+ "properties": map[string]any{
+ "adunitid": map[string]any{
+ "type": "string",
+ "location": "req.app.adunit.id",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "fail_readfile",
+ args: args{
+ file: "invalid.json",
+ },
+ want: want{
+ err: true,
+ node: nil,
+ },
+ },
+ }
+ path, err := setup()
+ assert.Nil(t, err)
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := readFile(path, tt.args.file)
+ assert.Equal(t, tt.want.err, err != nil, "mismatched error")
+ assert.Equal(t, tt.want.node, got, "mismatched map[string]any")
+ })
+ }
+}
diff --git a/adapters/ortbbidder/ortbbidder.go b/adapters/ortbbidder/ortbbidder.go
new file mode 100644
index 00000000000..4cc3f349f8a
--- /dev/null
+++ b/adapters/ortbbidder/ortbbidder.go
@@ -0,0 +1,188 @@
+package ortbbidder
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v2/adapters"
+ "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams"
+ "github.com/prebid/prebid-server/v2/config"
+ "github.com/prebid/prebid-server/v2/openrtb_ext"
+)
+
+// adapter implements adapters.Bidder interface
+type adapter struct {
+ adapterInfo
+ bidderParamsConfig *bidderparams.BidderConfig
+}
+
+const (
+ RequestModeSingle string = "single"
+)
+
+// adapterInfo contains oRTB bidder specific info required in MakeRequests/MakeBids functions
+type adapterInfo struct {
+ config.Adapter
+ extraInfo extraAdapterInfo
+ bidderName openrtb_ext.BidderName
+}
+type extraAdapterInfo struct {
+ RequestMode string `json:"requestMode"`
+}
+
+// global instance to hold bidderParamsConfig
+var g_bidderParamsConfig *bidderparams.BidderConfig
+
+// InitBidderParamsConfig initializes a g_bidderParamsConfig instance from the files provided in dirPath.
+func InitBidderParamsConfig(dirPath string) (err error) {
+ g_bidderParamsConfig, err = bidderparams.LoadBidderConfig(dirPath, isORTBBidder)
+ return err
+}
+
+// makeRequest converts openrtb2.BidRequest to adapters.RequestData, sets requestParams in request if required
+func (o adapterInfo) makeRequest(request *openrtb2.BidRequest, requestParams map[string]bidderparams.BidderParamMapper) (*adapters.RequestData, error) {
+ if request == nil {
+ return nil, fmt.Errorf("found nil request")
+ }
+ requestBody, err := json.Marshal(request)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal request %s", err.Error())
+ }
+ requestBody, err = setRequestParams(requestBody, requestParams)
+ if err != nil {
+ return nil, err
+ }
+ return &adapters.RequestData{
+ Method: http.MethodPost,
+ Uri: o.Endpoint,
+ Body: requestBody,
+ Headers: http.Header{
+ "Content-Type": {"application/json;charset=utf-8"},
+ "Accept": {"application/json"},
+ },
+ }, nil
+}
+
+// Builder returns an instance of oRTB adapter
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+ extraAdapterInfo := extraAdapterInfo{}
+ if len(config.ExtraAdapterInfo) > 0 {
+ err := json.Unmarshal([]byte(config.ExtraAdapterInfo), &extraAdapterInfo)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to parse extra_info for bidder:[%s] err:[%s]", bidderName, err.Error())
+ }
+ }
+ return &adapter{
+ adapterInfo: adapterInfo{config, extraAdapterInfo, bidderName},
+ bidderParamsConfig: g_bidderParamsConfig,
+ }, nil
+}
+
+// MakeRequests prepares oRTB bidder-specific request information using which prebid server make call(s) to bidder.
+func (o *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+ if request == nil || requestInfo == nil {
+ return nil, []error{fmt.Errorf("Found either nil request or nil requestInfo")}
+ }
+ if o.bidderParamsConfig == nil {
+ return nil, []error{fmt.Errorf("Found nil bidderParamsConfig")}
+ }
+ var errs []error
+ adapterInfo := o.adapterInfo
+ requestParams, _ := o.bidderParamsConfig.GetRequestParams(o.bidderName.String())
+
+ // bidder request supports single impression in single HTTP call.
+ if adapterInfo.extraInfo.RequestMode == RequestModeSingle {
+ requestData := make([]*adapters.RequestData, 0, len(request.Imp))
+ requestCopy := *request
+ for _, imp := range request.Imp {
+ requestCopy.Imp = []openrtb2.Imp{imp} // requestCopy contains single impression
+ reqData, err := adapterInfo.makeRequest(&requestCopy, requestParams)
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+ requestData = append(requestData, reqData)
+ }
+ return requestData, errs
+ }
+ // bidder request supports multi impressions in single HTTP call.
+ requestData, err := adapterInfo.makeRequest(request, requestParams)
+ if err != nil {
+ return nil, []error{err}
+ }
+ return []*adapters.RequestData{requestData}, nil
+}
+
+// MakeBids prepares bidderResponse from the oRTB bidder server's http.Response
+func (o *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+ if responseData == nil || adapters.IsResponseStatusCodeNoContent(responseData) {
+ return nil, nil
+ }
+
+ if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil {
+ return nil, []error{err}
+ }
+
+ var response openrtb2.BidResponse
+ if err := json.Unmarshal(responseData.Body, &response); err != nil {
+ return nil, []error{err}
+ }
+
+ bidResponse := adapters.BidderResponse{
+ Bids: make([]*adapters.TypedBid, 0),
+ }
+ for _, seatBid := range response.SeatBid {
+ for bidInd, bid := range seatBid.Bid {
+ bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+ Bid: &seatBid.Bid[bidInd],
+ BidType: getMediaTypeForBid(bid),
+ })
+ }
+ }
+ return &bidResponse, nil
+}
+
+// getMediaTypeForBid returns the BidType as per the bid.MType field
+// bid.MType has high priority over bidExt.Prebid.Type
+func getMediaTypeForBid(bid openrtb2.Bid) openrtb_ext.BidType {
+ var bidType openrtb_ext.BidType
+ if bid.MType > 0 {
+ bidType = getMediaTypeForBidFromMType(bid.MType)
+ } else {
+ if bid.Ext != nil {
+ var bidExt openrtb_ext.ExtBid
+ err := json.Unmarshal(bid.Ext, &bidExt)
+ if err == nil && bidExt.Prebid != nil {
+ bidType, _ = openrtb_ext.ParseBidType(string(bidExt.Prebid.Type))
+ }
+ }
+ }
+ if bidType == "" {
+ // TODO : detect mediatype from bid.AdM and request.imp parameter
+ }
+ return bidType
+}
+
+// getMediaTypeForBidFromMType returns the bidType from the MarkupType field
+func getMediaTypeForBidFromMType(mtype openrtb2.MarkupType) openrtb_ext.BidType {
+ var bidType openrtb_ext.BidType
+ switch mtype {
+ case openrtb2.MarkupBanner:
+ bidType = openrtb_ext.BidTypeBanner
+ case openrtb2.MarkupVideo:
+ bidType = openrtb_ext.BidTypeVideo
+ case openrtb2.MarkupAudio:
+ bidType = openrtb_ext.BidTypeAudio
+ case openrtb2.MarkupNative:
+ bidType = openrtb_ext.BidTypeNative
+ }
+ return bidType
+}
+
+// isORTBBidder returns true if the bidder is an oRTB bidder
+func isORTBBidder(bidderName string) bool {
+ return strings.HasPrefix(bidderName, "owortb_")
+}
diff --git a/adapters/ortbbidder/ortbbidder_test.go b/adapters/ortbbidder/ortbbidder_test.go
new file mode 100644
index 00000000000..b951efc4086
--- /dev/null
+++ b/adapters/ortbbidder/ortbbidder_test.go
@@ -0,0 +1,578 @@
+package ortbbidder
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v2/adapters"
+ "github.com/prebid/prebid-server/v2/adapters/adapterstest"
+ "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams"
+ "github.com/prebid/prebid-server/v2/config"
+ "github.com/prebid/prebid-server/v2/errortypes"
+ "github.com/prebid/prebid-server/v2/openrtb_ext"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMakeRequests(t *testing.T) {
+ type args struct {
+ request *openrtb2.BidRequest
+ requestInfo *adapters.ExtraRequestInfo
+ adapterInfo adapterInfo
+ bidderCfgMap *bidderparams.BidderConfig
+ }
+ type want struct {
+ requestData []*adapters.RequestData
+ errors []error
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "request_is_nil",
+ args: args{
+ bidderCfgMap: &bidderparams.BidderConfig{},
+ },
+ want: want{
+ errors: []error{fmt.Errorf("Found either nil request or nil requestInfo")},
+ },
+ },
+ {
+ name: "requestInfo_is_nil",
+ args: args{
+ bidderCfgMap: &bidderparams.BidderConfig{},
+ },
+ want: want{
+ errors: []error{fmt.Errorf("Found either nil request or nil requestInfo")},
+ },
+ },
+ {
+ name: "multi_requestmode_to_form_requestdata",
+ args: args{
+ request: &openrtb2.BidRequest{
+ ID: "reqid",
+ Imp: []openrtb2.Imp{
+ {ID: "imp1", TagID: "tag1"},
+ {ID: "imp2", TagID: "tag2"},
+ },
+ },
+ requestInfo: &adapters.ExtraRequestInfo{
+ BidderCoreName: openrtb_ext.BidderName("ortb_test_multi_requestmode"),
+ },
+ adapterInfo: adapterInfo{config.Adapter{Endpoint: "http://test_bidder.com"}, extraAdapterInfo{RequestMode: ""}, "testbidder"},
+ bidderCfgMap: &bidderparams.BidderConfig{},
+ },
+ want: want{
+ requestData: []*adapters.RequestData{
+ {
+ Method: http.MethodPost,
+ Uri: "http://test_bidder.com",
+ Body: []byte(`{"id":"reqid","imp":[{"id":"imp1","tagid":"tag1"},{"id":"imp2","tagid":"tag2"}]}`),
+ Headers: http.Header{
+ "Content-Type": {"application/json;charset=utf-8"},
+ "Accept": {"application/json"},
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "single_requestmode_to_form_requestdata",
+ args: args{
+ request: &openrtb2.BidRequest{
+ ID: "reqid",
+ Imp: []openrtb2.Imp{
+ {ID: "imp1", TagID: "tag1"},
+ {ID: "imp2", TagID: "tag2"},
+ },
+ },
+ requestInfo: &adapters.ExtraRequestInfo{
+ BidderCoreName: openrtb_ext.BidderName("ortb_test_single_requestmode"),
+ },
+ adapterInfo: adapterInfo{config.Adapter{Endpoint: "http://test_bidder.com"}, extraAdapterInfo{RequestMode: "single"}, "testbidder"},
+ bidderCfgMap: &bidderparams.BidderConfig{},
+ },
+ want: want{
+ requestData: []*adapters.RequestData{
+ {
+ Method: http.MethodPost,
+ Uri: "http://test_bidder.com",
+ Body: []byte(`{"id":"reqid","imp":[{"id":"imp1","tagid":"tag1"}]}`),
+ Headers: http.Header{
+ "Content-Type": {"application/json;charset=utf-8"},
+ "Accept": {"application/json"},
+ },
+ },
+ {
+ Method: http.MethodPost,
+ Uri: "http://test_bidder.com",
+ Body: []byte(`{"id":"reqid","imp":[{"id":"imp2","tagid":"tag2"}]}`),
+ Headers: http.Header{
+ "Content-Type": {"application/json;charset=utf-8"},
+ "Accept": {"application/json"},
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "biddersConfigMap_is_nil",
+ args: args{
+ request: &openrtb2.BidRequest{
+ ID: "reqid",
+ Imp: []openrtb2.Imp{{ID: "imp1", TagID: "tag1"}},
+ },
+ requestInfo: &adapters.ExtraRequestInfo{
+ BidderCoreName: openrtb_ext.BidderName("ortb_test_single_requestmode"),
+ },
+ adapterInfo: adapterInfo{config.Adapter{Endpoint: "http://test_bidder.com"}, extraAdapterInfo{RequestMode: "single"}, "testbidder"},
+ bidderCfgMap: nil,
+ },
+ want: want{
+ errors: []error{fmt.Errorf("Found nil bidderParamsConfig")},
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ adapter := &adapter{adapterInfo: tt.args.adapterInfo, bidderParamsConfig: tt.args.bidderCfgMap}
+ requestData, errors := adapter.MakeRequests(tt.args.request, tt.args.requestInfo)
+ assert.Equalf(t, tt.want.requestData, requestData, "mismatched requestData")
+ assert.Equalf(t, tt.want.errors, errors, "mismatched errors")
+ })
+ }
+}
+
+func TestMakeBids(t *testing.T) {
+ type args struct {
+ request *openrtb2.BidRequest
+ requestData *adapters.RequestData
+ responseData *adapters.ResponseData
+ }
+ type want struct {
+ response *adapters.BidderResponse
+ errors []error
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "responseData_is_nil",
+ args: args{
+ responseData: nil,
+ },
+ want: want{
+ response: nil,
+ errors: nil,
+ },
+ },
+ {
+ name: "StatusNoContent_in_responseData",
+ args: args{
+ responseData: &adapters.ResponseData{StatusCode: http.StatusNoContent},
+ },
+ want: want{
+ response: nil,
+ errors: nil,
+ },
+ },
+ {
+ name: "StatusBadRequest_in_responseData",
+ args: args{
+ responseData: &adapters.ResponseData{StatusCode: http.StatusBadRequest},
+ },
+ want: want{
+ response: nil,
+ errors: []error{&errortypes.BadInput{
+ Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", http.StatusBadRequest),
+ }},
+ },
+ },
+ {
+ name: "valid_response",
+ args: args{
+ responseData: &adapters.ResponseData{
+ StatusCode: http.StatusOK,
+ Body: []byte(`{"id":"bid-resp-id","seatbid":[{"seat":"test_bidder","bid":[{"id":"bid-1","mtype":2}]}]}`),
+ },
+ },
+ want: want{
+ response: &adapters.BidderResponse{
+ Bids: []*adapters.TypedBid{
+ {
+ Bid: &openrtb2.Bid{
+ ID: "bid-1",
+ MType: 2,
+ },
+ BidType: "video",
+ },
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ adapter := &adapter{}
+ response, errs := adapter.MakeBids(tt.args.request, tt.args.requestData, tt.args.responseData)
+ assert.Equalf(t, tt.want.response, response, "mismatched response")
+ assert.Equalf(t, tt.want.errors, errs, "mismatched errors")
+ })
+ }
+}
+
+func TestGetMediaTypeForBid(t *testing.T) {
+ type args struct {
+ bid openrtb2.Bid
+ }
+ type want struct {
+ bidType openrtb_ext.BidType
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "valid_banner_bid",
+ args: args{
+ bid: openrtb2.Bid{ID: "1", MType: openrtb2.MarkupBanner},
+ },
+ want: want{
+ bidType: openrtb_ext.BidTypeBanner,
+ },
+ },
+ {
+ name: "valid_video_bid",
+ args: args{
+ bid: openrtb2.Bid{ID: "2", MType: openrtb2.MarkupVideo},
+ },
+ want: want{
+ bidType: openrtb_ext.BidTypeVideo,
+ },
+ },
+ {
+ name: "valid_audio_bid",
+ args: args{
+ bid: openrtb2.Bid{ID: "3", MType: openrtb2.MarkupAudio},
+ },
+ want: want{
+ bidType: openrtb_ext.BidTypeAudio,
+ },
+ },
+ {
+ name: "valid_native_bid",
+ args: args{
+ bid: openrtb2.Bid{ID: "4", MType: openrtb2.MarkupNative},
+ },
+ want: want{
+ bidType: openrtb_ext.BidTypeNative,
+ },
+ },
+ {
+ name: "invalid_bid_type",
+ args: args{
+ bid: openrtb2.Bid{ID: "5", MType: 123},
+ },
+ want: want{
+ bidType: "",
+ },
+ },
+ {
+ name: "bid.MType_has_high_priority",
+ args: args{
+ bid: openrtb2.Bid{ID: "5", MType: openrtb2.MarkupVideo, Ext: json.RawMessage(`{"prebid":{"type":"video"}}`)},
+ },
+ want: want{
+ bidType: "video",
+ },
+ },
+ {
+ name: "bid.ext.prebid.type_is_absent",
+ args: args{
+ bid: openrtb2.Bid{ID: "5", Ext: json.RawMessage(`{"prebid":{}}`)},
+ },
+ want: want{
+ bidType: "",
+ },
+ },
+ {
+ name: "bid.ext.prebid.type_json_unmarshal_fails",
+ args: args{
+ bid: openrtb2.Bid{ID: "5", Ext: json.RawMessage(`{"prebid":{invalid-json}}`)},
+ },
+ want: want{
+ bidType: "",
+ },
+ },
+ {
+ name: "bid.ext.prebid.type_is_valid",
+ args: args{
+ bid: openrtb2.Bid{ID: "5", Ext: json.RawMessage(`{"prebid":{"type":"banner"}}`)},
+ },
+ want: want{
+ bidType: "banner",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ bidType := getMediaTypeForBid(tt.args.bid)
+ assert.Equal(t, tt.want.bidType, bidType, "mismatched bidType")
+ })
+ }
+}
+
+func TestJsonSamplesForSingleRequestMode(t *testing.T) {
+ oldMapper := g_bidderParamsConfig
+ defer func() {
+ g_bidderParamsConfig = oldMapper
+ }()
+ g_bidderParamsConfig = &bidderparams.BidderConfig{}
+ bidder, buildErr := Builder("owgeneric_single_requestmode",
+ config.Adapter{
+ Endpoint: "http://test_bidder.com",
+ ExtraAdapterInfo: `{"requestMode":"single"}`,
+ }, config.Server{})
+ if buildErr != nil {
+ t.Fatalf("Builder returned unexpected error %v", buildErr)
+ }
+ adapterstest.RunJSONBidderTest(t, "ortbbiddertest/owortb_generic_single_requestmode", bidder)
+}
+
+func TestJsonSamplesForMultiRequestMode(t *testing.T) {
+ oldMapper := g_bidderParamsConfig
+ defer func() {
+ g_bidderParamsConfig = oldMapper
+ }()
+ g_bidderParamsConfig = &bidderparams.BidderConfig{}
+ bidder, buildErr := Builder("owgeneric_multi_requestmode",
+ config.Adapter{
+ Endpoint: "http://test_bidder.com",
+ ExtraAdapterInfo: ``,
+ }, config.Server{})
+ if buildErr != nil {
+ t.Fatalf("Builder returned unexpected error %v", buildErr)
+ }
+ adapterstest.RunJSONBidderTest(t, "ortbbiddertest/owortb_generic_multi_requestmode", bidder)
+}
+
+func Test_makeRequest(t *testing.T) {
+ type fields struct {
+ Adapter config.Adapter
+ }
+ type args struct {
+ request *openrtb2.BidRequest
+ requestParams map[string]bidderparams.BidderParamMapper
+ }
+ type want struct {
+ requestData *adapters.RequestData
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want want
+ }{
+ {
+ name: "valid_request",
+ fields: fields{
+ Adapter: config.Adapter{Endpoint: "https://example.com"},
+ },
+ args: args{
+ request: &openrtb2.BidRequest{
+ ID: "123",
+ Imp: []openrtb2.Imp{{ID: "imp1"}},
+ },
+ requestParams: make(map[string]bidderparams.BidderParamMapper),
+ },
+ want: want{
+ requestData: &adapters.RequestData{
+ Method: http.MethodPost,
+ Uri: "https://example.com",
+ Body: []byte(`{"id":"123","imp":[{"id":"imp1"}]}`),
+ Headers: http.Header{
+ "Content-Type": {"application/json;charset=utf-8"},
+ "Accept": {"application/json"},
+ },
+ },
+ err: nil,
+ },
+ },
+ {
+ name: "nil_request",
+ fields: fields{
+ Adapter: config.Adapter{Endpoint: "https://example.com"},
+ },
+ args: args{
+ request: nil,
+ requestParams: make(map[string]bidderparams.BidderParamMapper),
+ },
+ want: want{
+ requestData: nil,
+ err: fmt.Errorf("found nil request"),
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ o := adapterInfo{
+ Adapter: tt.fields.Adapter,
+ }
+ got, err := o.makeRequest(tt.args.request, tt.args.requestParams)
+ assert.Equal(t, tt.want.requestData, got, "mismatched requestData")
+ assert.Equal(t, tt.want.err, err, "mismatched error")
+ })
+ }
+}
+
+func TestBuilder(t *testing.T) {
+ InitBidderParamsConfig("../../static/bidder-params")
+ type args struct {
+ bidderName openrtb_ext.BidderName
+ config config.Adapter
+ server config.Server
+ }
+ type want struct {
+ err error
+ bidder adapters.Bidder
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "fails_to_parse_extra_info",
+ args: args{
+ bidderName: "ortbbidder",
+ config: config.Adapter{
+ ExtraAdapterInfo: "invalid-string",
+ },
+ server: config.Server{},
+ },
+ want: want{
+ bidder: nil,
+ err: fmt.Errorf("Failed to parse extra_info for bidder:[ortbbidder] err:[invalid character 'i' looking for beginning of value]"),
+ },
+ },
+ {
+ name: "bidder_with_requestMode",
+ args: args{
+ bidderName: "ortbbidder",
+ config: config.Adapter{
+ ExtraAdapterInfo: `{"requestMode":"single"}`,
+ },
+ server: config.Server{},
+ },
+ want: want{
+ bidder: &adapter{
+ adapterInfo: adapterInfo{
+ extraInfo: extraAdapterInfo{
+ RequestMode: "single",
+ },
+ Adapter: config.Adapter{
+ ExtraAdapterInfo: `{"requestMode":"single"}`,
+ },
+ bidderName: "ortbbidder",
+ },
+ bidderParamsConfig: g_bidderParamsConfig,
+ },
+ err: nil,
+ },
+ },
+ {
+ name: "bidder_without_requestMode",
+ args: args{
+ bidderName: "ortbbidder",
+ config: config.Adapter{
+ ExtraAdapterInfo: "",
+ },
+ server: config.Server{},
+ },
+ want: want{
+ bidder: &adapter{
+ adapterInfo: adapterInfo{
+ Adapter: config.Adapter{
+ ExtraAdapterInfo: ``,
+ },
+ bidderName: "ortbbidder",
+ },
+ bidderParamsConfig: g_bidderParamsConfig,
+ },
+ err: nil,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := Builder(tt.args.bidderName, tt.args.config, tt.args.server)
+ assert.Equal(t, tt.want.bidder, got, "mismatched bidder")
+ assert.Equal(t, tt.want.err, err, "mismatched error")
+ })
+ }
+}
+
+func TestInitBidderParamsConfig(t *testing.T) {
+ tests := []struct {
+ name string
+ dirPath string
+ wantErr bool
+ }{
+ {
+ name: "test_InitBiddersConfigMap_success",
+ dirPath: "../../static/bidder-params/",
+ wantErr: false,
+ },
+ {
+ name: "test_InitBiddersConfigMap_failure",
+ dirPath: "/invalid_directory/",
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := InitBidderParamsConfig(tt.dirPath)
+ assert.Equal(t, err != nil, tt.wantErr, "mismatched error")
+ })
+ }
+}
+
+func TestIsORTBBidder(t *testing.T) {
+ type args struct {
+ bidderName string
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ }{
+ {
+ name: "ortb_bidder",
+ args: args{
+ bidderName: "owortb_magnite",
+ },
+ want: true,
+ },
+ {
+ name: "non_ortb_bidder",
+ args: args{
+ bidderName: "magnite",
+ },
+ want: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := isORTBBidder(tt.args.bidderName)
+ assert.Equal(t, tt.want, got, "mismatched output of isORTBBidder")
+ })
+ }
+}
diff --git a/adapters/ortbbidder/ortbbiddertest/owortb_generic_multi_requestmode/exemplary/multi_imps_req.json b/adapters/ortbbidder/ortbbiddertest/owortb_generic_multi_requestmode/exemplary/multi_imps_req.json
new file mode 100644
index 00000000000..5bb92ef28c4
--- /dev/null
+++ b/adapters/ortbbidder/ortbbiddertest/owortb_generic_multi_requestmode/exemplary/multi_imps_req.json
@@ -0,0 +1,139 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id-1",
+ "video": {
+ "mimes": ["video/mp4"]
+ },
+ "tagid": "tagid_1",
+ "ext": {
+ "prebid": {
+ "bidder": {
+ "magnite": {
+ "bidder_param_1": "value_1"
+ }
+ }
+ }
+ }
+ },
+ {
+ "id": "test-imp-id-2",
+ "video": {
+ "mimes": ["video/mp4"]
+ },
+ "tagid": "tagid_2",
+ "ext": {
+ "prebid": {
+ "bidder": {
+ "magnite": {
+ "bidder_param_2": "value_2"
+ }
+ }
+ }
+ }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "http://test_bidder.com",
+ "body": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id-1",
+ "video": {
+ "mimes": ["video/mp4"]
+ },
+ "tagid": "tagid_1",
+ "ext": {
+ "prebid": {
+ "bidder": {
+ "magnite": {
+ "bidder_param_1": "value_1"
+ }
+ }
+ }
+ }
+ },
+ {
+ "id": "test-imp-id-2",
+ "video": {
+ "mimes": ["video/mp4"]
+ },
+ "tagid": "tagid_2",
+ "ext": {
+ "prebid": {
+ "bidder": {
+ "magnite": {
+ "bidder_param_2": "value_2"
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-request-id",
+ "seatbid": [
+ {
+ "seat": "magnite",
+ "bid": [{
+ "id": "bid-1",
+ "impid": "test-imp-id-1",
+ "price": 0.500000,
+ "adm": "some-test-ad",
+ "crid": "crid_10",
+ "mtype": 2
+ },{
+ "id": "bid-2",
+ "impid": "test-imp-id-2",
+ "price": 10,
+ "adm": "some-test-ad-2",
+ "crid": "crid_10",
+ "mtype": 2
+ }]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ }
+ ],
+
+ "expectedBidResponses": [
+ {
+ "bids": [
+ {
+ "bid": {
+ "id": "bid-1",
+ "impid": "test-imp-id-1",
+ "price": 0.5,
+ "adm": "some-test-ad",
+ "crid": "crid_10",
+ "mtype":2
+ },
+ "type": "video"
+ },
+ {
+ "bid": {
+ "id": "bid-2",
+ "impid": "test-imp-id-2",
+ "price": 10,
+ "adm": "some-test-ad-2",
+ "crid": "crid_10",
+ "mtype":2
+ },
+ "type": "video"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/ortbbidder/ortbbiddertest/owortb_generic_single_requestmode/exemplary/multi_imps_req.json b/adapters/ortbbidder/ortbbiddertest/owortb_generic_single_requestmode/exemplary/multi_imps_req.json
new file mode 100644
index 00000000000..efb1cb93bf1
--- /dev/null
+++ b/adapters/ortbbidder/ortbbiddertest/owortb_generic_single_requestmode/exemplary/multi_imps_req.json
@@ -0,0 +1,166 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id-1",
+ "video": {
+ "mimes": ["video/mp4"]
+ },
+ "tagid": "tagid_1",
+ "ext": {
+ "prebid": {
+ "bidder": {
+ "magnite": {
+ "bidder_param_1": "value_1"
+ }
+ }
+ }
+ }
+ },
+ {
+ "id": "test-imp-id-2",
+ "video": {
+ "mimes": ["video/mp4"]
+ },
+ "tagid": "tagid_2",
+ "ext": {
+ "prebid": {
+ "bidder": {
+ "magnite": {
+ "bidder_param_2": "value_2"
+ }
+ }
+ }
+ }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "http://test_bidder.com",
+ "body": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id-1",
+ "video": {
+ "mimes": ["video/mp4"]
+ },
+ "tagid": "tagid_1",
+ "ext": {
+ "prebid": {
+ "bidder": {
+ "magnite": {
+ "bidder_param_1": "value_1"
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-request-id",
+ "seatbid": [
+ {
+ "seat": "magnite",
+ "bid": [{
+ "id": "bid-1",
+ "impid": "test-imp-id-1",
+ "price": 0.500000,
+ "adm": "some-test-ad",
+ "crid": "crid_10",
+ "mtype": 2
+ }]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ },
+ {
+ "expectedRequest": {
+ "uri": "http://test_bidder.com",
+ "body": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id-2",
+ "video": {
+ "mimes": ["video/mp4"]
+ },
+ "tagid": "tagid_2",
+ "ext": {
+ "prebid": {
+ "bidder": {
+ "magnite": {
+ "bidder_param_2": "value_2"
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-request-id",
+ "seatbid": [
+ {
+ "seat": "magnite",
+ "bid": [{
+ "id": "bid-2",
+ "impid": "test-imp-id-2",
+ "price": 10,
+ "adm": "some-test-ad-2",
+ "crid": "crid_10",
+ "mtype": 2
+ }]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ }
+ ],
+
+ "expectedBidResponses": [
+ {
+ "bids": [
+ {
+ "bid": {
+ "id": "bid-1",
+ "impid": "test-imp-id-1",
+ "price": 0.5,
+ "adm": "some-test-ad",
+ "crid": "crid_10",
+ "mtype":2
+ },
+ "type": "video"
+ }
+ ]
+ },
+ {
+ "bids": [
+ {
+ "bid": {
+ "id": "bid-2",
+ "impid": "test-imp-id-2",
+ "price": 10,
+ "adm": "some-test-ad-2",
+ "crid": "crid_10",
+ "mtype":2
+ },
+ "type": "video"
+ }
+ ]
+ }
+ ]
+ }
diff --git a/adapters/ortbbidder/requestParamMapper.go b/adapters/ortbbidder/requestParamMapper.go
new file mode 100644
index 00000000000..013db98c7c9
--- /dev/null
+++ b/adapters/ortbbidder/requestParamMapper.go
@@ -0,0 +1,71 @@
+package ortbbidder
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams"
+)
+
+const (
+ impKey = "imp"
+ extKey = "ext"
+ bidderKey = "bidder"
+ appsiteKey = "appsite"
+ siteKey = "site"
+ appKey = "app"
+)
+
+// setRequestParams updates the requestBody based on the requestParams mapping details.
+func setRequestParams(requestBody []byte, requestParams map[string]bidderparams.BidderParamMapper) ([]byte, error) {
+ if len(requestParams) == 0 {
+ return requestBody, nil
+ }
+ request := map[string]any{}
+ err := json.Unmarshal(requestBody, &request)
+ if err != nil {
+ return nil, err
+ }
+ imps, ok := request[impKey].([]any)
+ if !ok {
+ return nil, fmt.Errorf("error:[invalid_imp_found_in_requestbody], imp:[%v]", request[impKey])
+ }
+ updatedRequest := false
+ for ind, imp := range imps {
+ request[impKey] = imp
+ imp, ok := imp.(map[string]any)
+ if !ok {
+ return nil, fmt.Errorf("error:[invalid_imp_found_in_implist], imp:[%v]", request[impKey])
+ }
+ ext, ok := imp[extKey].(map[string]any)
+ if !ok {
+ continue
+ }
+ bidderParams, ok := ext[bidderKey].(map[string]any)
+ if !ok {
+ continue
+ }
+ for paramName, paramValue := range bidderParams {
+ paramMapper, ok := requestParams[paramName]
+ if !ok {
+ continue
+ }
+ // set the value in the request according to the mapping details and remove the parameter.
+ if setValue(request, paramMapper.GetLocation(), paramValue) {
+ delete(bidderParams, paramName)
+ updatedRequest = true
+ }
+ }
+ imps[ind] = request[impKey]
+ }
+ // update the impression list in the request
+ request[impKey] = imps
+ // if the request was modified, marshal it back to JSON.
+ if updatedRequest {
+ requestBody, err = json.Marshal(request)
+ if err != nil {
+ return nil, fmt.Errorf("error:[fail_to_update_request_body] msg:[%s]", err.Error())
+ }
+ }
+ return requestBody, nil
+}
diff --git a/adapters/ortbbidder/requestParamMapper_test.go b/adapters/ortbbidder/requestParamMapper_test.go
new file mode 100644
index 00000000000..3aad8961e4b
--- /dev/null
+++ b/adapters/ortbbidder/requestParamMapper_test.go
@@ -0,0 +1,257 @@
+package ortbbidder
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/prebid/prebid-server/v2/adapters/ortbbidder/bidderparams"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSetRequestParams(t *testing.T) {
+ type args struct {
+ requestBody []byte
+ mapper map[string]bidderparams.BidderParamMapper
+ }
+ type want struct {
+ err string
+ requestBody []byte
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "empty_mapper",
+ args: args{
+ requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{}}}]}`),
+ },
+ want: want{
+ err: "",
+ requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{}}}]}`),
+ },
+ },
+ {
+ name: "nil_requestbody",
+ args: args{
+ requestBody: nil,
+ mapper: map[string]bidderparams.BidderParamMapper{
+ "adunit": {},
+ },
+ },
+ want: want{
+ err: "unexpected end of JSON input",
+ },
+ },
+ {
+ name: "requestbody_has_invalid_imps",
+ args: args{
+ requestBody: json.RawMessage(`{"imp":{"id":"1"}}`),
+ mapper: map[string]bidderparams.BidderParamMapper{
+ "adunit": {},
+ },
+ },
+ want: want{
+ err: "error:[invalid_imp_found_in_requestbody], imp:[map[id:1]]",
+ },
+ },
+ {
+ name: "missing_imp_ext",
+ args: args{
+ requestBody: json.RawMessage(`{"imp":[{}]}`),
+ mapper: map[string]bidderparams.BidderParamMapper{
+ "adunit": {},
+ },
+ },
+ want: want{
+ err: "",
+ requestBody: json.RawMessage(`{"imp":[{}]}`),
+ },
+ },
+ {
+ name: "missing_bidder_in_imp_ext",
+ args: args{
+ requestBody: json.RawMessage(`{"imp":[{"ext":{}}]}`),
+ mapper: map[string]bidderparams.BidderParamMapper{
+ "adunit": {},
+ },
+ },
+ want: want{
+ err: "",
+ requestBody: json.RawMessage(`{"imp":[{"ext":{}}]}`),
+ },
+ },
+ {
+ name: "missing_bidderparams_in_imp_ext",
+ args: args{
+ requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{}}}]}`),
+ mapper: map[string]bidderparams.BidderParamMapper{
+ "adunit": {},
+ },
+ },
+ want: want{
+ err: "",
+ requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{}}}]}`),
+ },
+ },
+ {
+ name: "mapper_not_contains_bidder_param_location",
+ args: args{
+ requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{"adunit":123}}}]}`),
+ mapper: func() map[string]bidderparams.BidderParamMapper {
+ bpm := bidderparams.BidderParamMapper{}
+ bpm.SetLocation([]string{"ext"})
+ return map[string]bidderparams.BidderParamMapper{
+ "slot": bpm,
+ }
+ }(),
+ },
+ want: want{
+ err: "",
+ requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{"adunit":123}}}]}`),
+ },
+ },
+ {
+ name: "mapper_contains_bidder_param_location",
+ args: args{
+ requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{"adunit":123}}}]}`),
+ mapper: func() map[string]bidderparams.BidderParamMapper {
+ bpm := bidderparams.BidderParamMapper{}
+ bpm.SetLocation([]string{"ext", "adunit"})
+ return map[string]bidderparams.BidderParamMapper{
+ "adunit": bpm,
+ }
+ }(),
+ },
+ want: want{
+ err: "",
+ requestBody: json.RawMessage(`{"ext":{"adunit":123},"imp":[{"ext":{"bidder":{}}}]}`),
+ },
+ },
+ {
+ name: "do_not_delete_bidder_param_if_failed_to_set_value",
+ args: args{
+ requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{"adunit":123}}}]}`),
+ mapper: func() map[string]bidderparams.BidderParamMapper {
+ bpm := bidderparams.BidderParamMapper{}
+ bpm.SetLocation([]string{"req", "", ""})
+ return map[string]bidderparams.BidderParamMapper{
+ "adunit": bpm,
+ }
+ }(),
+ },
+ want: want{
+ err: "",
+ requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{"adunit":123}}}]}`),
+ },
+ },
+ {
+ name: "set_multiple_bidder_params",
+ args: args{
+ requestBody: json.RawMessage(`{"app":{"name":"sampleapp"},"imp":[{"tagid":"oldtagid","ext":{"bidder":{"paramWithoutLocation":"value","adunit":123,"slot":"test_slot","wrapper":{"pubid":5890,"profile":1}}}}]}`),
+ mapper: func() map[string]bidderparams.BidderParamMapper {
+ adunit := bidderparams.BidderParamMapper{}
+ adunit.SetLocation([]string{"adunit", "id"})
+ slot := bidderparams.BidderParamMapper{}
+ slot.SetLocation([]string{"imp", "tagid"})
+ wrapper := bidderparams.BidderParamMapper{}
+ wrapper.SetLocation([]string{"app", "ext"})
+ return map[string]bidderparams.BidderParamMapper{
+ "adunit": adunit,
+ "slot": slot,
+ "wrapper": wrapper,
+ }
+ }(),
+ },
+ want: want{
+ err: "",
+ requestBody: json.RawMessage(`{"adunit":{"id":123},"app":{"ext":{"profile":1,"pubid":5890},"name":"sampleapp"},"imp":[{"ext":{"bidder":{"paramWithoutLocation":"value"}},"tagid":"test_slot"}]}`),
+ },
+ },
+ {
+ name: "conditional_mapping_set_app_object",
+ args: args{
+ requestBody: json.RawMessage(`{"app":{"name":"sampleapp"},"imp":[{"tagid":"oldtagid","ext":{"bidder":{"paramWithoutLocation":"value","adunit":123,"slot":"test_slot","wrapper":{"pubid":5890,"profile":1}}}}]}`),
+ mapper: func() map[string]bidderparams.BidderParamMapper {
+ bpm := bidderparams.BidderParamMapper{}
+ bpm.SetLocation([]string{"appsite", "wrapper"})
+ return map[string]bidderparams.BidderParamMapper{
+ "wrapper": bpm,
+ }
+ }(),
+ },
+ want: want{
+ err: "",
+ requestBody: json.RawMessage(`{"app":{"name":"sampleapp","wrapper":{"profile":1,"pubid":5890}},"imp":[{"ext":{"bidder":{"adunit":123,"paramWithoutLocation":"value","slot":"test_slot"}},"tagid":"oldtagid"}]}`),
+ },
+ },
+ {
+ name: "conditional_mapping_set_site_object",
+ args: args{
+ requestBody: json.RawMessage(`{"site":{"name":"sampleapp"},"imp":[{"tagid":"oldtagid","ext":{"bidder":{"paramWithoutLocation":"value","adunit":123,"slot":"test_slot","wrapper":{"pubid":5890,"profile":1}}}}]}`),
+ mapper: func() map[string]bidderparams.BidderParamMapper {
+ bpm := bidderparams.BidderParamMapper{}
+ bpm.SetLocation([]string{"appsite", "wrapper"})
+ return map[string]bidderparams.BidderParamMapper{
+ "wrapper": bpm,
+ }
+ }(),
+ },
+ want: want{
+ err: "",
+ requestBody: json.RawMessage(`{"imp":[{"ext":{"bidder":{"adunit":123,"paramWithoutLocation":"value","slot":"test_slot"}},"tagid":"oldtagid"}],"site":{"name":"sampleapp","wrapper":{"profile":1,"pubid":5890}}}`),
+ },
+ },
+ {
+ name: "multi_imps_bidder_params_mapping",
+ args: args{
+ requestBody: json.RawMessage(`{"app":{"name":"sampleapp"},"imp":[{"tagid":"tagid_1","ext":{"bidder":{"paramWithoutLocation":"value","adunit":111,"slot":"test_slot_1","wrapper":{"pubid":5890,"profile":1}}}},{"tagid":"tagid_2","ext":{"bidder":{"slot":"test_slot_2","adunit":222}}}]}`),
+ mapper: func() map[string]bidderparams.BidderParamMapper {
+ adunit := bidderparams.BidderParamMapper{}
+ adunit.SetLocation([]string{"adunit", "id"})
+ slot := bidderparams.BidderParamMapper{}
+ slot.SetLocation([]string{"imp", "tagid"})
+ wrapper := bidderparams.BidderParamMapper{}
+ wrapper.SetLocation([]string{"app", "ext"})
+ return map[string]bidderparams.BidderParamMapper{
+ "adunit": adunit,
+ "slot": slot,
+ "wrapper": wrapper,
+ }
+ }(),
+ },
+ want: want{
+ err: "",
+ requestBody: json.RawMessage(`{"adunit":{"id":222},"app":{"ext":{"profile":1,"pubid":5890},"name":"sampleapp"},"imp":[{"ext":{"bidder":{"paramWithoutLocation":"value"}},"tagid":"test_slot_1"},{"ext":{"bidder":{}},"tagid":"test_slot_2"}]}`),
+ },
+ },
+ {
+ name: "multi_imps_bidder_params_mapping_override_if_same_param_present",
+ args: args{
+ requestBody: json.RawMessage(`{"app":{"name":"sampleapp"},"imp":[{"tagid":"tagid_1","ext":{"bidder":{"paramWithoutLocation":"value","adunit":111}}},{"tagid":"tagid_2","ext":{"bidder":{"adunit":222}}}]}`),
+ mapper: func() map[string]bidderparams.BidderParamMapper {
+ bpm := bidderparams.BidderParamMapper{}
+ bpm.SetLocation([]string{"adunit", "id"})
+ return map[string]bidderparams.BidderParamMapper{
+ "adunit": bpm,
+ }
+ }(),
+ },
+ want: want{
+ err: "",
+ requestBody: json.RawMessage(`{"adunit":{"id":222},"app":{"name":"sampleapp"},"imp":[{"ext":{"bidder":{"paramWithoutLocation":"value"}},"tagid":"tagid_1"},{"ext":{"bidder":{}},"tagid":"tagid_2"}]}`),
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := setRequestParams(tt.args.requestBody, tt.args.mapper)
+ assert.Equal(t, string(tt.want.requestBody), string(got), "mismatched request body")
+ assert.Equal(t, len(tt.want.err) == 0, err == nil, "mismatched error")
+ if err != nil {
+ assert.Equal(t, err.Error(), tt.want.err, "mismatched error string")
+ }
+ })
+ }
+}
diff --git a/adapters/ortbbidder/util.go b/adapters/ortbbidder/util.go
new file mode 100644
index 00000000000..5b2dccc67bf
--- /dev/null
+++ b/adapters/ortbbidder/util.go
@@ -0,0 +1,63 @@
+package ortbbidder
+
+/*
+setValue updates or creates a value in a node based on a specified location.
+The location is a string that specifies a path through the node hierarchy,
+separated by dots ('.'). The value can be any type, and the function will
+create intermediate nodes as necessary if they do not exist.
+
+Arguments:
+- node: the root of the map in which to set the value
+- locations: slice of strings indicating the path to set the value.
+- value: The value to set at the specified location. Can be of any type.
+
+Example:
+ - location = imp.ext.adunitid; value = 123 ==> {"imp": {"ext" : {"adunitid":123}}}
+*/
+func setValue(node map[string]any, locations []string, value any) bool {
+ if value == nil || len(locations) == 0 {
+ return false
+ }
+
+ lastNodeIndex := len(locations) - 1
+ currentNode := node
+
+ for index, loc := range locations {
+ if len(loc) == 0 { // if location part is empty string
+ return false
+ }
+ if index == lastNodeIndex { // if it's the last part in location, set the value
+ currentNode[loc] = value
+ break
+ }
+ nextNode := getNode(currentNode, loc)
+ // not the last part, navigate deeper
+ if nextNode == nil {
+ // loc does not exist, set currentNode to a new node
+ newNode := make(map[string]any)
+ currentNode[loc] = newNode
+ currentNode = newNode
+ continue
+ }
+ // loc exists, set currentNode to nextNode
+ nextNodeTyped, ok := nextNode.(map[string]any)
+ if !ok {
+ return false
+ }
+ currentNode = nextNodeTyped
+ }
+ return true
+}
+
+// getNode retrieves the value for a given key from a map with special handling for the "appsite" key
+func getNode(nodes map[string]any, key string) any {
+ switch key {
+ case appsiteKey:
+ // if key is "appsite" and if nodes contains "site" object then return nodes["site"] else return nodes["app"]
+ if value, ok := nodes[siteKey]; ok {
+ return value
+ }
+ return nodes[appKey]
+ }
+ return nodes[key]
+}
diff --git a/adapters/ortbbidder/util_test.go b/adapters/ortbbidder/util_test.go
new file mode 100644
index 00000000000..059303f6dfb
--- /dev/null
+++ b/adapters/ortbbidder/util_test.go
@@ -0,0 +1,243 @@
+package ortbbidder
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSetValue(t *testing.T) {
+ type args struct {
+ node map[string]any
+ location []string
+ value any
+ }
+ type want struct {
+ node map[string]any
+ status bool
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "set_nil_value",
+ args: args{
+ node: map[string]any{},
+ location: []string{"key"},
+ value: nil,
+ },
+ want: want{
+ status: false,
+ node: map[string]any{},
+ },
+ },
+ {
+ name: "set_value_in_empty_location",
+ args: args{
+ node: map[string]any{},
+ location: []string{},
+ value: 123,
+ },
+ want: want{
+ status: false,
+ node: map[string]any{},
+ },
+ },
+ {
+ name: "set_value_in_invalid_location_modifies_node",
+ args: args{
+ node: map[string]any{},
+ location: []string{"key", ""},
+ value: 123,
+ },
+ want: want{
+ status: false,
+ node: map[string]any{
+ "key": map[string]any{},
+ },
+ },
+ },
+ {
+ name: "set_value_at_root_level_in_empty_node",
+ args: args{
+ node: map[string]any{},
+ location: []string{"key"},
+ value: 123,
+ },
+ want: want{
+ status: true,
+ node: map[string]any{"key": 123},
+ },
+ },
+ {
+ name: "set_value_at_root_level_in_non-empty_node",
+ args: args{
+ node: map[string]any{"oldKey": "oldValue"},
+ location: []string{"key"},
+ value: 123,
+ },
+ want: want{
+ status: true,
+ node: map[string]any{"oldKey": "oldValue", "key": 123},
+ },
+ },
+ {
+ name: "set_value_at_non-root_level_in_non-json_node",
+ args: args{
+ node: map[string]any{"rootKey": "rootValue"},
+ location: []string{"rootKey", "key"},
+ value: 123,
+ },
+ want: want{
+ status: false,
+ node: map[string]any{"rootKey": "rootValue"},
+ },
+ },
+ {
+ name: "set_value_at_non-root_level_in_json_node",
+ args: args{
+ node: map[string]any{"rootKey": map[string]any{
+ "oldKey": "oldValue",
+ }},
+ location: []string{"rootKey", "newKey"},
+ value: 123,
+ },
+ want: want{
+ status: true,
+ node: map[string]any{"rootKey": map[string]any{
+ "oldKey": "oldValue",
+ "newKey": 123,
+ }},
+ },
+ },
+ {
+ name: "set_value_at_non-root_level_in_nested-json_node",
+ args: args{
+ node: map[string]any{"rootKey": map[string]any{
+ "parentKey1": map[string]any{
+ "innerKey": "innerValue",
+ },
+ }},
+ location: []string{"rootKey", "parentKey2"},
+ value: "newKeyValue",
+ },
+ want: want{
+ status: true,
+ node: map[string]any{"rootKey": map[string]any{
+ "parentKey1": map[string]any{
+ "innerKey": "innerValue",
+ },
+ "parentKey2": "newKeyValue",
+ }},
+ },
+ },
+ {
+ name: "override_existing_key's_value",
+ args: args{
+ node: map[string]any{"rootKey": map[string]any{
+ "parentKey": map[string]any{
+ "innerKey": "innerValue",
+ },
+ }},
+ location: []string{"rootKey", "parentKey"},
+ value: "newKeyValue",
+ },
+ want: want{
+ status: true,
+ node: map[string]any{"rootKey": map[string]any{
+ "parentKey": "newKeyValue",
+ }},
+ },
+ },
+ {
+ name: "appsite_key_app_object_present",
+ args: args{
+ node: map[string]any{"app": map[string]any{
+ "parentKey": "oldValue",
+ }},
+ location: []string{"appsite", "parentKey"},
+ value: "newKeyValue",
+ },
+ want: want{
+ status: true,
+ node: map[string]any{"app": map[string]any{
+ "parentKey": "newKeyValue",
+ }},
+ },
+ },
+ {
+ name: "appsite_key_site_object_present",
+ args: args{
+ node: map[string]any{"site": map[string]any{
+ "parentKey": "oldValue",
+ }},
+ location: []string{"appsite", "parentKey"},
+ value: "newKeyValue",
+ },
+ want: want{
+ status: true,
+ node: map[string]any{"site": map[string]any{
+ "parentKey": "newKeyValue",
+ }},
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := setValue(tt.args.node, tt.args.location, tt.args.value)
+ assert.Equalf(t, tt.want.node, tt.args.node, "SetValue failed to update node object")
+ assert.Equalf(t, tt.want.status, got, "SetValue returned invalid status")
+ })
+ }
+}
+
+func TestGetNode(t *testing.T) {
+ type args struct {
+ nodes map[string]any
+ key string
+ }
+ tests := []struct {
+ name string
+ args args
+ want any
+ }{
+ {
+ name: "appsite_key_present_when_app_object_present",
+ args: args{
+ nodes: map[string]any{"app": map[string]any{
+ "parentKey": "oldValue",
+ }},
+ key: "appsite",
+ },
+ want: map[string]any{"parentKey": "oldValue"},
+ },
+ {
+ name: "appsite_key_present_when_site_object_present",
+ args: args{
+ nodes: map[string]any{"site": map[string]any{
+ "siteKey": "siteValue",
+ }},
+ key: "appsite",
+ },
+ want: map[string]any{"siteKey": "siteValue"},
+ },
+ {
+ name: "appsite_key_absent",
+ args: args{
+ nodes: map[string]any{"device": map[string]any{
+ "deviceKey": "deviceVal",
+ }},
+ key: "appsite",
+ },
+ want: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ node := getNode(tt.args.nodes, tt.args.key)
+ assert.Equal(t, tt.want, node)
+ })
+ }
+}
diff --git a/endpoints/openrtb2/ctv/adpod/adpod.go b/endpoints/openrtb2/ctv/adpod/adpod.go
new file mode 100644
index 00000000000..a05bf8f7b39
--- /dev/null
+++ b/endpoints/openrtb2/ctv/adpod/adpod.go
@@ -0,0 +1,46 @@
+package adpod
+
+import (
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/types"
+ "github.com/prebid/prebid-server/v2/metrics"
+ "github.com/prebid/prebid-server/v2/openrtb_ext"
+)
+
+type PodType int8
+
+const (
+ Structured PodType = 0
+ Dynamic PodType = 1
+ Hybrid PodType = 2
+ NotAdpod PodType = -1
+)
+
+type Adpod interface {
+ GetPodType() PodType
+ AddImpressions(imp openrtb2.Imp)
+ Validate() []error
+ GetImpressions() []openrtb2.Imp
+ CollectBid(bid *openrtb2.Bid, seat string)
+ HoldAuction()
+ GetAdpodSeatBids() []openrtb2.SeatBid
+ GetAdpodExtension(blockedVastTagID map[string]map[string][]string) *types.ImpData
+}
+
+type AdpodCtx struct {
+ PubId string
+ Type PodType
+ Imps []openrtb2.Imp
+ ReqAdpodExt *openrtb_ext.ExtRequestAdPod
+ Exclusion Exclusion
+ MetricsEngine metrics.MetricsEngine
+}
+
+type Exclusion struct {
+ AdvertiserDomainExclusion bool
+ IABCategoryExclusion bool
+}
+
+func (ex *Exclusion) shouldApplyExclusion() bool {
+ return ex.AdvertiserDomainExclusion || ex.IABCategoryExclusion
+}
diff --git a/endpoints/openrtb2/ctv/adpod/dynamic_adpod.go b/endpoints/openrtb2/ctv/adpod/dynamic_adpod.go
new file mode 100644
index 00000000000..97ee43e10d6
--- /dev/null
+++ b/endpoints/openrtb2/ctv/adpod/dynamic_adpod.go
@@ -0,0 +1,522 @@
+package adpod
+
+import (
+ "encoding/json"
+ "fmt"
+ "math"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/beevik/etree"
+ "github.com/buger/jsonparser"
+ "github.com/gofrs/uuid"
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v2/endpoints/events"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/combination"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/constant"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/impressions"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/response"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/types"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/util"
+ "github.com/prebid/prebid-server/v2/metrics"
+ "github.com/prebid/prebid-server/v2/openrtb_ext"
+ "github.com/prebid/prebid-server/v2/util/ptrutil"
+)
+
+type dynamicAdpod struct {
+ AdpodCtx
+ MinPodDuration int64 `json:"-"`
+ MaxPodDuration int64 `json:"-"`
+ MaxExtended int64 `json:"-"`
+ Imp openrtb2.Imp `json:"-"`
+ VideoExt *openrtb_ext.ExtVideoAdPod `json:"vidext,omitempty"`
+ ImpConfigs []*types.ImpAdPodConfig `json:"imp,omitempty"`
+ AdpodBid *types.AdPodBid `json:"-"`
+ WinningBids *types.AdPodBid `json:"-"`
+ Error *openrtb_ext.ExtBidderMessage `json:"ec,omitempty"`
+}
+
+func NewDynamicAdpod(pubId string, imp openrtb2.Imp, adpodExt openrtb_ext.ExtVideoAdPod, metricsEngine metrics.MetricsEngine, reqAdpodExt *openrtb_ext.ExtRequestAdPod) *dynamicAdpod {
+ //set Pod Duration
+ minPodDuration := imp.Video.MinDuration
+ maxPodDuration := imp.Video.MaxDuration
+
+ if adpodExt.AdPod == nil {
+ adpodExt = openrtb_ext.ExtVideoAdPod{
+ Offset: ptrutil.ToPtr(0),
+ AdPod: &openrtb_ext.VideoAdPod{
+ MinAds: ptrutil.ToPtr(1),
+ MaxAds: ptrutil.ToPtr(int(imp.Video.MaxSeq)),
+ MinDuration: ptrutil.ToPtr(int(imp.Video.MinDuration)),
+ MaxDuration: ptrutil.ToPtr(int(imp.Video.MaxDuration)),
+ AdvertiserExclusionPercent: ptrutil.ToPtr(100),
+ IABCategoryExclusionPercent: ptrutil.ToPtr(100),
+ },
+ }
+ maxPodDuration = imp.Video.PodDur
+ }
+
+ adpod := dynamicAdpod{
+ AdpodCtx: AdpodCtx{
+ PubId: pubId,
+ Type: Dynamic,
+ ReqAdpodExt: reqAdpodExt,
+ MetricsEngine: metricsEngine,
+ },
+ Imp: imp,
+ VideoExt: &adpodExt,
+ MinPodDuration: minPodDuration,
+ MaxPodDuration: maxPodDuration,
+ }
+
+ return &adpod
+}
+
+func (da *dynamicAdpod) GetPodType() PodType {
+ return da.Type
+}
+
+func (da *dynamicAdpod) AddImpressions(imp openrtb2.Imp) {
+ da.Imps = append(da.Imps, imp)
+}
+
+func (da *dynamicAdpod) GetImpressions() []openrtb2.Imp {
+ da.getAdPodImpConfigs()
+
+ // Generate Impressions based on configs
+ for i := range da.ImpConfigs {
+ da.AddImpressions(newImpression(da.Imp, da.ImpConfigs[i]))
+ }
+
+ return da.Imps
+}
+
+func (da *dynamicAdpod) CollectBid(bid *openrtb2.Bid, seat string) {
+ originalImpId, sequence := util.DecodeImpressionID(bid.ImpID)
+
+ if da.AdpodBid == nil {
+ da.AdpodBid = &types.AdPodBid{
+ Bids: make([]*types.Bid, 0),
+ OriginalImpID: originalImpId,
+ SeatName: string(openrtb_ext.BidderOWPrebidCTV),
+ }
+ }
+
+ ext := openrtb_ext.ExtBid{}
+ if bid.Ext != nil {
+ json.Unmarshal(bid.Ext, &ext)
+ }
+
+ //get duration of creative
+ duration, status := getBidDuration(bid, da.ReqAdpodExt, da.ImpConfigs, da.ImpConfigs[sequence-1].MaxDuration)
+
+ da.AdpodBid.Bids = append(da.AdpodBid.Bids, &types.Bid{
+ Bid: bid,
+ ExtBid: ext,
+ Status: status,
+ Duration: int(duration),
+ DealTierSatisfied: util.GetDealTierSatisfied(&ext),
+ Seat: seat,
+ })
+}
+
+func (da *dynamicAdpod) HoldAuction() {
+ if da.AdpodBid == nil || len(da.AdpodBid.Bids) == 0 {
+ return
+ }
+
+ // Check if we need sorting
+ // sort.Slice(da.AdpodBid.Bids, func(i, j int) bool { return da.AdpodBid.Bids[i].Price > da.AdpodBid.Bids[j].Price })
+
+ buckets := util.GetDurationWiseBidsBucket(da.AdpodBid.Bids)
+ if len(buckets) == 0 {
+ da.Error = util.DurationMismatchWarning
+ return
+ }
+
+ //combination generator
+ comb := combination.NewCombination(buckets, uint64(da.MinPodDuration), uint64(da.MaxPodDuration), da.VideoExt.AdPod)
+
+ //adpod generator
+ adpodGenerator := response.NewAdPodGenerator(buckets, comb, da.VideoExt.AdPod, da.MetricsEngine)
+
+ adpodBid := adpodGenerator.GetAdPodBids()
+ if adpodBid == nil {
+ da.Error = util.UnableToGenerateAdPodWarning
+ return
+ }
+ adpodBid.OriginalImpID = da.AdpodBid.OriginalImpID
+ adpodBid.SeatName = da.AdpodBid.SeatName
+
+ da.WinningBids = adpodBid
+
+}
+
+func (da *dynamicAdpod) Validate() []error {
+ var valdiationErrs []error
+
+ if da.VideoExt == nil {
+ return valdiationErrs
+ }
+
+ extErrs := da.VideoExt.Validate()
+ if len(extErrs) > 0 {
+ valdiationErrs = append(valdiationErrs, extErrs...)
+ }
+
+ durationErrs := da.VideoExt.AdPod.ValidateAdPodDurations(da.MinPodDuration, da.MaxPodDuration, da.MaxExtended)
+ if len(durationErrs) > 0 {
+ valdiationErrs = append(valdiationErrs, durationErrs...)
+ }
+
+ return valdiationErrs
+}
+
+func (da *dynamicAdpod) GetAdpodSeatBids() []openrtb2.SeatBid {
+ // Record Rejected bids
+ da.recordRejectedAdPodBids(da.PubId)
+
+ return da.getBidResponseSeatBids()
+}
+
+func (da *dynamicAdpod) GetAdpodExtension(blockedVastTagID map[string]map[string][]string) *types.ImpData {
+ da.setBidExtParams()
+
+ data := types.ImpData{
+ ImpID: da.Imp.ID,
+ Bid: da.AdpodBid,
+ VideoExt: da.VideoExt,
+ Config: da.ImpConfigs,
+ BlockedVASTTags: blockedVastTagID[da.Imp.ID],
+ Error: da.Error,
+ }
+
+ return &data
+}
+
+/***************************** Dynamic adpod processing method ************************************/
+
+// getAdPodImpsConfigs will return number of impressions configurations within adpod
+func (da *dynamicAdpod) getAdPodImpConfigs() {
+ // monitor
+ start := time.Now()
+ selectedAlgorithm := impressions.SelectAlgorithm(da.ReqAdpodExt)
+ impGen := impressions.NewImpressions(da.MinPodDuration, da.MaxPodDuration, da.ReqAdpodExt, da.VideoExt.AdPod, selectedAlgorithm)
+ impRanges := impGen.Get()
+ labels := metrics.PodLabels{AlgorithmName: impressions.MonitorKey[selectedAlgorithm], NoOfImpressions: new(int)}
+
+ //log number of impressions in stats
+ *labels.NoOfImpressions = len(impRanges)
+ da.MetricsEngine.RecordPodImpGenTime(labels, start)
+
+ // check if algorithm has generated impressions
+ if len(impRanges) == 0 {
+ da.Error = &openrtb_ext.ExtBidderMessage{
+ Code: util.UnableToGenerateImpressionsError.Code(),
+ Message: util.UnableToGenerateImpressionsError.Message,
+ }
+ return
+ }
+
+ config := make([]*types.ImpAdPodConfig, len(impRanges))
+ for i, value := range impRanges {
+ config[i] = &types.ImpAdPodConfig{
+ ImpID: util.GetCTVImpressionID(da.Imp.ID, i+1),
+ MinDuration: value[0],
+ MaxDuration: value[1],
+ SequenceNumber: int8(i + 1), /* Must be starting with 1 */
+ }
+ }
+
+ da.ImpConfigs = config
+}
+
+// newImpression will clone existing impression object and create video object with ImpAdPodConfig.
+func newImpression(imp openrtb2.Imp, config *types.ImpAdPodConfig) openrtb2.Imp {
+ video := *imp.Video
+ video.MinDuration = config.MinDuration
+ video.MaxDuration = config.MaxDuration
+ video.Sequence = config.SequenceNumber
+ video.MaxExtended = 0
+
+ video.Ext = jsonparser.Delete(video.Ext, "adpod")
+ video.Ext = jsonparser.Delete(video.Ext, "offset")
+ if string(video.Ext) == "{}" {
+ video.Ext = nil
+ }
+
+ newImp := imp
+ newImp.ID = config.ImpID
+ newImp.Video = &video
+ return newImp
+}
+
+/*
+getBidDuration determines the duration of video ad from given bid.
+it will try to get the actual ad duration returned by the bidder using prebid.video.duration
+if prebid.video.duration not present then uses defaultDuration passed as an argument
+if video lengths matching policy is present for request then it will validate and update duration based on policy
+*/
+func getBidDuration(bid *openrtb2.Bid, reqExt *openrtb_ext.ExtRequestAdPod, config []*types.ImpAdPodConfig, defaultDuration int64) (int64, constant.BidStatus) {
+
+ // C1: Read it from bid.ext.prebid.video.duration field
+ duration, err := jsonparser.GetInt(bid.Ext, "prebid", "video", "duration")
+ if nil != err || duration <= 0 {
+ // incase if duration is not present use impression duration directly as it is
+ return defaultDuration, constant.StatusOK
+ }
+
+ // C2: Based on video lengths matching policy validate and return duration
+ if nil != reqExt && len(reqExt.VideoAdDurationMatching) > 0 {
+ return getDurationBasedOnDurationMatchingPolicy(duration, reqExt.VideoAdDurationMatching, config)
+ }
+
+ //default return duration which is present in bid.ext.prebid.vide.duration field
+ return duration, constant.StatusOK
+}
+
+// getDurationBasedOnDurationMatchingPolicy will return duration based on durationmatching policy
+func getDurationBasedOnDurationMatchingPolicy(duration int64, policy openrtb_ext.OWVideoAdDurationMatchingPolicy, config []*types.ImpAdPodConfig) (int64, constant.BidStatus) {
+ switch policy {
+ case openrtb_ext.OWExactVideoAdDurationMatching:
+ tmp := util.GetNearestDuration(duration, config)
+ if tmp != duration {
+ return duration, constant.StatusDurationMismatch
+ }
+ //its and valid duration return it with StatusOK
+
+ case openrtb_ext.OWRoundupVideoAdDurationMatching:
+ tmp := util.GetNearestDuration(duration, config)
+ if tmp == -1 {
+ return duration, constant.StatusDurationMismatch
+ }
+ //update duration with nearest one duration
+ duration = tmp
+ //its and valid duration return it with StatusOK
+ }
+
+ return duration, constant.StatusOK
+}
+
+/***************************Bid Response Processing************************/
+
+func (da *dynamicAdpod) getBidResponseSeatBids() []openrtb2.SeatBid {
+ if da.WinningBids == nil || len(da.WinningBids.Bids) == 0 {
+ return nil
+ }
+
+ bid := da.getAdPodBid(da.WinningBids)
+ if bid == nil {
+ return nil
+ }
+
+ adpodSeat := openrtb2.SeatBid{
+ Seat: da.AdpodBid.SeatName,
+ }
+ adpodSeat.Bid = append(adpodSeat.Bid, *bid.Bid)
+
+ return []openrtb2.SeatBid{adpodSeat}
+}
+
+// getAdPodBid
+func (da *dynamicAdpod) getAdPodBid(adpod *types.AdPodBid) *types.Bid {
+ bid := types.Bid{
+ Bid: &openrtb2.Bid{},
+ }
+
+ //TODO: Write single for loop to get all details
+ bidID, err := uuid.NewV4()
+ if err == nil {
+ bid.ID = bidID.String()
+ } else {
+ bid.ID = adpod.Bids[0].ID
+ }
+
+ bid.ImpID = adpod.OriginalImpID
+ bid.Price = adpod.Price
+ bid.ADomain = adpod.ADomain[:]
+ bid.Cat = adpod.Cat[:]
+ bid.AdM = *getAdPodBidCreative(adpod, true)
+ bid.Ext = getAdPodBidExtension(adpod)
+ return &bid
+}
+
+// getAdPodBidCreative get commulative adpod bid details
+func getAdPodBidCreative(adpod *types.AdPodBid, generatedBidID bool) *string {
+ doc := etree.NewDocument()
+ vast := doc.CreateElement(constant.VASTElement)
+ sequenceNumber := 1
+ var version float64 = 2.0
+
+ for _, bid := range adpod.Bids {
+ var newAd *etree.Element
+
+ if strings.HasPrefix(bid.AdM, constant.HTTPPrefix) {
+ newAd = etree.NewElement(constant.VASTAdElement)
+ wrapper := newAd.CreateElement(constant.VASTWrapperElement)
+ vastAdTagURI := wrapper.CreateElement(constant.VASTAdTagURIElement)
+ vastAdTagURI.CreateCharData(bid.AdM)
+ } else {
+ adDoc := etree.NewDocument()
+ if err := adDoc.ReadFromString(bid.AdM); err != nil {
+ continue
+ }
+
+ if generatedBidID == false {
+ // adjust bidid in video event trackers and update
+ adjustBidIDInVideoEventTrackers(adDoc, bid.Bid)
+ adm, err := adDoc.WriteToString()
+ if nil != err {
+ util.JLogf("ERROR, %v", err.Error())
+ } else {
+ bid.AdM = adm
+ }
+ }
+
+ vastTag := adDoc.SelectElement(constant.VASTElement)
+ if vastTag == nil {
+ util.Logf("error:[vast_element_missing_in_adm] seat:[%s] adm:[%s]", bid.Seat, bid.AdM)
+ continue
+ }
+
+ //Get Actual VAST Version
+ bidVASTVersion, _ := strconv.ParseFloat(vastTag.SelectAttrValue(constant.VASTVersionAttribute, constant.VASTDefaultVersionStr), 64)
+ version = math.Max(version, bidVASTVersion)
+
+ ads := vastTag.SelectElements(constant.VASTAdElement)
+ if len(ads) > 0 {
+ newAd = ads[0].Copy()
+ }
+ }
+
+ if nil != newAd {
+ //creative.AdId attribute needs to be updated
+ newAd.CreateAttr(constant.VASTSequenceAttribute, fmt.Sprint(sequenceNumber))
+ vast.AddChild(newAd)
+ sequenceNumber++
+ }
+ }
+
+ if int(version) > len(constant.VASTVersionsStr) {
+ version = constant.VASTMaxVersion
+ }
+
+ vast.CreateAttr(constant.VASTVersionAttribute, constant.VASTVersionsStr[int(version)])
+ bidAdM, err := doc.WriteToString()
+ if err != nil {
+ fmt.Printf("ERROR, %v", err.Error())
+ return nil
+ }
+ return &bidAdM
+}
+
+func adjustBidIDInVideoEventTrackers(doc *etree.Document, bid *openrtb2.Bid) {
+ // adjusment: update bid.id with ctv module generated bid.id
+ creatives := events.FindCreatives(doc)
+ for _, creative := range creatives {
+ trackingEvents := creative.FindElements("TrackingEvents/Tracking")
+ if nil != trackingEvents {
+ // update bidid= value with ctv generated bid id for this bid
+ for _, trackingEvent := range trackingEvents {
+ u, e := url.Parse(trackingEvent.Text())
+ if nil == e {
+ values, e := url.ParseQuery(u.RawQuery)
+ // only do replacment if operId=8
+ if nil == e && nil != values["bidid"] && nil != values["operId"] && values["operId"][0] == "8" {
+ values.Set("bidid", bid.ID)
+ } else {
+ continue
+ }
+
+ //OTT-183: Fix
+ if nil != values["operId"] && values["operId"][0] == "8" {
+ operID := values.Get("operId")
+ values.Del("operId")
+ values.Add("_operId", operID) // _ (underscore) will keep it as first key
+ }
+
+ u.RawQuery = values.Encode() // encode sorts query params by key. _ must be first (assuing no other query param with _)
+ // replace _operId with operId
+ u.RawQuery = strings.ReplaceAll(u.RawQuery, "_operId", "operId")
+ trackingEvent.SetText(u.String())
+ }
+ }
+ }
+ }
+}
+
+// getAdPodBidExtension get commulative adpod bid details
+func getAdPodBidExtension(adpod *types.AdPodBid) json.RawMessage {
+ bidExt := &openrtb_ext.ExtOWBid{
+ ExtBid: openrtb_ext.ExtBid{
+ Prebid: &openrtb_ext.ExtBidPrebid{
+ Type: openrtb_ext.BidTypeVideo,
+ Video: &openrtb_ext.ExtBidPrebidVideo{},
+ },
+ },
+ AdPod: &openrtb_ext.BidAdPodExt{
+ RefBids: make([]string, len(adpod.Bids)),
+ },
+ }
+
+ for i, bid := range adpod.Bids {
+ //get unique bid id
+ bidID := bid.ID
+ if bid.ExtBid.Prebid != nil && bid.ExtBid.Prebid.BidId != "" {
+ bidID = bid.ExtBid.Prebid.BidId
+ }
+
+ //adding bid id in adpod.refbids
+ bidExt.AdPod.RefBids[i] = bidID
+
+ //updating exact duration of adpod creative
+ bidExt.Prebid.Video.Duration += int(bid.Duration)
+
+ //setting bid status as winning bid
+ bid.Status = constant.StatusWinningBid
+ }
+ rawExt, _ := json.Marshal(bidExt)
+ return rawExt
+}
+
+// recordRejectedAdPodBids records the bids lost in ad-pod auction using metricsEngine
+func (da *dynamicAdpod) recordRejectedAdPodBids(pubID string) {
+ if da.AdpodBid != nil && len(da.AdpodBid.Bids) > 0 {
+ for _, bid := range da.AdpodBid.Bids {
+ if bid.Status != constant.StatusWinningBid {
+ reason := ConvertAPRCToNBRC(bid.Status)
+ if reason == nil {
+ continue
+ }
+ rejReason := strconv.FormatInt(int64(*reason), 10)
+ da.MetricsEngine.RecordRejectedBids(pubID, bid.Seat, rejReason)
+ }
+ }
+ }
+
+}
+
+// setBidExtParams function sets the prebid.video.duration and adpod.aprc parameters
+func (da *dynamicAdpod) setBidExtParams() {
+ if da.AdpodBid != nil {
+ for _, bid := range da.AdpodBid.Bids {
+ //update adm
+ //bid.AdM = constant.VASTDefaultTag
+
+ //add duration value
+ raw, err := jsonparser.Set(bid.Ext, []byte(strconv.Itoa(int(bid.Duration))), "prebid", "video", "duration")
+ if nil == err {
+ bid.Ext = raw
+ }
+
+ //add bid filter reason value
+ raw, err = jsonparser.Set(bid.Ext, []byte(strconv.FormatInt(bid.Status, 10)), "adpod", "aprc")
+ if nil == err {
+ bid.Ext = raw
+ }
+ }
+ }
+
+}
diff --git a/endpoints/openrtb2/ctv/adpod/dynamic_adpod_test.go b/endpoints/openrtb2/ctv/adpod/dynamic_adpod_test.go
new file mode 100644
index 00000000000..445b79523ff
--- /dev/null
+++ b/endpoints/openrtb2/ctv/adpod/dynamic_adpod_test.go
@@ -0,0 +1,487 @@
+package adpod
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/constant"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/types"
+ "github.com/prebid/prebid-server/v2/metrics"
+ "github.com/prebid/prebid-server/v2/openrtb_ext"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+)
+
+func TestGetDurationBasedOnDurationMatchingPolicy(t *testing.T) {
+ type args struct {
+ duration int64
+ policy openrtb_ext.OWVideoAdDurationMatchingPolicy
+ config []*types.ImpAdPodConfig
+ }
+ type want struct {
+ duration int64
+ status constant.BidStatus
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "empty_duration_policy",
+ args: args{
+ duration: 10,
+ policy: "",
+ config: []*types.ImpAdPodConfig{
+ {MaxDuration: 10},
+ {MaxDuration: 20},
+ {MaxDuration: 30},
+ {MaxDuration: 40},
+ },
+ },
+ want: want{
+ duration: 10,
+ status: constant.StatusOK,
+ },
+ },
+ {
+ name: "policy_exact",
+ args: args{
+ duration: 10,
+ policy: openrtb_ext.OWExactVideoAdDurationMatching,
+ config: []*types.ImpAdPodConfig{
+ {MaxDuration: 10},
+ {MaxDuration: 20},
+ {MaxDuration: 30},
+ {MaxDuration: 40},
+ },
+ },
+ want: want{
+ duration: 10,
+ status: constant.StatusOK,
+ },
+ },
+ {
+ name: "policy_exact_didnot_match",
+ args: args{
+ duration: 15,
+ policy: openrtb_ext.OWExactVideoAdDurationMatching,
+ config: []*types.ImpAdPodConfig{
+ {MaxDuration: 10},
+ {MaxDuration: 20},
+ {MaxDuration: 30},
+ {MaxDuration: 40},
+ },
+ },
+ want: want{
+ duration: 15,
+ status: constant.StatusDurationMismatch,
+ },
+ },
+ {
+ name: "policy_roundup_exact",
+ args: args{
+ duration: 20,
+ policy: openrtb_ext.OWRoundupVideoAdDurationMatching,
+ config: []*types.ImpAdPodConfig{
+ {MaxDuration: 10},
+ {MaxDuration: 20},
+ {MaxDuration: 30},
+ {MaxDuration: 40},
+ },
+ },
+ want: want{
+ duration: 20,
+ status: constant.StatusOK,
+ },
+ },
+ {
+ name: "policy_roundup",
+ args: args{
+ duration: 25,
+ policy: openrtb_ext.OWRoundupVideoAdDurationMatching,
+ config: []*types.ImpAdPodConfig{
+ {MaxDuration: 10},
+ {MaxDuration: 20},
+ {MaxDuration: 30},
+ {MaxDuration: 40},
+ },
+ },
+ want: want{
+ duration: 30,
+ status: constant.StatusOK,
+ },
+ },
+ {
+ name: "policy_roundup_didnot_match",
+ args: args{
+ duration: 45,
+ policy: openrtb_ext.OWRoundupVideoAdDurationMatching,
+ config: []*types.ImpAdPodConfig{
+ {MaxDuration: 10},
+ {MaxDuration: 20},
+ {MaxDuration: 30},
+ {MaxDuration: 40},
+ },
+ },
+ want: want{
+ duration: 45,
+ status: constant.StatusDurationMismatch,
+ },
+ },
+
+ // TODO: Add test cases.
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ duration, status := getDurationBasedOnDurationMatchingPolicy(tt.args.duration, tt.args.policy, tt.args.config)
+ assert.Equal(t, tt.want.duration, duration)
+ assert.Equal(t, tt.want.status, status)
+ })
+ }
+}
+
+func TestGetBidDuration(t *testing.T) {
+ type args struct {
+ bid *openrtb2.Bid
+ reqExt *openrtb_ext.ExtRequestAdPod
+ config []*types.ImpAdPodConfig
+ defaultDuration int64
+ }
+ type want struct {
+ duration int64
+ status constant.BidStatus
+ }
+ var tests = []struct {
+ name string
+ args args
+ want want
+ expect int
+ }{
+ {
+ name: "nil_bid_ext",
+ args: args{
+ bid: &openrtb2.Bid{},
+ reqExt: nil,
+ config: nil,
+ defaultDuration: 100,
+ },
+ want: want{
+ duration: 100,
+ status: constant.StatusOK,
+ },
+ },
+ {
+ name: "use_default_duration",
+ args: args{
+ bid: &openrtb2.Bid{
+ Ext: json.RawMessage(`{"tmp":123}`),
+ },
+ reqExt: nil,
+ config: nil,
+ defaultDuration: 100,
+ },
+ want: want{
+ duration: 100,
+ status: constant.StatusOK,
+ },
+ },
+ {
+ name: "invalid_duration_in_bid_ext",
+ args: args{
+ bid: &openrtb2.Bid{
+ Ext: json.RawMessage(`{"prebid":{"video":{"duration":"invalid"}}}`),
+ },
+ reqExt: nil,
+ config: nil,
+ defaultDuration: 100,
+ },
+ want: want{
+ duration: 100,
+ status: constant.StatusOK,
+ },
+ },
+ {
+ name: "0sec_duration_in_bid_ext",
+ args: args{
+ bid: &openrtb2.Bid{
+ Ext: json.RawMessage(`{"prebid":{"video":{"duration":0}}}`),
+ },
+ reqExt: nil,
+ config: nil,
+ defaultDuration: 100,
+ },
+ want: want{
+ duration: 100,
+ status: constant.StatusOK,
+ },
+ },
+ {
+ name: "negative_duration_in_bid_ext",
+ args: args{
+ bid: &openrtb2.Bid{
+ Ext: json.RawMessage(`{"prebid":{"video":{"duration":-30}}}`),
+ },
+ reqExt: nil,
+ config: nil,
+ defaultDuration: 100,
+ },
+ want: want{
+ duration: 100,
+ status: constant.StatusOK,
+ },
+ },
+ {
+ name: "30sec_duration_in_bid_ext",
+ args: args{
+ bid: &openrtb2.Bid{
+ Ext: json.RawMessage(`{"prebid":{"video":{"duration":30}}}`),
+ },
+ reqExt: nil,
+ config: nil,
+ defaultDuration: 100,
+ },
+ want: want{
+ duration: 30,
+ status: constant.StatusOK,
+ },
+ },
+ {
+ name: "duration_matching_empty",
+ args: args{
+ bid: &openrtb2.Bid{
+ Ext: json.RawMessage(`{"prebid":{"video":{"duration":30}}}`),
+ },
+ reqExt: &openrtb_ext.ExtRequestAdPod{
+ VideoAdDurationMatching: "",
+ },
+ config: nil,
+ defaultDuration: 100,
+ },
+ want: want{
+ duration: 30,
+ status: constant.StatusOK,
+ },
+ },
+ {
+ name: "duration_matching_exact",
+ args: args{
+ bid: &openrtb2.Bid{
+ Ext: json.RawMessage(`{"prebid":{"video":{"duration":30}}}`),
+ },
+ reqExt: &openrtb_ext.ExtRequestAdPod{
+ VideoAdDurationMatching: openrtb_ext.OWExactVideoAdDurationMatching,
+ },
+ config: []*types.ImpAdPodConfig{
+ {MaxDuration: 10},
+ {MaxDuration: 20},
+ {MaxDuration: 30},
+ {MaxDuration: 40},
+ },
+ defaultDuration: 100,
+ },
+ want: want{
+ duration: 30,
+ status: constant.StatusOK,
+ },
+ },
+ {
+ name: "duration_matching_exact_not_present",
+ args: args{
+ bid: &openrtb2.Bid{
+ Ext: json.RawMessage(`{"prebid":{"video":{"duration":35}}}`),
+ },
+ reqExt: &openrtb_ext.ExtRequestAdPod{
+ VideoAdDurationMatching: openrtb_ext.OWExactVideoAdDurationMatching,
+ },
+ config: []*types.ImpAdPodConfig{
+ {MaxDuration: 10},
+ {MaxDuration: 20},
+ {MaxDuration: 30},
+ {MaxDuration: 40},
+ },
+ defaultDuration: 100,
+ },
+ want: want{
+ duration: 35,
+ status: constant.StatusDurationMismatch,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ duration, status := getBidDuration(tt.args.bid, tt.args.reqExt, tt.args.config, tt.args.defaultDuration)
+ assert.Equal(t, tt.want.duration, duration)
+ assert.Equal(t, tt.want.status, status)
+ })
+ }
+}
+
+func TestRecordAdPodRejectedBids(t *testing.T) {
+ type args struct {
+ bids types.AdPodBid
+ }
+
+ type want struct {
+ expectedCalls int
+ }
+
+ tests := []struct {
+ description string
+ args args
+ want want
+ }{
+ {
+ description: "multiple rejected bids",
+ args: args{
+ bids: types.AdPodBid{
+ Bids: []*types.Bid{
+ {
+ Bid: &openrtb2.Bid{},
+ Status: constant.StatusCategoryExclusion,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{},
+ Status: constant.StatusWinningBid,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{},
+ Status: constant.StatusOK,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{},
+ Status: 100,
+ Seat: "pubmatic",
+ },
+ },
+ },
+ },
+ want: want{
+ expectedCalls: 2,
+ },
+ },
+ }
+
+ for _, test := range tests {
+ me := &metrics.MetricsEngineMock{}
+ me.On("RecordRejectedBids", mock.Anything, mock.Anything, mock.Anything).Return()
+
+ deps := dynamicAdpod{
+ AdpodCtx: AdpodCtx{
+ MetricsEngine: me,
+ },
+ AdpodBid: &test.args.bids,
+ }
+ deps.recordRejectedAdPodBids("pub_001")
+ me.AssertNumberOfCalls(t, "RecordRejectedBids", test.want.expectedCalls)
+ }
+}
+
+func TestSetBidExtParams(t *testing.T) {
+ type args struct {
+ adpodBid *types.AdPodBid
+ }
+ type want struct {
+ adpodBid *types.AdPodBid
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "sample",
+ args: args{
+ adpodBid: &types.AdPodBid{
+ Bids: []*types.Bid{
+ {
+ Bid: &openrtb2.Bid{
+ Ext: json.RawMessage(`{"prebid": {"video": {} },"adpod": {}}`),
+ },
+ Duration: 10,
+ Status: 1,
+ },
+ },
+ },
+ },
+ want: want{
+ adpodBid: &types.AdPodBid{
+ Bids: []*types.Bid{
+ {
+ Bid: &openrtb2.Bid{
+ Ext: json.RawMessage(`{"prebid": {"video": {"duration":10} },"adpod": {"aprc":1}}`),
+ },
+ Duration: 10,
+ Status: 1,
+ },
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ adpod := dynamicAdpod{
+ AdpodBid: tt.args.adpodBid,
+ }
+
+ adpod.setBidExtParams()
+ assert.Equal(t, tt.want.adpodBid.Bids[0].Ext, adpod.AdpodBid.Bids[0].Ext)
+ })
+ }
+}
+
+func TestGetAdPodBidCreative(t *testing.T) {
+ type args struct {
+ adpod *types.AdPodBid
+ generatedBidID bool
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "VAST_element_missing_in_adm",
+ args: args{
+ adpod: &types.AdPodBid{
+ Bids: []*types.Bid{
+ {
+ Bid: &openrtb2.Bid{
+ AdM: "any_creative_without_vast",
+ },
+ },
+ },
+ },
+ generatedBidID: false,
+ },
+ want: "",
+ },
+ {
+ name: "VAST_element_present_in_adm",
+ args: args{
+ adpod: &types.AdPodBid{
+ Bids: []*types.Bid{
+ {
+ Bid: &openrtb2.Bid{
+ AdM: "url_creative",
+ },
+ },
+ },
+ },
+ generatedBidID: false,
+ },
+ want: "",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := getAdPodBidCreative(tt.args.adpod, tt.args.generatedBidID)
+ assert.Equalf(t, tt.want, *got, "found incorrect creative")
+ })
+ }
+}
diff --git a/endpoints/openrtb2/ctv/adpod/structured_adpod.go b/endpoints/openrtb2/ctv/adpod/structured_adpod.go
new file mode 100644
index 00000000000..1c4cf16e735
--- /dev/null
+++ b/endpoints/openrtb2/ctv/adpod/structured_adpod.go
@@ -0,0 +1,379 @@
+package adpod
+
+import (
+ "encoding/json"
+
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/types"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/util"
+ "github.com/prebid/prebid-server/v2/metrics"
+ "github.com/prebid/prebid-server/v2/openrtb_ext"
+)
+
+type structuredAdpod struct {
+ AdpodCtx
+ ImpBidMap map[string][]*types.Bid
+ WinningBid map[string]types.Bid
+ SelectedCategories map[string]bool
+ SelectedDomains map[string]bool
+}
+
+type Slot struct {
+ ImpId string
+ Index int
+ TotalBids int
+}
+
+func NewStructuredAdpod(pubId string, metricsEngine metrics.MetricsEngine, reqAdpodExt *openrtb_ext.ExtRequestAdPod) *structuredAdpod {
+ adpod := structuredAdpod{
+ AdpodCtx: AdpodCtx{
+ PubId: pubId,
+ Type: Structured,
+ ReqAdpodExt: reqAdpodExt,
+ MetricsEngine: metricsEngine,
+ },
+ ImpBidMap: make(map[string][]*types.Bid),
+ WinningBid: make(map[string]types.Bid),
+ }
+
+ return &adpod
+}
+
+func (da *structuredAdpod) GetPodType() PodType {
+ return da.Type
+}
+
+func (sa *structuredAdpod) AddImpressions(imp openrtb2.Imp) {
+ sa.Imps = append(sa.Imps, imp)
+}
+
+func (sa *structuredAdpod) GetImpressions() []openrtb2.Imp {
+ return sa.Imps
+}
+
+func (sa *structuredAdpod) CollectBid(bid *openrtb2.Bid, seat string) {
+ ext := openrtb_ext.ExtBid{}
+ if bid.Ext != nil {
+ json.Unmarshal(bid.Ext, &ext)
+ }
+
+ adpodBid := types.Bid{
+ Bid: bid,
+ ExtBid: ext,
+ DealTierSatisfied: util.GetDealTierSatisfied(&ext),
+ Seat: seat,
+ }
+ bids := sa.ImpBidMap[bid.ImpID]
+
+ bids = append(bids, &adpodBid)
+ sa.ImpBidMap[bid.ImpID] = bids
+}
+
+func (sa *structuredAdpod) HoldAuction() {
+ if len(sa.ImpBidMap) == 0 {
+ return
+ }
+
+ // Sort Bids impression wise
+ for _, bids := range sa.ImpBidMap {
+ util.SortBids(bids)
+ }
+
+ // Create Slots
+ slots := make([]Slot, 0)
+
+ for impId, bids := range sa.ImpBidMap {
+ slot := Slot{
+ ImpId: impId,
+ Index: 0,
+ TotalBids: len(bids),
+ }
+ slots = append(slots, slot)
+ }
+
+ sa.selectBidForSlot(slots)
+
+ // Select Winning bids
+ for i := range slots {
+ bids := sa.ImpBidMap[slots[i].ImpId]
+ // Add validations on len of array and index chosen
+ sa.WinningBid[slots[i].ImpId] = *bids[slots[i].Index]
+ }
+
+}
+
+func (sa *structuredAdpod) Validate() []error {
+ return nil
+}
+
+func (sa *structuredAdpod) GetAdpodSeatBids() []openrtb2.SeatBid {
+ if len(sa.WinningBid) == 0 {
+ return nil
+ }
+
+ var seatBid []openrtb2.SeatBid
+ for _, bid := range sa.WinningBid {
+ adpodSeat := openrtb2.SeatBid{
+ Bid: []openrtb2.Bid{*bid.Bid},
+ Seat: bid.Seat,
+ }
+ seatBid = append(seatBid, adpodSeat)
+ }
+
+ return seatBid
+}
+
+func (sa *structuredAdpod) GetAdpodExtension(blockedVastTagID map[string]map[string][]string) *types.ImpData {
+ return nil
+}
+
+/****************************Exclusion*******************************/
+
+func (sa *structuredAdpod) addCategories(categories []string) {
+ if sa.SelectedCategories == nil {
+ sa.SelectedCategories = make(map[string]bool)
+ }
+
+ for _, cat := range categories {
+ sa.SelectedCategories[cat] = true
+ }
+}
+
+func (sa *structuredAdpod) addDomains(domains []string) {
+ if sa.SelectedDomains == nil {
+ sa.SelectedDomains = make(map[string]bool)
+ }
+
+ for _, domain := range domains {
+ sa.SelectedDomains[domain] = true
+ }
+}
+
+func (sa *structuredAdpod) isCategoryAlreadySelected(bid *types.Bid) bool {
+ if bid == nil || bid.Cat == nil {
+ return false
+ }
+
+ if sa.SelectedCategories == nil {
+ return false
+ }
+
+ for i := range bid.Cat {
+ if _, ok := sa.SelectedCategories[bid.Cat[i]]; ok {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (sa *structuredAdpod) isDomainAlreadySelected(bid *types.Bid) bool {
+ if bid == nil || bid.ADomain == nil {
+ return false
+ }
+
+ if sa.SelectedDomains == nil {
+ return false
+ }
+
+ for i := range bid.ADomain {
+ if _, ok := sa.SelectedDomains[bid.ADomain[i]]; ok {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (sa *structuredAdpod) isCatOverlap(cats []string, catMap map[string]bool) bool {
+ if !sa.Exclusion.IABCategoryExclusion {
+ return false
+ }
+
+ return isAtrributesOverlap(cats, catMap)
+}
+
+func (sa *structuredAdpod) isDomainOverlap(domains []string, domainMap map[string]bool) bool {
+ if !sa.Exclusion.AdvertiserDomainExclusion {
+ return false
+ }
+
+ return isAtrributesOverlap(domains, domainMap)
+}
+
+func isAtrributesOverlap(attributes []string, checkMap map[string]bool) bool {
+ for _, item := range attributes {
+ if _, ok := checkMap[item]; ok {
+ return true
+ }
+ }
+ return false
+}
+
+/*******************Structured Adpod Auction Methods***********************/
+
+func isDealBid(bid *types.Bid) bool {
+ return bid.DealTierSatisfied
+}
+
+func (sa *structuredAdpod) isOverlap(bid *types.Bid, catMap map[string]bool, domainMap map[string]bool) bool {
+ return sa.isCatOverlap(bid.Cat, catMap) || sa.isDomainOverlap(bid.ADomain, domainMap)
+}
+
+func (sa *structuredAdpod) selectBidForSlot(slots []Slot) {
+ if len(slots) == 0 {
+ return
+ }
+
+ slotIndex := sa.getSlotIndexWithHighestBid(slots)
+
+ // Get current bid for selected slot
+ selectedSlot := slots[slotIndex]
+ slotBids := sa.ImpBidMap[selectedSlot.ImpId]
+ selectedBid := slotBids[selectedSlot.Index]
+
+ if sa.Exclusion.shouldApplyExclusion() {
+ if bidIndex, ok := sa.isBetterBidThanDeal(slots, slotIndex, selectedSlot, selectedBid); ok {
+ selectedSlot.Index = bidIndex
+ slots[slotIndex] = selectedSlot
+ } else if sa.isCategoryAlreadySelected(selectedBid) || sa.isDomainAlreadySelected(selectedBid) {
+ // Get bid for current slot for which category is not overlapping
+ for i := selectedSlot.Index + 1; i < len(slotBids); i++ {
+ if !sa.isCategoryAlreadySelected(slotBids[i]) && !sa.isDomainAlreadySelected(slotBids[i]) {
+ selectedSlot.Index = i
+ break
+ }
+ }
+
+ // Update selected Slot in slots array
+ slots[slotIndex] = selectedSlot
+ }
+ }
+
+ // Add bid categories to selected categories
+ sa.addCategories(slotBids[selectedSlot.Index].Cat)
+ sa.addDomains(slotBids[selectedSlot.Index].ADomain)
+
+ // Swap selected slot at initial position
+ slots[0], slots[slotIndex] = slots[slotIndex], slots[0]
+
+ sa.selectBidForSlot(slots[1:])
+}
+
+func (sa *structuredAdpod) getSlotIndexWithHighestBid(slots []Slot) int {
+ var index int
+ maxBid := &types.Bid{
+ Bid: &openrtb2.Bid{},
+ }
+
+ for i := range slots {
+ impBids := sa.ImpBidMap[slots[i].ImpId]
+ bid := impBids[slots[i].Index]
+
+ if bid.DealTierSatisfied == maxBid.DealTierSatisfied {
+ if bid.Price > maxBid.Price {
+ maxBid = bid
+ index = i
+ }
+ } else if bid.DealTierSatisfied {
+ maxBid = bid
+ index = i
+ }
+ }
+
+ return index
+}
+
+// isBetterBidAvailable checks if a better bid is available for the selected slot.
+// It returns true if
+func (sa *structuredAdpod) isBetterBidAvailable(slots []Slot, selectedSlotIndex int, selectedBid *types.Bid) bool {
+ if len(selectedBid.Cat) == 0 && len(selectedBid.ADomain) == 0 {
+ return false
+ }
+
+ catMap := createMapFromSlice(selectedBid.Cat)
+ domainMap := createMapFromSlice(selectedBid.ADomain)
+
+ return sa.shouldUpdateSelectedBid(slots, selectedSlotIndex, catMap, domainMap)
+}
+
+// shouldUpdateSelectedBid checks if a bid should be updated for a selected slot.
+func (sa *structuredAdpod) shouldUpdateSelectedBid(slots []Slot, selectedSlotIndex int, catMap map[string]bool, domainMap map[string]bool) bool {
+ for i := range slots {
+ if selectedSlotIndex == i {
+ continue
+ }
+ slotBids := sa.ImpBidMap[slots[i].ImpId]
+ slotIndex := slots[i].Index
+
+ // Get bid for current slot
+ bid := slotBids[slotIndex]
+
+ if bid.DealTierSatisfied && sa.isOverlap(bid, catMap, domainMap) {
+ return sa.shouldUpdateBid(slotBids, slotIndex, catMap, domainMap)
+ }
+ }
+ return false
+}
+
+// shouldUpdateBid checks if a bid should be updated for a selected slot.
+// It iterates through the remaining slot bids of overlapped slot starting from the given slot index,
+// and checks exclusions conditions for only deal bids.
+// It will ensure more deal bids in final adpod.
+func (sa *structuredAdpod) shouldUpdateBid(slotBids []*types.Bid, slotIndex int, catMap map[string]bool, domainMap map[string]bool) bool {
+ for i := slotIndex + 1; i < len(slotBids); i++ {
+ bid := slotBids[i]
+
+ if !bid.DealTierSatisfied {
+ break
+ }
+
+ if !sa.isOverlap(bid, catMap, domainMap) {
+ return false
+ }
+ }
+ return true
+}
+
+func (sa *structuredAdpod) getBetterBid(slotBids []*types.Bid, selectedBid *types.Bid, selectedBidtIndex int) (int, bool) {
+ catMap := createMapFromSlice(selectedBid.Cat)
+ domainMap := createMapFromSlice(selectedBid.ADomain)
+
+ for i := selectedBidtIndex + 1; i < len(slotBids); i++ {
+ bid := slotBids[i]
+
+ // Check for deal bid and select if exclusion conditions are satisfied
+ if isDealBid(bid) {
+ if !sa.isOverlap(bid, catMap, domainMap) {
+ return i, true
+ }
+ continue
+ }
+
+ // New selected bid exclusion parameters should not be overlaped
+ if sa.isOverlap(bid, catMap, domainMap) {
+ continue
+ }
+
+ // Check for bid price is greater than deal price
+ if bid.Price > selectedBid.Price {
+ return i, true
+ }
+ }
+
+ return selectedBidtIndex, false
+}
+
+func (sa *structuredAdpod) isBetterBidThanDeal(slots []Slot, selectedSlotIndx int, selectedSlot Slot, selectedBid *types.Bid) (int, bool) {
+ selectedBidIndex := selectedSlot.Index
+
+ if !isDealBid(selectedBid) {
+ return selectedBidIndex, false
+ }
+
+ if !sa.isBetterBidAvailable(slots, selectedSlotIndx, selectedBid) {
+ return selectedBidIndex, false
+ }
+
+ return sa.getBetterBid(sa.ImpBidMap[selectedSlot.ImpId], selectedBid, selectedBidIndex)
+}
diff --git a/endpoints/openrtb2/ctv/adpod/structured_adpod_test.go b/endpoints/openrtb2/ctv/adpod/structured_adpod_test.go
new file mode 100644
index 00000000000..70b3ceee8d0
--- /dev/null
+++ b/endpoints/openrtb2/ctv/adpod/structured_adpod_test.go
@@ -0,0 +1,862 @@
+package adpod
+
+import (
+ "testing"
+
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/types"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestStructuredAdpodPerformAuctionAndExclusion(t *testing.T) {
+ type fields struct {
+ AdpodCtx AdpodCtx
+ ImpBidMap map[string][]*types.Bid
+ WinningBid map[string]types.Bid
+ }
+ tests := []struct {
+ name string
+ fields fields
+ wantWinningBid map[string]types.Bid
+ }{
+ {
+ name: "only_price_based_auction_with_no_exclusion",
+ fields: fields{
+ AdpodCtx: AdpodCtx{
+ Type: Structured,
+ },
+ ImpBidMap: map[string][]*types.Bid{
+ "imp1": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 2,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 5,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 1,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp2": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ },
+ DealTierSatisfied: false,
+ Seat: "index",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 6,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp3": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 10,
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ },
+ },
+ WinningBid: make(map[string]types.Bid),
+ },
+ wantWinningBid: map[string]types.Bid{
+ "imp1": {
+ Bid: &openrtb2.Bid{
+ Price: 5,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ "imp2": {
+ Bid: &openrtb2.Bid{
+ Price: 6,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ "imp3": {
+ Bid: &openrtb2.Bid{
+ Price: 10,
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ },
+ },
+ {
+ name: "only_price_based_auction_with_exclusion",
+ fields: fields{
+ AdpodCtx: AdpodCtx{
+ Type: Structured,
+ Exclusion: Exclusion{
+ IABCategoryExclusion: true,
+ AdvertiserDomainExclusion: true,
+ },
+ },
+ ImpBidMap: map[string][]*types.Bid{
+ "imp1": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 6,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 1,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp2": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "index",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 3,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp3": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 10,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 1,
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ },
+ },
+ WinningBid: make(map[string]types.Bid),
+ },
+ wantWinningBid: map[string]types.Bid{
+ "imp1": {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ "imp2": {
+ Bid: &openrtb2.Bid{
+ Price: 3,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ "imp3": {
+ Bid: &openrtb2.Bid{
+ Price: 10,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ },
+ },
+ {
+ name: "price_and_deal_based_auction_with_no_exclusion",
+ fields: fields{
+ AdpodCtx: AdpodCtx{
+ Type: Structured,
+ },
+ ImpBidMap: map[string][]*types.Bid{
+ "imp1": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 6,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 1,
+ },
+ DealTierSatisfied: true,
+ Seat: "pubmatic",
+ },
+ },
+ "imp2": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "index",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 3,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp3": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 10,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "god",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 1,
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ },
+ },
+ WinningBid: make(map[string]types.Bid),
+ },
+ wantWinningBid: map[string]types.Bid{
+ "imp1": {
+ Bid: &openrtb2.Bid{
+ Price: 1,
+ },
+ DealTierSatisfied: true,
+ Seat: "pubmatic",
+ },
+ "imp2": {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "index",
+ },
+ "imp3": {
+ Bid: &openrtb2.Bid{
+ Price: 10,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "god",
+ },
+ },
+ },
+ {
+ name: "price_and_deal_based_auction_with_exclusion",
+ fields: fields{
+ AdpodCtx: AdpodCtx{
+ Type: Structured,
+ Exclusion: Exclusion{
+ IABCategoryExclusion: true,
+ AdvertiserDomainExclusion: true,
+ },
+ },
+ ImpBidMap: map[string][]*types.Bid{
+ "imp1": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 6,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 1,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp2": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "index",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 3,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp3": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 10,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "god",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 1,
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ },
+ },
+ WinningBid: make(map[string]types.Bid),
+ },
+ wantWinningBid: map[string]types.Bid{
+ "imp1": {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ "imp2": {
+ Bid: &openrtb2.Bid{
+ Price: 3,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ "imp3": {
+ Bid: &openrtb2.Bid{
+ Price: 10,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "god",
+ },
+ },
+ },
+ {
+ name: "price_and_deal_based_auction_with_exclusion_better_price_1",
+ fields: fields{
+ AdpodCtx: AdpodCtx{
+ Type: Structured,
+ Exclusion: Exclusion{
+ IABCategoryExclusion: true,
+ AdvertiserDomainExclusion: true,
+ },
+ },
+ ImpBidMap: map[string][]*types.Bid{
+ "imp1": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 6,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 1,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp2": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "index",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 3,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp3": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 8,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "god",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 9,
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ },
+ },
+ WinningBid: make(map[string]types.Bid),
+ },
+ wantWinningBid: map[string]types.Bid{
+ "imp1": {
+ Bid: &openrtb2.Bid{
+ Price: 6,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "pubmatic",
+ },
+ "imp2": {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ "imp3": {
+ Bid: &openrtb2.Bid{
+ Price: 9,
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ },
+ },
+ {
+ name: "price_and_deal_based_auction_with_exclusion_better_price_2",
+ fields: fields{
+ AdpodCtx: AdpodCtx{
+ Type: Structured,
+ Exclusion: Exclusion{
+ IABCategoryExclusion: true,
+ AdvertiserDomainExclusion: true,
+ },
+ },
+ ImpBidMap: map[string][]*types.Bid{
+ "imp1": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 6,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 1,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp2": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "index",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 3,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp3": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 8,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "god",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "god",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 9,
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ },
+ },
+ WinningBid: make(map[string]types.Bid),
+ },
+ wantWinningBid: map[string]types.Bid{
+ "imp1": {
+ Bid: &openrtb2.Bid{
+ Price: 6,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "pubmatic",
+ },
+ "imp2": {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ "imp3": {
+ Bid: &openrtb2.Bid{
+ Price: 9,
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ },
+ },
+ {
+ name: "price_and_deal_based_auction_with_exclusion_better_price_3",
+ fields: fields{
+ AdpodCtx: AdpodCtx{
+ Type: Structured,
+ Exclusion: Exclusion{
+ IABCategoryExclusion: true,
+ AdvertiserDomainExclusion: true,
+ },
+ },
+ ImpBidMap: map[string][]*types.Bid{
+ "imp1": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 6,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 1,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp2": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "index",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 3,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp3": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 8,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "god",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 9,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ },
+ },
+ WinningBid: make(map[string]types.Bid),
+ },
+ wantWinningBid: map[string]types.Bid{
+ "imp1": {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ "imp2": {
+ Bid: &openrtb2.Bid{
+ Price: 3,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ "imp3": {
+ Bid: &openrtb2.Bid{
+ Price: 8,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "god",
+ },
+ },
+ },
+ {
+ name: "price_and_deal_based_auction_with_exclusion_better_price_4",
+ fields: fields{
+ AdpodCtx: AdpodCtx{
+ Type: Structured,
+ Exclusion: Exclusion{
+ IABCategoryExclusion: true,
+ AdvertiserDomainExclusion: true,
+ },
+ },
+ ImpBidMap: map[string][]*types.Bid{
+ "imp1": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 6,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 1,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp2": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "index",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 3,
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ },
+ "imp3": {
+ {
+ Bid: &openrtb2.Bid{
+ Price: 8,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "god",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 10,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ {
+ Bid: &openrtb2.Bid{
+ Price: 9,
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ },
+ },
+ WinningBid: make(map[string]types.Bid),
+ },
+ wantWinningBid: map[string]types.Bid{
+ "imp1": {
+ Bid: &openrtb2.Bid{
+ Price: 6,
+ Cat: []string{"IAB-1"},
+ },
+ DealTierSatisfied: true,
+ Seat: "pubmatic",
+ },
+ "imp2": {
+ Bid: &openrtb2.Bid{
+ Price: 4,
+ Cat: []string{"IAB-2"},
+ },
+ DealTierSatisfied: false,
+ Seat: "pubmatic",
+ },
+ "imp3": {
+ Bid: &openrtb2.Bid{
+ Price: 9,
+ },
+ DealTierSatisfied: false,
+ Seat: "god",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ sa := &structuredAdpod{
+ AdpodCtx: tt.fields.AdpodCtx,
+ ImpBidMap: tt.fields.ImpBidMap,
+ WinningBid: tt.fields.WinningBid,
+ }
+ sa.HoldAuction()
+
+ assert.Equal(t, sa.WinningBid, tt.wantWinningBid, "Auction failed")
+ })
+ }
+}
diff --git a/endpoints/openrtb2/ctv/adpod/util.go b/endpoints/openrtb2/ctv/adpod/util.go
new file mode 100644
index 00000000000..5945be9f642
--- /dev/null
+++ b/endpoints/openrtb2/ctv/adpod/util.go
@@ -0,0 +1,85 @@
+package adpod
+
+import (
+ "errors"
+ "strconv"
+
+ "github.com/buger/jsonparser"
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/openrtb/v20/openrtb3"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/constant"
+ "github.com/prebid/prebid-server/v2/exchange"
+ "github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/models/nbr"
+ "github.com/prebid/prebid-server/v2/openrtb_ext"
+)
+
+func AddTargetingKey(bid *openrtb2.Bid, key openrtb_ext.TargetingKey, value string) error {
+ if nil == bid {
+ return errors.New("Invalid bid")
+ }
+
+ raw, err := jsonparser.Set(bid.Ext, []byte(strconv.Quote(value)), "prebid", "targeting", string(key))
+ if nil == err {
+ bid.Ext = raw
+ }
+ return err
+}
+
+// ConvertAPRCToNBRC converts the aprc to NonBidStatusCode
+func ConvertAPRCToNBRC(bidStatus int64) *openrtb3.NoBidReason {
+ var nbrCode openrtb3.NoBidReason
+
+ switch bidStatus {
+ case constant.StatusOK:
+ nbrCode = nbr.LossBidLostToHigherBid
+ case constant.StatusCategoryExclusion:
+ nbrCode = exchange.ResponseRejectedCreativeCategoryExclusions
+ case constant.StatusDomainExclusion:
+ nbrCode = exchange.ResponseRejectedCreativeAdvertiserExclusions
+ case constant.StatusDurationMismatch:
+ nbrCode = exchange.ResponseRejectedInvalidCreative
+
+ default:
+ return nil
+ }
+ return &nbrCode
+}
+
+func GetPodType(imp openrtb2.Imp, extAdpod openrtb_ext.ExtVideoAdPod) PodType {
+ if extAdpod.AdPod != nil {
+ return Dynamic
+ }
+
+ if len(imp.Video.PodID) > 0 && imp.Video.PodDur > 0 {
+ return Dynamic
+ }
+
+ if len(imp.Video.PodID) > 0 {
+ return Structured
+ }
+
+ return NotAdpod
+}
+
+func ConvertToV25VideoRequest(request *openrtb2.BidRequest) {
+ for i := range request.Imp {
+ imp := request.Imp[i]
+
+ if imp.Video == nil {
+ continue
+ }
+
+ // Remove 2.6 Adpod parameters
+ imp.Video.PodID = ""
+ imp.Video.PodDur = 0
+ imp.Video.MaxSeq = 0
+ }
+}
+
+func createMapFromSlice(slice []string) map[string]bool {
+ resultMap := make(map[string]bool)
+ for _, item := range slice {
+ resultMap[item] = true
+ }
+ return resultMap
+}
diff --git a/endpoints/openrtb2/ctv/adpod/util_test.go b/endpoints/openrtb2/ctv/adpod/util_test.go
new file mode 100644
index 00000000000..57855b38461
--- /dev/null
+++ b/endpoints/openrtb2/ctv/adpod/util_test.go
@@ -0,0 +1,85 @@
+package adpod
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v2/openrtb_ext"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAddTargetingKeys(t *testing.T) {
+ var tests = []struct {
+ scenario string // Testcase scenario
+ key string
+ value string
+ bidExt string
+ expect map[string]string
+ }{
+ {scenario: "key_not_exists", key: "hb_pb_cat_dur", value: "some_value", bidExt: `{"prebid":{"targeting":{}}}`, expect: map[string]string{"hb_pb_cat_dur": "some_value"}},
+ {scenario: "key_already_exists", key: "hb_pb_cat_dur", value: "new_value", bidExt: `{"prebid":{"targeting":{"hb_pb_cat_dur":"old_value"}}}`, expect: map[string]string{"hb_pb_cat_dur": "new_value"}},
+ }
+ for _, test := range tests {
+ t.Run(test.scenario, func(t *testing.T) {
+ bid := new(openrtb2.Bid)
+ bid.Ext = []byte(test.bidExt)
+ key := openrtb_ext.TargetingKey(test.key)
+ assert.Nil(t, AddTargetingKey(bid, key, test.value))
+ extBid := openrtb_ext.ExtBid{}
+ json.Unmarshal(bid.Ext, &extBid)
+ assert.Equal(t, test.expect, extBid.Prebid.Targeting)
+ })
+ }
+ assert.Equal(t, "Invalid bid", AddTargetingKey(nil, openrtb_ext.HbCategoryDurationKey, "some value").Error())
+}
+
+func TestConvertToV25VideoRequest(t *testing.T) {
+ type args struct {
+ request *openrtb2.BidRequest
+ }
+ tests := []struct {
+ name string
+ args args
+ want *openrtb2.BidRequest
+ }{
+ {
+ name: "Remove adpod parameters",
+ args: args{
+ request: &openrtb2.BidRequest{
+ ID: "request",
+ Imp: []openrtb2.Imp{
+ {
+ ID: "imp1",
+ Video: &openrtb2.Video{
+ MinDuration: 10,
+ MaxDuration: 30,
+ PodDur: 90,
+ PodID: "pod1",
+ MaxSeq: 3,
+ },
+ },
+ },
+ },
+ },
+ want: &openrtb2.BidRequest{
+ ID: "request",
+ Imp: []openrtb2.Imp{
+ {
+ ID: "imp1",
+ Video: &openrtb2.Video{
+ MinDuration: 10,
+ MaxDuration: 30,
+ },
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ConvertToV25VideoRequest(tt.args.request)
+ assert.Equal(t, tt.want, tt.args.request, "Failed to remove adpod paramaters")
+ })
+ }
+}
diff --git a/endpoints/openrtb2/ctv/response/adpod_generator.go b/endpoints/openrtb2/ctv/response/adpod_generator.go
index 67301776936..c63817104c2 100644
--- a/endpoints/openrtb2/ctv/response/adpod_generator.go
+++ b/endpoints/openrtb2/ctv/response/adpod_generator.go
@@ -1,11 +1,9 @@
package response
import (
- "fmt"
"sync"
"time"
- "github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/combination"
"github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/constant"
"github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/types"
@@ -40,30 +38,24 @@ type highestCombination struct {
// AdPodGenerator AdPodGenerator
type AdPodGenerator struct {
IAdPodGenerator
- request *openrtb2.BidRequest
- impIndex int
- buckets types.BidsBuckets
- comb combination.ICombination
- adpod *openrtb_ext.VideoAdPod
- met metrics.MetricsEngine
+ buckets types.BidsBuckets
+ comb combination.ICombination
+ adpod *openrtb_ext.VideoAdPod
+ met metrics.MetricsEngine
}
// NewAdPodGenerator will generate adpod based on configuration
-func NewAdPodGenerator(request *openrtb2.BidRequest, impIndex int, buckets types.BidsBuckets, comb combination.ICombination, adpod *openrtb_ext.VideoAdPod, met metrics.MetricsEngine) *AdPodGenerator {
+func NewAdPodGenerator(buckets types.BidsBuckets, comb combination.ICombination, adpod *openrtb_ext.VideoAdPod, met metrics.MetricsEngine) *AdPodGenerator {
return &AdPodGenerator{
- request: request,
- impIndex: impIndex,
- buckets: buckets,
- comb: comb,
- adpod: adpod,
- met: met,
+ buckets: buckets,
+ comb: comb,
+ adpod: adpod,
+ met: met,
}
}
// GetAdPodBids will return Adpod based on configurations
func (o *AdPodGenerator) GetAdPodBids() *types.AdPodBid {
- defer util.TimeTrack(time.Now(), fmt.Sprintf("Tid:%v ImpId:%v adpodgenerator", o.request.ID, o.request.Imp[o.impIndex].ID))
-
results := o.getAdPodBids(10 * time.Millisecond)
adpodBid := o.getMaxAdPodBid(results)
@@ -74,8 +66,8 @@ func (o *AdPodGenerator) cleanup(wg *sync.WaitGroup, responseCh chan *highestCom
defer func() {
close(responseCh)
for extra := range responseCh {
- if nil != extra {
- util.Logf("Tid:%v ImpId:%v Delayed Response Durations:%v Bids:%v", o.request.ID, o.request.Imp[o.impIndex].ID, extra.durations, extra.bidIDs)
+ if extra != nil {
+ util.Logf("Delayed Response Durations:%v Bids:%v", extra.durations, extra.bidIDs)
}
}
}()
@@ -83,9 +75,6 @@ func (o *AdPodGenerator) cleanup(wg *sync.WaitGroup, responseCh chan *highestCom
}
func (o *AdPodGenerator) getAdPodBids(timeout time.Duration) []*highestCombination {
- start := time.Now()
- defer util.TimeTrack(start, fmt.Sprintf("Tid:%v ImpId:%v getAdPodBids", o.request.ID, o.request.Imp[o.impIndex].ID))
-
maxRoutines := 2
isTimedOutORReceivedAllResponses := false
results := []*highestCombination{}
@@ -103,7 +92,6 @@ func (o *AdPodGenerator) getAdPodBids(timeout time.Duration) []*highestCombinati
hbc := o.getUniqueBids(durations)
hbc.timeTakenCombGen = combGenElapsedTime
responseCh <- hbc
- util.Logf("Tid:%v GetUniqueBids Durations:%v Price:%v DealBids:%v Time:%v Bids:%v combGenElapsedTime:%v", o.request.ID, hbc.durations[:], hbc.price, hbc.nDealBids, hbc.timeTakenCompExcl, hbc.bidIDs[:], combGenElapsedTime)
}
combinationCount := 0
for i := 0; i < maxRoutines; i++ {
@@ -122,7 +110,7 @@ func (o *AdPodGenerator) getAdPodBids(timeout time.Duration) []*highestCombinati
hbc := o.getUniqueBids(durations)
hbc.timeTakenCombGen = combGenElapsedTime
responseCh <- hbc
- util.Logf("Tid:%v GetUniqueBids Durations:%v Price:%v DealBids:%v Time:%v Bids:%v combGenElapsedTime:%v", o.request.ID, hbc.durations[:], hbc.price, hbc.nDealBids, hbc.timeTakenCompExcl, hbc.bidIDs[:], combGenElapsedTime)
+ util.Logf("GetUniqueBids Durations:%v Price:%v DealBids:%v Time:%v Bids:%v combGenElapsedTime:%v", hbc.durations[:], hbc.price, hbc.nDealBids, hbc.timeTakenCompExcl, hbc.bidIDs[:], combGenElapsedTime)
}
wg.Done()
}()
@@ -138,7 +126,7 @@ func (o *AdPodGenerator) getAdPodBids(timeout time.Duration) []*highestCombinati
select {
case hbc, ok := <-responseCh:
- if false == ok {
+ if !ok {
isTimedOutORReceivedAllResponses = true
break
}
@@ -150,7 +138,6 @@ func (o *AdPodGenerator) getAdPodBids(timeout time.Duration) []*highestCombinati
}
case <-ticker.C:
isTimedOutORReceivedAllResponses = true
- util.Logf("Tid:%v ImpId:%v GetAdPodBids Timeout Reached %v", o.request.ID, o.request.Imp[o.impIndex].ID, timeout)
}
}
@@ -177,8 +164,7 @@ func (o *AdPodGenerator) getAdPodBids(timeout time.Duration) []*highestCombinati
}
func (o *AdPodGenerator) getMaxAdPodBid(results []*highestCombination) *types.AdPodBid {
- if 0 == len(results) {
- util.Logf("Tid:%v ImpId:%v NoBid", o.request.ID, o.request.Imp[o.impIndex].ID)
+ if len(results) == 0 {
return nil
}
@@ -201,8 +187,7 @@ func (o *AdPodGenerator) getMaxAdPodBid(results []*highestCombination) *types.Ad
}
}
- if nil == maxResult {
- util.Logf("Tid:%v ImpId:%v All Combination Filtered in Ad Exclusion", o.request.ID, o.request.Imp[o.impIndex].ID)
+ if maxResult == nil {
return nil
}
@@ -223,7 +208,7 @@ func (o *AdPodGenerator) getMaxAdPodBid(results []*highestCombination) *types.Ad
adpodBid.Cat = append(adpodBid.Cat, cat)
}
- util.Logf("Tid:%v ImpId:%v Selected Durations:%v Price:%v Bids:%v", o.request.ID, o.request.Imp[o.impIndex].ID, maxResult.durations[:], maxResult.price, maxResult.bidIDs[:])
+ util.Logf("Selected Durations:%v Price:%v Bids:%v", maxResult.durations, maxResult.price, maxResult.bidIDs)
return adpodBid
}
@@ -233,11 +218,9 @@ func (o *AdPodGenerator) getUniqueBids(durationSequence []int) *highestCombinati
data := [][]*types.Bid{}
combinations := []int{}
- defer util.TimeTrack(startTime, fmt.Sprintf("Tid:%v ImpId:%v getUniqueBids:%v", o.request.ID, o.request.Imp[o.impIndex].ID, durationSequence))
-
uniqueDuration := 0
for index, duration := range durationSequence {
- if 0 != index && durationSequence[index-1] == duration {
+ if index != 0 && durationSequence[index-1] == duration {
combinations[uniqueDuration-1]++
continue
}
@@ -274,7 +257,7 @@ func findUniqueCombinations(data [][]*types.Bid, combination []int, maxCategoryS
filterBids := map[string]*filteredBid{}
// maintain highest price combination
- for true {
+ for {
ehc, inext, jnext, rc = evaluate(data[:], indices[:], totalBids, maxCategoryScore, maxDomainScore)
if nil != ehc {
@@ -296,7 +279,7 @@ func findUniqueCombinations(data [][]*types.Bid, combination []int, maxCategoryS
}
}
- if -1 == inext {
+ if inext == -1 {
inext, jnext = n-1, 0
}
diff --git a/endpoints/openrtb2/ctv/response/adpod_generator_test.go b/endpoints/openrtb2/ctv/response/adpod_generator_test.go
index 9cbf3619dee..7944d58acea 100644
--- a/endpoints/openrtb2/ctv/response/adpod_generator_test.go
+++ b/endpoints/openrtb2/ctv/response/adpod_generator_test.go
@@ -378,14 +378,10 @@ func TestAdPodGenerator_getMaxAdPodBid(t *testing.T) {
Price: 10,
},
},
- // TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- o := &AdPodGenerator{
- request: tt.fields.request,
- impIndex: tt.fields.impIndex,
- }
+ o := &AdPodGenerator{}
got := o.getMaxAdPodBid(tt.args.results)
if nil != got {
sort.Strings(got.ADomain)
diff --git a/endpoints/openrtb2/ctv/util/util.go b/endpoints/openrtb2/ctv/util/util.go
index 1c0aac8523b..49f28178dbb 100644
--- a/endpoints/openrtb2/ctv/util/util.go
+++ b/endpoints/openrtb2/ctv/util/util.go
@@ -38,14 +38,14 @@ func GetDurationWiseBidsBucket(bids []*types.Bid) types.BidsBuckets {
for k, v := range result {
//sort.Slice(v[:], func(i, j int) bool { return v[i].Price > v[j].Price })
- sortBids(v[:])
+ SortBids(v[:])
result[k] = v
}
return result
}
-func sortBids(bids []*types.Bid) {
+func SortBids(bids []*types.Bid) {
sort.Slice(bids, func(i, j int) bool {
if bids[i].DealTierSatisfied == bids[j].DealTierSatisfied {
return bids[i].Price > bids[j].Price
diff --git a/endpoints/openrtb2/ctv/util/util_test.go b/endpoints/openrtb2/ctv/util/util_test.go
index 136f1b3c9a1..d96cd451c65 100644
--- a/endpoints/openrtb2/ctv/util/util_test.go
+++ b/endpoints/openrtb2/ctv/util/util_test.go
@@ -164,8 +164,8 @@ func TestSortByDealPriority(t *testing.T) {
for _, bid := range bids {
fmt.Println(bid.ID, ",", bid.Price, ",", bid.DealTierSatisfied)
}
- sortBids(bids[:])
- fmt.Println("After sort")
+ SortBids(bids[:])
+
actual := []string{}
for _, bid := range bids {
fmt.Println(bid.ID, ",", bid.Price, ", ", bid.DealTierSatisfied)
diff --git a/endpoints/openrtb2/ctv_auction.go b/endpoints/openrtb2/ctv_auction.go
index b4ac7212e30..91cb1be54ff 100644
--- a/endpoints/openrtb2/ctv_auction.go
+++ b/endpoints/openrtb2/ctv_auction.go
@@ -5,29 +5,19 @@ import (
"encoding/json"
"errors"
"fmt"
- "math"
"net/http"
- "net/url"
- "sort"
- "strconv"
"strings"
"time"
- "github.com/beevik/etree"
"github.com/buger/jsonparser"
- uuid "github.com/gofrs/uuid"
"github.com/golang/glog"
"github.com/julienschmidt/httprouter"
"github.com/prebid/openrtb/v20/openrtb2"
- "github.com/prebid/openrtb/v20/openrtb3"
accountService "github.com/prebid/prebid-server/v2/account"
"github.com/prebid/prebid-server/v2/analytics"
"github.com/prebid/prebid-server/v2/config"
- "github.com/prebid/prebid-server/v2/endpoints/events"
- "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/combination"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/adpod"
"github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/constant"
- "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/impressions"
- "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/response"
"github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/types"
"github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/util"
"github.com/prebid/prebid-server/v2/errortypes"
@@ -36,7 +26,6 @@ import (
"github.com/prebid/prebid-server/v2/hooks"
"github.com/prebid/prebid-server/v2/hooks/hookexecution"
"github.com/prebid/prebid-server/v2/metrics"
- "github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/models/nbr"
"github.com/prebid/prebid-server/v2/openrtb_ext"
"github.com/prebid/prebid-server/v2/privacy"
"github.com/prebid/prebid-server/v2/stored_requests"
@@ -50,12 +39,12 @@ type ctvEndpointDeps struct {
endpointDeps
request *openrtb2.BidRequest
reqExt *openrtb_ext.ExtRequestAdPod
- impData []*types.ImpData
- videoSeats []*openrtb2.SeatBid //stores pure video impression bids
- impIndices map[string]int
- isAdPodRequest bool
impsExtPrebidBidder map[string]map[string]map[string]interface{}
impPartnerBlockedTagIDMap map[string]map[string][]string
+ impToPodId map[string]string
+ podCtx map[string]adpod.Adpod
+ videoImps []openrtb2.Imp
+ videoSeats []*openrtb2.SeatBid //stores pure video impression bids
labels metrics.Labels
}
@@ -163,27 +152,32 @@ func (deps *ctvEndpointDeps) CTVAuctionEndpoint(w http.ResponseWriter, r *http.R
}
request = reqWrapper.BidRequest
- util.JLogf("Original BidRequest", request) //TODO: REMOVE LOG
-
//init
deps.init(request)
+ // Read Request extension
+ if errs := deps.readRequestExtension(); len(errs) > 0 {
+ writeError(errs, w, &deps.labels)
+ return
+ }
+
+ // set adpod context
+ if errs := deps.prepareAdpodCtx(request); len(errs) > 0 {
+ writeError(errs, w, &deps.labels)
+ return
+ }
+
//Set Default Values
deps.setDefaultValues()
- util.JLogf("Extensions Request Extension", deps.reqExt)
- util.JLogf("Extensions ImpData", deps.impData)
//Validate CTV BidRequest
- if err := deps.validateBidRequest(); err != nil {
- errL = append(errL, err...)
- writeError(errL, w, &deps.labels)
+ if errs := deps.ValidateAdpodCtx(); errs != nil {
+ writeError(errs, w, &deps.labels)
return
}
- if deps.isAdPodRequest {
- //Create New BidRequest
+ if len(deps.podCtx) > 0 {
request = deps.createBidRequest(request)
- util.JLogf("CTV BidRequest", request) //TODO: REMOVE LOG
}
//Parsing Cookies and Set Stats
@@ -263,33 +257,18 @@ func (deps *ctvEndpointDeps) CTVAuctionEndpoint(w http.ResponseWriter, r *http.R
if err != nil {
util.JLogf("Error setting seatNonBid in responseExt: %v", err) //TODO: REMOVE LOG
}
- util.JLogf("BidResponse", response) //TODO: REMOVE LOG
-
- if deps.isAdPodRequest {
- //Validate Bid Response
- if err := deps.validateBidResponse(request, response); err != nil {
- errL = append(errL, err)
- writeError(errL, w, &deps.labels)
- return
- }
+ if len(deps.podCtx) > 0 {
//Create Impression Bids
- deps.getBids(response)
+ deps.collectBids(response)
- //Do AdPod Exclusions
- bids := deps.doAdPodExclusions()
+ //Hold Auction
+ deps.doAdpodAuction()
//Create Bid Response
- adPodBidResponse := deps.createAdPodBidResponse(response, bids)
-
- //Set bid.Ext params - adpod.aprc, prebid.video.duration
- deps.setBidExtParams()
-
- deps.recordRejectedAdPodBids(deps.labels.PubID)
+ adPodBidResponse := deps.createAdPodBidResponse(response)
adPodBidResponse.Ext = deps.getBidResponseExt(response)
response = adPodBidResponse
-
- util.JLogf("CTV BidResponse", response) //TODO: REMOVE LOG
}
ao.Response = response
@@ -325,62 +304,107 @@ func (deps *ctvEndpointDeps) holdAuction(ctx context.Context, auctionRequest exc
func (deps *ctvEndpointDeps) init(req *openrtb2.BidRequest) {
deps.request = req
- deps.impData = make([]*types.ImpData, len(req.Imp))
- deps.impIndices = make(map[string]int, len(req.Imp))
+ deps.impToPodId = make(map[string]string)
+}
+
+/*
+PrepareAdpodCtx will check for adpod param, and create adpod context if they are
+available in the request. It will check for both ORTB 2.6 and legacy adpod parmaters.
+*/
+func (deps *ctvEndpointDeps) prepareAdpodCtx(request *openrtb2.BidRequest) (errs []error) {
+ deps.podCtx = make(map[string]adpod.Adpod)
+
+ for _, imp := range request.Imp {
+ if imp.Video != nil {
+ // check for adpod in the extension
+ extAdpod, err := deps.readVideoAdPodExt(imp)
+ if err != nil {
+ errs = append(errs, err)
+ }
- for i := range req.Imp {
- deps.impIndices[req.Imp[i].ID] = i
- deps.impData[i] = &types.ImpData{}
+ switch adpod.GetPodType(imp, extAdpod) {
+ case adpod.Dynamic:
+ deps.createDynamicAdpodCtx(imp, extAdpod)
+ case adpod.Structured:
+ deps.createStructuredAdpodCtx(imp)
+ default:
+ // Pure video impressions
+ deps.videoImps = append(deps.videoImps, imp)
+ }
+ }
}
+ return
}
-func (deps *ctvEndpointDeps) readVideoAdPodExt() (err []error) {
- for index, imp := range deps.request.Imp {
- if nil != imp.Video {
- vidExt := openrtb_ext.ExtVideoAdPod{}
- if len(imp.Video.Ext) > 0 {
- errL := json.Unmarshal(imp.Video.Ext, &vidExt)
- if nil != err {
- err = append(err, errL)
- continue
- }
+func (deps *ctvEndpointDeps) createDynamicAdpodCtx(imp openrtb2.Imp, adpodExt openrtb_ext.ExtVideoAdPod) {
+ podId := imp.Video.PodID
+ if len(podId) == 0 {
+ podId = imp.ID
+ }
+ deps.impToPodId[imp.ID] = podId
- imp.Video.Ext = jsonparser.Delete(imp.Video.Ext, constant.CTVAdpod)
- imp.Video.Ext = jsonparser.Delete(imp.Video.Ext, constant.CTVOffset)
- if string(imp.Video.Ext) == `{}` {
- imp.Video.Ext = nil
- }
- }
+ deps.podCtx[podId] = adpod.NewDynamicAdpod(deps.labels.PubID, imp, adpodExt, deps.metricsEngine, deps.reqExt)
+}
- if nil == vidExt.AdPod {
- if nil == deps.reqExt {
- continue
- }
- vidExt.AdPod = &openrtb_ext.VideoAdPod{}
- }
+func (deps *ctvEndpointDeps) createStructuredAdpodCtx(imp openrtb2.Imp) {
+ deps.impToPodId[imp.ID] = imp.Video.PodID
- //Use Request Level Parameters
- if nil != deps.reqExt {
- vidExt.AdPod.Merge(&deps.reqExt.VideoAdPod)
- }
+ podContext, ok := deps.podCtx[imp.Video.PodID]
+ if !ok {
+ podContext = adpod.NewStructuredAdpod(deps.labels.PubID, deps.metricsEngine, deps.reqExt)
+ }
- //Set Default Values
- vidExt.SetDefaultValue()
- vidExt.AdPod.SetDefaultAdDurations(imp.Video.MinDuration, imp.Video.MaxDuration)
+ podContext.AddImpressions(imp)
+ deps.podCtx[imp.Video.PodID] = podContext
+}
- deps.impData[index].VideoExt = &vidExt
+func (deps *ctvEndpointDeps) ValidateAdpodCtx() []error {
+ var errs []error
+ for _, eachpod := range deps.podCtx {
+ err := eachpod.Validate()
+ if err != nil {
+ errs = append(errs, err...)
}
}
- return err
+
+ return errs
+}
+
+func (deps *ctvEndpointDeps) readVideoAdPodExt(imp openrtb2.Imp) (openrtb_ext.ExtVideoAdPod, error) {
+ var adpodExt openrtb_ext.ExtVideoAdPod
+
+ if imp.Video != nil && len(imp.Video.Ext) > 0 {
+ err := json.Unmarshal(imp.Video.Ext, &adpodExt)
+ if err != nil {
+ return adpodExt, err
+ }
+ }
+
+ if adpodExt.AdPod == nil && deps.reqExt == nil {
+ return adpodExt, nil
+ }
+
+ if deps.reqExt != nil && deps.reqExt.VideoAdPod != nil {
+ if adpodExt.AdPod == nil {
+ adpodExt.AdPod = &openrtb_ext.VideoAdPod{}
+ }
+ adpodExt.AdPod.Merge(deps.reqExt.VideoAdPod)
+ }
+
+ //Set Default Values
+ if adpodExt.AdPod != nil {
+ adpodExt.SetDefaultValue()
+ adpodExt.AdPod.SetDefaultAdDurations(imp.Video.MinDuration, imp.Video.MaxDuration)
+ }
+
+ return adpodExt, nil
}
func (deps *ctvEndpointDeps) readRequestExtension() (err []error) {
if len(deps.request.Ext) > 0 {
-
//TODO: use jsonparser library for get adpod and remove that key
extAdPod, jsonType, _, errL := jsonparser.Get(deps.request.Ext, constant.CTVAdpod)
-
- if nil != errL {
+ if errL != nil {
//parsing error
if jsonparser.NotExist != jsonType {
//assuming key not present
@@ -389,7 +413,6 @@ func (deps *ctvEndpointDeps) readRequestExtension() (err []error) {
}
} else {
deps.reqExt = &openrtb_ext.ExtRequestAdPod{}
-
if errL := json.Unmarshal(extAdPod, deps.reqExt); nil != errL {
err = append(err, errL)
return
@@ -397,70 +420,19 @@ func (deps *ctvEndpointDeps) readRequestExtension() (err []error) {
deps.reqExt.SetDefaultValue()
}
- }
- return
-}
-
-func (deps *ctvEndpointDeps) readExtensions() (err []error) {
- if errL := deps.readRequestExtension(); nil != errL {
- err = append(err, errL...)
- }
-
- if errL := deps.readVideoAdPodExt(); nil != errL {
- err = append(err, errL...)
- }
- return err
-}
-
-func (deps *ctvEndpointDeps) setIsAdPodRequest() {
- deps.isAdPodRequest = false
- for _, data := range deps.impData {
- if nil != data.VideoExt && nil != data.VideoExt.AdPod {
- deps.isAdPodRequest = true
- break
- }
+ err = deps.reqExt.Validate()
}
+ return
}
// setDefaultValues will set adpod and other default values
func (deps *ctvEndpointDeps) setDefaultValues() {
- //read and set extension values
- deps.readExtensions()
-
- //set request is adpod request or normal request
- deps.setIsAdPodRequest()
-
- if deps.isAdPodRequest {
+ if len(deps.podCtx) > 0 {
deps.readImpExtensionsAndTags()
}
}
-// validateBidRequest will validate AdPod specific mandatory Parameters and returns error
-func (deps *ctvEndpointDeps) validateBidRequest() (err []error) {
- //validating video extension adpod configurations
- if nil != deps.reqExt {
- err = deps.reqExt.Validate()
- }
-
- for index, imp := range deps.request.Imp {
- if nil != imp.Video && nil != deps.impData[index].VideoExt {
- ext := deps.impData[index].VideoExt
- if errL := ext.Validate(); nil != errL {
- err = append(err, errL...)
- }
-
- if nil != ext.AdPod {
- if errL := ext.AdPod.ValidateAdPodDurations(imp.Video.MinDuration, imp.Video.MaxDuration, imp.Video.MaxExtended); nil != errL {
- err = append(err, errL...)
- }
- }
- }
-
- }
- return
-}
-
// readImpExtensionsAndTags will read the impression extensions
func (deps *ctvEndpointDeps) readImpExtensionsAndTags() (errs []error) {
deps.impsExtPrebidBidder = make(map[string]map[string]map[string]interface{})
@@ -509,22 +481,26 @@ func (deps *ctvEndpointDeps) readImpExtensionsAndTags() (errs []error) {
// createBidRequest will return new bid request with all things copy from bid request except impression objects
func (deps *ctvEndpointDeps) createBidRequest(req *openrtb2.BidRequest) *openrtb2.BidRequest {
ctvRequest := *req
+ var imps []openrtb2.Imp
- //get configurations for all impressions
- deps.getAllAdPodImpsConfigs()
+ for _, adpodCtx := range deps.podCtx {
+ imps = append(imps, adpodCtx.GetImpressions()...)
+ }
- //createImpressions
- ctvRequest.Imp = deps.createImpressions()
+ if len(deps.videoImps) > 0 {
+ imps = append(imps, deps.videoImps...)
+ }
+ ctvRequest.Imp = imps
+
+ adpod.ConvertToV25VideoRequest(&ctvRequest)
deps.filterImpsVastTagsByDuration(&ctvRequest)
- //TODO: remove adpod extension if not required to send further
return &ctvRequest
}
// filterImpsVastTagsByDuration checks if a Vast tag should be called for a generated impression based on the duration of tag and impression
func (deps *ctvEndpointDeps) filterImpsVastTagsByDuration(bidReq *openrtb2.BidRequest) {
-
for impCount, imp := range bidReq.Imp {
index := strings.LastIndex(imp.ID, "_")
if index == -1 {
@@ -588,15 +564,6 @@ func (deps *ctvEndpointDeps) filterImpsVastTagsByDuration(bidReq *openrtb2.BidRe
imp.Ext = impExt
bidReq.Imp[impCount] = imp
}
-
- for impID, blockedTags := range deps.impPartnerBlockedTagIDMap {
- for _, datum := range deps.impData {
- if datum.ImpID == impID {
- datum.BlockedVASTTags = blockedTags
- break
- }
- }
- }
}
func remove(slice []string, item string) []string {
@@ -615,157 +582,44 @@ func remove(slice []string, item string) []string {
return append(slice[:index], slice[index+1:]...)
}
-// getAllAdPodImpsConfigs will return all impression adpod configurations
-func (deps *ctvEndpointDeps) getAllAdPodImpsConfigs() {
- for index, imp := range deps.request.Imp {
- if nil == imp.Video || nil == deps.impData[index].VideoExt || nil == deps.impData[index].VideoExt.AdPod {
- continue
- }
- deps.impData[index].ImpID = imp.ID
-
- config, err := deps.getAdPodImpsConfigs(&imp, deps.impData[index].VideoExt.AdPod)
- if err != nil {
- deps.impData[index].Error = util.ErrToBidderMessage(err)
- continue
- }
- deps.impData[index].Config = config[:]
- }
-}
-
-// getAdPodImpsConfigs will return number of impressions configurations within adpod
-func (deps *ctvEndpointDeps) getAdPodImpsConfigs(imp *openrtb2.Imp, adpod *openrtb_ext.VideoAdPod) ([]*types.ImpAdPodConfig, error) {
- // monitor
- start := time.Now()
- selectedAlgorithm := impressions.SelectAlgorithm(deps.reqExt)
- impGen := impressions.NewImpressions(imp.Video.MinDuration, imp.Video.MaxDuration, deps.reqExt, adpod, selectedAlgorithm)
- impRanges := impGen.Get()
- labels := metrics.PodLabels{AlgorithmName: impressions.MonitorKey[selectedAlgorithm], NoOfImpressions: new(int)}
-
- //log number of impressions in stats
- *labels.NoOfImpressions = len(impRanges)
- deps.metricsEngine.RecordPodImpGenTime(labels, start)
-
- // check if algorithm has generated impressions
- if len(impRanges) == 0 {
- return nil, util.UnableToGenerateImpressionsError
- }
-
- config := make([]*types.ImpAdPodConfig, len(impRanges))
- for i, value := range impRanges {
- config[i] = &types.ImpAdPodConfig{
- ImpID: util.GetCTVImpressionID(imp.ID, i+1),
- MinDuration: value[0],
- MaxDuration: value[1],
- SequenceNumber: int8(i + 1), /* Must be starting with 1 */
- }
- }
- return config[:], nil
-}
-
-// createImpressions will create multiple impressions based on adpod configurations
-func (deps *ctvEndpointDeps) createImpressions() []openrtb2.Imp {
- impCount := 0
- for _, imp := range deps.impData {
- if nil == imp.Error {
- if len(imp.Config) == 0 {
- impCount = impCount + 1
- } else {
- impCount = impCount + len(imp.Config)
- }
- }
- }
-
- count := 0
- imps := make([]openrtb2.Imp, impCount)
- for index, imp := range deps.request.Imp {
- if nil == deps.impData[index].Error {
- adPodConfig := deps.impData[index].Config
- if len(adPodConfig) == 0 {
- //non adpod request it will be normal video impression
- imps[count] = imp
- count++
- } else {
- //for adpod request it will create new impression based on configurations
- for _, config := range adPodConfig {
- imps[count] = *(newImpression(&imp, config))
- count++
- }
- }
- }
- }
- return imps[:]
-}
-
-// newImpression will clone existing impression object and create video object with ImpAdPodConfig.
-func newImpression(imp *openrtb2.Imp, config *types.ImpAdPodConfig) *openrtb2.Imp {
- video := *imp.Video
- video.MinDuration = config.MinDuration
- video.MaxDuration = config.MaxDuration
- video.Sequence = config.SequenceNumber
- video.MaxExtended = 0
- //TODO: remove video adpod extension if not required
-
- newImp := *imp
- newImp.ID = config.ImpID
- //newImp.BidFloor = 0
- newImp.Video = &video
- return &newImp
-}
-
/********************* Prebid BidResponse Processing *********************/
-// validateBidResponse
-func (deps *ctvEndpointDeps) validateBidResponse(req *openrtb2.BidRequest, resp *openrtb2.BidResponse) error {
- //remove bids withoug cat and adomain
-
- return nil
-}
-
-// getBids reads bids from bidresponse object
-func (deps *ctvEndpointDeps) getBids(resp *openrtb2.BidResponse) {
+func (deps *ctvEndpointDeps) collectBids(response *openrtb2.BidResponse) {
var vseat *openrtb2.SeatBid
- result := make(map[string]*types.AdPodBid)
- for i := range resp.SeatBid {
- seat := resp.SeatBid[i]
+ for i := range response.SeatBid {
+ seat := response.SeatBid[i]
vseat = nil
-
for j := range seat.Bid {
bid := &seat.Bid[j]
- if len(bid.ID) == 0 {
- bidID, err := uuid.NewV4()
- if nil != err {
- continue
- }
- bid.ID = bidID.String()
- }
-
if bid.Price == 0 {
- //filter invalid bids
continue
}
- originalImpID, sequenceNumber := deps.getImpressionID(bid.ImpID)
- if sequenceNumber < 0 {
- continue
+ if len(bid.ID) == 0 {
+ bidId, err := jsonparser.GetString(bid.Ext, "prebid", "bidid")
+ if err == nil {
+ bid.ID = bidId
+ }
}
+ originalImpID, _ := util.DecodeImpressionID(bid.ImpID) //TODO: check if we can reomove and maintain map
+
value, err := util.GetTargeting(openrtb_ext.HbCategoryDurationKey, openrtb_ext.BidderName(seat.Seat), *bid)
if nil == err {
// ignore error
- addTargetingKey(bid, openrtb_ext.HbCategoryDurationKey, value)
+ adpod.AddTargetingKey(bid, openrtb_ext.HbCategoryDurationKey, value)
}
value, err = util.GetTargeting(openrtb_ext.HbpbConstantKey, openrtb_ext.BidderName(seat.Seat), *bid)
if nil == err {
// ignore error
- addTargetingKey(bid, openrtb_ext.HbpbConstantKey, value)
+ adpod.AddTargetingKey(bid, openrtb_ext.HbpbConstantKey, value)
}
- index := deps.impIndices[originalImpID]
- if len(deps.impData[index].Config) == 0 {
- //adding pure video bids
+ podId, ok := deps.impToPodId[originalImpID]
+ if !ok {
if vseat == nil {
vseat = &openrtb2.SeatBid{
Seat: seat.Seat,
@@ -775,436 +629,61 @@ func (deps *ctvEndpointDeps) getBids(resp *openrtb2.BidResponse) {
deps.videoSeats = append(deps.videoSeats, vseat)
}
vseat.Bid = append(vseat.Bid, *bid)
- } else {
- //reading extension, ingorning parsing error
- ext := openrtb_ext.ExtBid{}
- if nil != bid.Ext {
- json.Unmarshal(bid.Ext, &ext)
- }
-
- //Adding adpod bids
- impBids, ok := result[originalImpID]
- if !ok {
- impBids = &types.AdPodBid{
- OriginalImpID: originalImpID,
- SeatName: string(openrtb_ext.BidderOWPrebidCTV),
- }
- result[originalImpID] = impBids
- }
-
- if deps.cfg.GenerateBidID == false {
- //making unique bid.id's per impression
- bid.ID = util.GetUniqueBidID(bid.ID, len(impBids.Bids)+1)
- }
-
- //get duration of creative
- duration, status := getBidDuration(bid, deps.reqExt, deps.impData[index].Config,
- deps.impData[index].Config[sequenceNumber-1].MaxDuration)
-
- impBids.Bids = append(impBids.Bids, &types.Bid{
- Bid: bid,
- ExtBid: ext,
- Status: status,
- Duration: int(duration),
- DealTierSatisfied: util.GetDealTierSatisfied(&ext),
- Seat: seat.Seat,
- })
- }
- }
- }
-
- //Sort Bids by Price
- for index, imp := range deps.request.Imp {
- impBids, ok := result[imp.ID]
- if ok {
- //sort bids
- sort.Slice(impBids.Bids[:], func(i, j int) bool { return impBids.Bids[i].Price > impBids.Bids[j].Price })
- deps.impData[index].Bid = impBids
- }
- }
-}
-
-// getImpressionID will return impression id and sequence number
-func (deps *ctvEndpointDeps) getImpressionID(id string) (string, int) {
- //get original impression id and sequence number
- originalImpID, sequenceNumber := util.DecodeImpressionID(id)
-
- //check originalImpID present in request or not
- index, ok := deps.impIndices[originalImpID]
- if !ok {
- //if not present check impression id present in request or not
- _, ok = deps.impIndices[id]
- if !ok {
- return id, -1
- }
- return originalImpID, 0
- }
-
- if sequenceNumber < 0 || sequenceNumber > len(deps.impData[index].Config) {
- return id, -1
- }
-
- return originalImpID, sequenceNumber
-}
-
-// doAdPodExclusions
-func (deps *ctvEndpointDeps) doAdPodExclusions() types.AdPodBids {
- defer util.TimeTrack(time.Now(), fmt.Sprintf("Tid:%v doAdPodExclusions", deps.request.ID))
-
- result := types.AdPodBids{}
- for index := 0; index < len(deps.request.Imp); index++ {
- bid := deps.impData[index].Bid
- if nil != bid && len(bid.Bids) > 0 {
- //TODO: MULTI ADPOD IMPRESSIONS
- //duration wise buckets sorted
- buckets := util.GetDurationWiseBidsBucket(bid.Bids[:])
-
- if len(buckets) == 0 {
- deps.impData[index].Error = util.DurationMismatchWarning
continue
}
- //combination generator
- comb := combination.NewCombination(
- buckets,
- uint64(deps.request.Imp[index].Video.MinDuration),
- uint64(deps.request.Imp[index].Video.MaxDuration),
- deps.impData[index].VideoExt.AdPod)
-
- //adpod generator
- adpodGenerator := response.NewAdPodGenerator(deps.request, index, buckets, comb, deps.impData[index].VideoExt.AdPod, deps.metricsEngine)
-
- adpodBids := adpodGenerator.GetAdPodBids()
- if adpodBids == nil {
- deps.impData[index].Error = util.UnableToGenerateAdPodWarning
+ adpodCtx, ok := deps.podCtx[podId]
+ if !ok {
continue
}
- adpodBids.OriginalImpID = bid.OriginalImpID
- adpodBids.SeatName = bid.SeatName
- result = append(result, adpodBids)
+ adpodCtx.CollectBid(bid, seat.Seat)
}
}
- return result
}
-/********************* Creating CTV BidResponse *********************/
-
-// createAdPodBidResponse
-func (deps *ctvEndpointDeps) createAdPodBidResponse(resp *openrtb2.BidResponse, adpods types.AdPodBids) *openrtb2.BidResponse {
- defer util.TimeTrack(time.Now(), fmt.Sprintf("Tid:%v createAdPodBidResponse", deps.request.ID))
-
- bidResp := &openrtb2.BidResponse{
- ID: resp.ID,
- Cur: resp.Cur,
- CustomData: resp.CustomData,
- SeatBid: deps.getBidResponseSeatBids(adpods),
+func (deps *ctvEndpointDeps) doAdpodAuction() {
+ for _, adpodCtx := range deps.podCtx {
+ adpodCtx.HoldAuction()
}
- return bidResp
}
-func (deps *ctvEndpointDeps) getBidResponseSeatBids(adpods types.AdPodBids) []openrtb2.SeatBid {
- seats := []openrtb2.SeatBid{}
+/********************* Creating CTV BidResponse *********************/
+
+// createAdPodBidResponse
+func (deps *ctvEndpointDeps) createAdPodBidResponse(resp *openrtb2.BidResponse) *openrtb2.BidResponse {
+ var seatbids []openrtb2.SeatBid
//append pure video request seats
for _, seat := range deps.videoSeats {
- seats = append(seats, *seat)
- }
-
- var adpodSeat *openrtb2.SeatBid
- for _, adpod := range adpods {
- if len(adpod.Bids) == 0 {
- continue
- }
-
- bid := deps.getAdPodBid(adpod)
- if bid != nil {
- if nil == adpodSeat {
- adpodSeat = &openrtb2.SeatBid{
- Seat: adpod.SeatName,
- }
- }
- adpodSeat.Bid = append(adpodSeat.Bid, *bid.Bid)
- }
- }
- if nil != adpodSeat {
- seats = append(seats, *adpodSeat)
- }
- return seats[:]
-}
-
-// getAdPodBid
-func (deps *ctvEndpointDeps) getAdPodBid(adpod *types.AdPodBid) *types.Bid {
- bid := types.Bid{
- Bid: &openrtb2.Bid{},
- }
-
- //TODO: Write single for loop to get all details
- bidID, err := uuid.NewV4()
- if nil == err {
- bid.ID = bidID.String()
- } else {
- bid.ID = adpod.Bids[0].ID
- }
-
- bid.ImpID = adpod.OriginalImpID
- bid.Price = adpod.Price
- bid.ADomain = adpod.ADomain[:]
- bid.Cat = adpod.Cat[:]
- bid.AdM = *getAdPodBidCreative(adpod, deps.cfg.GenerateBidID)
- bid.Ext = getAdPodBidExtension(adpod)
- return &bid
-}
-
-// getAdPodBidCreative get commulative adpod bid details
-func getAdPodBidCreative(adpod *types.AdPodBid, generatedBidID bool) *string {
- doc := etree.NewDocument()
- vast := doc.CreateElement(constant.VASTElement)
- sequenceNumber := 1
- var version float64 = 2.0
-
- for _, bid := range adpod.Bids {
- var newAd *etree.Element
-
- if strings.HasPrefix(bid.AdM, constant.HTTPPrefix) {
- newAd = etree.NewElement(constant.VASTAdElement)
- wrapper := newAd.CreateElement(constant.VASTWrapperElement)
- vastAdTagURI := wrapper.CreateElement(constant.VASTAdTagURIElement)
- vastAdTagURI.CreateCharData(bid.AdM)
- } else {
- adDoc := etree.NewDocument()
- if err := adDoc.ReadFromString(bid.AdM); err != nil {
- continue
- }
-
- if generatedBidID == false {
- // adjust bidid in video event trackers and update
- adjustBidIDInVideoEventTrackers(adDoc, bid.Bid)
- adm, err := adDoc.WriteToString()
- if nil != err {
- util.JLogf("ERROR, %v", err.Error())
- } else {
- bid.AdM = adm
- }
- }
-
- vastTag := adDoc.SelectElement(constant.VASTElement)
- if vastTag == nil {
- util.Logf("error:[vast_element_missing_in_adm] seat:[%s] adm:[%s]", bid.Seat, bid.AdM)
- continue
- }
-
- //Get Actual VAST Version
- bidVASTVersion, _ := strconv.ParseFloat(vastTag.SelectAttrValue(constant.VASTVersionAttribute, constant.VASTDefaultVersionStr), 64)
- version = math.Max(version, bidVASTVersion)
-
- ads := vastTag.SelectElements(constant.VASTAdElement)
- if len(ads) > 0 {
- newAd = ads[0].Copy()
- }
- }
-
- if nil != newAd {
- //creative.AdId attribute needs to be updated
- newAd.CreateAttr(constant.VASTSequenceAttribute, fmt.Sprint(sequenceNumber))
- vast.AddChild(newAd)
- sequenceNumber++
- }
- }
-
- if int(version) > len(constant.VASTVersionsStr) {
- version = constant.VASTMaxVersion
+ seatbids = append(seatbids, *seat)
}
- vast.CreateAttr(constant.VASTVersionAttribute, constant.VASTVersionsStr[int(version)])
- bidAdM, err := doc.WriteToString()
- if nil != err {
- fmt.Printf("ERROR, %v", err.Error())
- return nil
- }
- return &bidAdM
-}
-
-// getAdPodBidExtension get commulative adpod bid details
-func getAdPodBidExtension(adpod *types.AdPodBid) json.RawMessage {
- bidExt := &openrtb_ext.ExtOWBid{
- ExtBid: openrtb_ext.ExtBid{
- Prebid: &openrtb_ext.ExtBidPrebid{
- Type: openrtb_ext.BidTypeVideo,
- Video: &openrtb_ext.ExtBidPrebidVideo{},
- },
- },
- AdPod: &openrtb_ext.BidAdPodExt{
- RefBids: make([]string, len(adpod.Bids)),
- },
- }
-
- for i, bid := range adpod.Bids {
- //get unique bid id
- bidID := bid.ID
- if bid.ExtBid.Prebid != nil && bid.ExtBid.Prebid.BidId != "" {
- bidID = bid.ExtBid.Prebid.BidId
- }
-
- //adding bid id in adpod.refbids
- bidExt.AdPod.RefBids[i] = bidID
-
- //updating exact duration of adpod creative
- bidExt.Prebid.Video.Duration += int(bid.Duration)
-
- //setting bid status as winning bid
- bid.Status = constant.StatusWinningBid
- }
- rawExt, _ := json.Marshal(bidExt)
- return rawExt
-}
-
-// getDurationBasedOnDurationMatchingPolicy will return duration based on durationmatching policy
-func getDurationBasedOnDurationMatchingPolicy(duration int64, policy openrtb_ext.OWVideoAdDurationMatchingPolicy, config []*types.ImpAdPodConfig) (int64, constant.BidStatus) {
- switch policy {
- case openrtb_ext.OWExactVideoAdDurationMatching:
- tmp := util.GetNearestDuration(duration, config)
- if tmp != duration {
- return duration, constant.StatusDurationMismatch
- }
- //its and valid duration return it with StatusOK
-
- case openrtb_ext.OWRoundupVideoAdDurationMatching:
- tmp := util.GetNearestDuration(duration, config)
- if tmp == -1 {
- return duration, constant.StatusDurationMismatch
- }
- //update duration with nearest one duration
- duration = tmp
- //its and valid duration return it with StatusOK
+ for _, adpod := range deps.podCtx {
+ seatbids = append(seatbids, adpod.GetAdpodSeatBids()...)
}
- return duration, constant.StatusOK
-}
-
-/*
-getBidDuration determines the duration of video ad from given bid.
-it will try to get the actual ad duration returned by the bidder using prebid.video.duration
-if prebid.video.duration not present then uses defaultDuration passed as an argument
-if video lengths matching policy is present for request then it will validate and update duration based on policy
-*/
-func getBidDuration(bid *openrtb2.Bid, reqExt *openrtb_ext.ExtRequestAdPod, config []*types.ImpAdPodConfig, defaultDuration int64) (int64, constant.BidStatus) {
-
- // C1: Read it from bid.ext.prebid.video.duration field
- duration, err := jsonparser.GetInt(bid.Ext, "prebid", "video", "duration")
- if nil != err || duration <= 0 {
- // incase if duration is not present use impression duration directly as it is
- return defaultDuration, constant.StatusOK
- }
-
- // C2: Based on video lengths matching policy validate and return duration
- if nil != reqExt && len(reqExt.VideoAdDurationMatching) > 0 {
- return getDurationBasedOnDurationMatchingPolicy(duration, reqExt.VideoAdDurationMatching, config)
- }
-
- //default return duration which is present in bid.ext.prebid.vide.duration field
- return duration, constant.StatusOK
-}
-
-func addTargetingKey(bid *openrtb2.Bid, key openrtb_ext.TargetingKey, value string) error {
- if nil == bid {
- return errors.New("Invalid bid")
- }
-
- raw, err := jsonparser.Set(bid.Ext, []byte(strconv.Quote(value)), "prebid", "targeting", string(key))
- if nil == err {
- bid.Ext = raw
- }
- return err
-}
-
-func adjustBidIDInVideoEventTrackers(doc *etree.Document, bid *openrtb2.Bid) {
- // adjusment: update bid.id with ctv module generated bid.id
- creatives := events.FindCreatives(doc)
- for _, creative := range creatives {
- trackingEvents := creative.FindElements("TrackingEvents/Tracking")
- if nil != trackingEvents {
- // update bidid= value with ctv generated bid id for this bid
- for _, trackingEvent := range trackingEvents {
- u, e := url.Parse(trackingEvent.Text())
- if nil == e {
- values, e := url.ParseQuery(u.RawQuery)
- // only do replacment if operId=8
- if nil == e && nil != values["bidid"] && nil != values["operId"] && values["operId"][0] == "8" {
- values.Set("bidid", bid.ID)
- } else {
- continue
- }
-
- //OTT-183: Fix
- if nil != values["operId"] && values["operId"][0] == "8" {
- operID := values.Get("operId")
- values.Del("operId")
- values.Add("_operId", operID) // _ (underscore) will keep it as first key
- }
-
- u.RawQuery = values.Encode() // encode sorts query params by key. _ must be first (assuing no other query param with _)
- // replace _operId with operId
- u.RawQuery = strings.ReplaceAll(u.RawQuery, "_operId", "operId")
- trackingEvent.SetText(u.String())
- }
- }
- }
- }
-}
-
-// ConvertAPRCToNBRC converts the aprc to NonBidStatusCode
-func ConvertAPRCToNBRC(bidStatus int64) *openrtb3.NoBidReason {
- var nbrCode openrtb3.NoBidReason
-
- switch bidStatus {
- case constant.StatusOK:
- nbrCode = nbr.LossBidLostToHigherBid
- case constant.StatusCategoryExclusion:
- nbrCode = exchange.ResponseRejectedCreativeCategoryExclusions
- case constant.StatusDomainExclusion:
- nbrCode = exchange.ResponseRejectedCreativeAdvertiserExclusions
- case constant.StatusDurationMismatch:
- nbrCode = exchange.ResponseRejectedInvalidCreative
-
- default:
- return nil
- }
- return &nbrCode
-}
-
-// recordRejectedAdPodBids records the bids lost in ad-pod auction using metricsEngine
-func (deps *ctvEndpointDeps) recordRejectedAdPodBids(pubID string) {
-
- for _, imp := range deps.impData {
- if nil != imp.Bid && len(imp.Bid.Bids) > 0 {
- for _, bid := range imp.Bid.Bids {
- if bid.Status != constant.StatusWinningBid {
- reason := ConvertAPRCToNBRC(bid.Status)
- if reason == nil {
- continue
- }
- rejReason := strconv.FormatInt(int64(*reason), 10)
- deps.metricsEngine.RecordRejectedBids(pubID, bid.Seat, rejReason)
- }
- }
- }
+ bidResp := &openrtb2.BidResponse{
+ ID: resp.ID,
+ Cur: resp.Cur,
+ CustomData: resp.CustomData,
+ SeatBid: deps.combineBidsSeatWise(seatbids),
}
+ return bidResp
}
// getBidResponseExt prepare and return the bidresponse extension
func (deps *ctvEndpointDeps) getBidResponseExt(resp *openrtb2.BidResponse) (data json.RawMessage) {
-
var err error
-
adpodExt := types.BidResponseAdPodExt{
Response: *resp,
- Config: make(map[string]*types.ImpData, len(deps.impData)),
+ Config: make(map[string]*types.ImpData),
}
- for index, imp := range deps.impData {
- if nil != imp.VideoExt && nil != imp.VideoExt.AdPod {
- adpodExt.Config[deps.request.Imp[index].ID] = imp
+ for podId, adpodCtx := range deps.podCtx {
+ ext := adpodCtx.GetAdpodExtension(deps.impPartnerBlockedTagIDMap)
+ if ext != nil {
+ adpodExt.Config[podId] = ext
}
}
@@ -1234,31 +713,27 @@ func (deps *ctvEndpointDeps) getBidResponseExt(resp *openrtb2.BidResponse) (data
return nil
}
}
- return data[:]
+ return data
}
-// setBidExtParams function sets the prebid.video.duration and adpod.aprc parameters
-func (deps *ctvEndpointDeps) setBidExtParams() {
-
- for _, imp := range deps.impData {
- if imp.Bid != nil {
- for _, bid := range imp.Bid.Bids {
-
- //update adm
- //bid.AdM = constant.VASTDefaultTag
+func (deps *ctvEndpointDeps) combineBidsSeatWise(seatBids []openrtb2.SeatBid) []openrtb2.SeatBid {
+ if len(seatBids) == 0 {
+ return nil
+ }
- //add duration value
- raw, err := jsonparser.Set(bid.Ext, []byte(strconv.Itoa(int(bid.Duration))), "prebid", "video", "duration")
- if nil == err {
- bid.Ext = raw
- }
+ seatMap := map[string][]openrtb2.Bid{}
+ for _, seatBid := range seatBids {
+ seatMap[seatBid.Seat] = append(seatMap[seatBid.Seat], seatBid.Bid...)
+ }
- //add bid filter reason value
- raw, err = jsonparser.Set(bid.Ext, []byte(strconv.FormatInt(bid.Status, 10)), "adpod", "aprc")
- if nil == err {
- bid.Ext = raw
- }
- }
+ var responseSeatBids []openrtb2.SeatBid
+ for seat, bids := range seatMap {
+ seat := openrtb2.SeatBid{
+ Bid: bids,
+ Seat: seat,
}
+ responseSeatBids = append(responseSeatBids, seat)
}
+
+ return responseSeatBids
}
diff --git a/endpoints/openrtb2/ctv_auction_test.go b/endpoints/openrtb2/ctv_auction_test.go
index 01a161a87e9..e50d3b85315 100644
--- a/endpoints/openrtb2/ctv_auction_test.go
+++ b/endpoints/openrtb2/ctv_auction_test.go
@@ -1,43 +1,24 @@
package openrtb2
import (
+ "bytes"
"encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "os"
"testing"
+ "github.com/julienschmidt/httprouter"
"github.com/prebid/openrtb/v20/openrtb2"
- "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/constant"
+ "github.com/prebid/prebid-server/v2/config"
+ "github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/adpod"
"github.com/prebid/prebid-server/v2/endpoints/openrtb2/ctv/types"
"github.com/prebid/prebid-server/v2/metrics"
"github.com/prebid/prebid-server/v2/openrtb_ext"
+ "github.com/prebid/prebid-server/v2/util/ptrutil"
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
)
-func TestAddTargetingKeys(t *testing.T) {
- var tests = []struct {
- scenario string // Testcase scenario
- key string
- value string
- bidExt string
- expect map[string]string
- }{
- {scenario: "key_not_exists", key: "hb_pb_cat_dur", value: "some_value", bidExt: `{"prebid":{"targeting":{}}}`, expect: map[string]string{"hb_pb_cat_dur": "some_value"}},
- {scenario: "key_already_exists", key: "hb_pb_cat_dur", value: "new_value", bidExt: `{"prebid":{"targeting":{"hb_pb_cat_dur":"old_value"}}}`, expect: map[string]string{"hb_pb_cat_dur": "new_value"}},
- }
- for _, test := range tests {
- t.Run(test.scenario, func(t *testing.T) {
- bid := new(openrtb2.Bid)
- bid.Ext = []byte(test.bidExt)
- key := openrtb_ext.TargetingKey(test.key)
- assert.Nil(t, addTargetingKey(bid, key, test.value))
- extBid := openrtb_ext.ExtBid{}
- json.Unmarshal(bid.Ext, &extBid)
- assert.Equal(t, test.expect, extBid.Prebid.Targeting)
- })
- }
- assert.Equal(t, "Invalid bid", addTargetingKey(nil, openrtb_ext.HbCategoryDurationKey, "some value").Error())
-}
-
func TestFilterImpsVastTagsByDuration(t *testing.T) {
type inputParams struct {
request *openrtb2.BidRequest
@@ -250,322 +231,13 @@ func TestFilterImpsVastTagsByDuration(t *testing.T) {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
- deps := ctvEndpointDeps{request: tc.input.request, impData: tc.input.impData}
+ deps := ctvEndpointDeps{request: tc.input.request}
deps.readImpExtensionsAndTags()
outputBids := tc.input.generatedRequest
deps.filterImpsVastTagsByDuration(outputBids)
assert.Equal(t, tc.expectedOutput.reqs, *outputBids, "Expected length of impressions array was %d but actual was %d", tc.expectedOutput.reqs, outputBids)
-
- for i, datum := range deps.impData {
- assert.Equal(t, tc.expectedOutput.blockedTags[i], datum.BlockedVASTTags, "Expected and actual impData was different")
- }
- })
- }
-}
-
-func TestGetBidDuration(t *testing.T) {
- type args struct {
- bid *openrtb2.Bid
- reqExt *openrtb_ext.ExtRequestAdPod
- config []*types.ImpAdPodConfig
- defaultDuration int64
- }
- type want struct {
- duration int64
- status constant.BidStatus
- }
- var tests = []struct {
- name string
- args args
- want want
- expect int
- }{
- {
- name: "nil_bid_ext",
- args: args{
- bid: &openrtb2.Bid{},
- reqExt: nil,
- config: nil,
- defaultDuration: 100,
- },
- want: want{
- duration: 100,
- status: constant.StatusOK,
- },
- },
- {
- name: "use_default_duration",
- args: args{
- bid: &openrtb2.Bid{
- Ext: json.RawMessage(`{"tmp":123}`),
- },
- reqExt: nil,
- config: nil,
- defaultDuration: 100,
- },
- want: want{
- duration: 100,
- status: constant.StatusOK,
- },
- },
- {
- name: "invalid_duration_in_bid_ext",
- args: args{
- bid: &openrtb2.Bid{
- Ext: json.RawMessage(`{"prebid":{"video":{"duration":"invalid"}}}`),
- },
- reqExt: nil,
- config: nil,
- defaultDuration: 100,
- },
- want: want{
- duration: 100,
- status: constant.StatusOK,
- },
- },
- {
- name: "0sec_duration_in_bid_ext",
- args: args{
- bid: &openrtb2.Bid{
- Ext: json.RawMessage(`{"prebid":{"video":{"duration":0}}}`),
- },
- reqExt: nil,
- config: nil,
- defaultDuration: 100,
- },
- want: want{
- duration: 100,
- status: constant.StatusOK,
- },
- },
- {
- name: "negative_duration_in_bid_ext",
- args: args{
- bid: &openrtb2.Bid{
- Ext: json.RawMessage(`{"prebid":{"video":{"duration":-30}}}`),
- },
- reqExt: nil,
- config: nil,
- defaultDuration: 100,
- },
- want: want{
- duration: 100,
- status: constant.StatusOK,
- },
- },
- {
- name: "30sec_duration_in_bid_ext",
- args: args{
- bid: &openrtb2.Bid{
- Ext: json.RawMessage(`{"prebid":{"video":{"duration":30}}}`),
- },
- reqExt: nil,
- config: nil,
- defaultDuration: 100,
- },
- want: want{
- duration: 30,
- status: constant.StatusOK,
- },
- },
- {
- name: "duration_matching_empty",
- args: args{
- bid: &openrtb2.Bid{
- Ext: json.RawMessage(`{"prebid":{"video":{"duration":30}}}`),
- },
- reqExt: &openrtb_ext.ExtRequestAdPod{
- VideoAdDurationMatching: "",
- },
- config: nil,
- defaultDuration: 100,
- },
- want: want{
- duration: 30,
- status: constant.StatusOK,
- },
- },
- {
- name: "duration_matching_exact",
- args: args{
- bid: &openrtb2.Bid{
- Ext: json.RawMessage(`{"prebid":{"video":{"duration":30}}}`),
- },
- reqExt: &openrtb_ext.ExtRequestAdPod{
- VideoAdDurationMatching: openrtb_ext.OWExactVideoAdDurationMatching,
- },
- config: []*types.ImpAdPodConfig{
- {MaxDuration: 10},
- {MaxDuration: 20},
- {MaxDuration: 30},
- {MaxDuration: 40},
- },
- defaultDuration: 100,
- },
- want: want{
- duration: 30,
- status: constant.StatusOK,
- },
- },
- {
- name: "duration_matching_exact_not_present",
- args: args{
- bid: &openrtb2.Bid{
- Ext: json.RawMessage(`{"prebid":{"video":{"duration":35}}}`),
- },
- reqExt: &openrtb_ext.ExtRequestAdPod{
- VideoAdDurationMatching: openrtb_ext.OWExactVideoAdDurationMatching,
- },
- config: []*types.ImpAdPodConfig{
- {MaxDuration: 10},
- {MaxDuration: 20},
- {MaxDuration: 30},
- {MaxDuration: 40},
- },
- defaultDuration: 100,
- },
- want: want{
- duration: 35,
- status: constant.StatusDurationMismatch,
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- duration, status := getBidDuration(tt.args.bid, tt.args.reqExt, tt.args.config, tt.args.defaultDuration)
- assert.Equal(t, tt.want.duration, duration)
- assert.Equal(t, tt.want.status, status)
- })
- }
-}
-
-func Test_getDurationBasedOnDurationMatchingPolicy(t *testing.T) {
- type args struct {
- duration int64
- policy openrtb_ext.OWVideoAdDurationMatchingPolicy
- config []*types.ImpAdPodConfig
- }
- type want struct {
- duration int64
- status constant.BidStatus
- }
- tests := []struct {
- name string
- args args
- want want
- }{
- {
- name: "empty_duration_policy",
- args: args{
- duration: 10,
- policy: "",
- config: []*types.ImpAdPodConfig{
- {MaxDuration: 10},
- {MaxDuration: 20},
- {MaxDuration: 30},
- {MaxDuration: 40},
- },
- },
- want: want{
- duration: 10,
- status: constant.StatusOK,
- },
- },
- {
- name: "policy_exact",
- args: args{
- duration: 10,
- policy: openrtb_ext.OWExactVideoAdDurationMatching,
- config: []*types.ImpAdPodConfig{
- {MaxDuration: 10},
- {MaxDuration: 20},
- {MaxDuration: 30},
- {MaxDuration: 40},
- },
- },
- want: want{
- duration: 10,
- status: constant.StatusOK,
- },
- },
- {
- name: "policy_exact_didnot_match",
- args: args{
- duration: 15,
- policy: openrtb_ext.OWExactVideoAdDurationMatching,
- config: []*types.ImpAdPodConfig{
- {MaxDuration: 10},
- {MaxDuration: 20},
- {MaxDuration: 30},
- {MaxDuration: 40},
- },
- },
- want: want{
- duration: 15,
- status: constant.StatusDurationMismatch,
- },
- },
- {
- name: "policy_roundup_exact",
- args: args{
- duration: 20,
- policy: openrtb_ext.OWRoundupVideoAdDurationMatching,
- config: []*types.ImpAdPodConfig{
- {MaxDuration: 10},
- {MaxDuration: 20},
- {MaxDuration: 30},
- {MaxDuration: 40},
- },
- },
- want: want{
- duration: 20,
- status: constant.StatusOK,
- },
- },
- {
- name: "policy_roundup",
- args: args{
- duration: 25,
- policy: openrtb_ext.OWRoundupVideoAdDurationMatching,
- config: []*types.ImpAdPodConfig{
- {MaxDuration: 10},
- {MaxDuration: 20},
- {MaxDuration: 30},
- {MaxDuration: 40},
- },
- },
- want: want{
- duration: 30,
- status: constant.StatusOK,
- },
- },
- {
- name: "policy_roundup_didnot_match",
- args: args{
- duration: 45,
- policy: openrtb_ext.OWRoundupVideoAdDurationMatching,
- config: []*types.ImpAdPodConfig{
- {MaxDuration: 10},
- {MaxDuration: 20},
- {MaxDuration: 30},
- {MaxDuration: 40},
- },
- },
- want: want{
- duration: 45,
- status: constant.StatusDurationMismatch,
- },
- },
-
- // TODO: Add test cases.
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- duration, status := getDurationBasedOnDurationMatchingPolicy(tt.args.duration, tt.args.policy, tt.args.config)
- assert.Equal(t, tt.want.duration, duration)
- assert.Equal(t, tt.want.status, status)
})
}
}
@@ -596,7 +268,6 @@ func TestCreateAdPodBidResponse(t *testing.T) {
ID: "id1",
Cur: "USD",
CustomData: "custom",
- SeatBid: make([]openrtb2.SeatBid, 0),
},
},
},
@@ -608,75 +279,13 @@ func TestCreateAdPodBidResponse(t *testing.T) {
ID: "1",
},
}
- actual := deps.createAdPodBidResponse(tt.args.resp, nil)
+ actual := deps.createAdPodBidResponse(tt.args.resp)
assert.Equal(t, tt.want.resp, actual)
})
}
}
-func TestSetBidExtParams(t *testing.T) {
- type args struct {
- impData []*types.ImpData
- }
- type want struct {
- impData []*types.ImpData
- }
- tests := []struct {
- name string
- args args
- want want
- }{
- {
- name: "sample",
- args: args{
- impData: []*types.ImpData{
- {
- Bid: &types.AdPodBid{
- Bids: []*types.Bid{
- {
- Bid: &openrtb2.Bid{
- Ext: json.RawMessage(`{"prebid": {"video": {} },"adpod": {}}`),
- },
- Duration: 10,
- Status: 1,
- },
- },
- },
- },
- },
- },
- want: want{
- impData: []*types.ImpData{
- {
- Bid: &types.AdPodBid{
- Bids: []*types.Bid{
- {
- Bid: &openrtb2.Bid{
- Ext: json.RawMessage(`{"prebid": {"video": {"duration":10} },"adpod": {"aprc":1}}`),
- },
- Duration: 10,
- Status: 1,
- },
- },
- },
- },
- },
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
-
- deps := ctvEndpointDeps{
- impData: tt.args.impData,
- }
- deps.setBidExtParams()
- assert.Equal(t, tt.want.impData[0].Bid.Bids[0].Ext, deps.impData[0].Bid.Bids[0].Ext)
- })
- }
-}
-
func TestGetAdPodExt(t *testing.T) {
type args struct {
resp *openrtb2.BidResponse
@@ -742,23 +351,25 @@ func TestGetAdPodExt(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- deps := ctvEndpointDeps{
- impData: []*types.ImpData{
+ req := &openrtb2.BidRequest{
+ Imp: []openrtb2.Imp{
{
- ImpID: "imp1",
- VideoExt: &openrtb_ext.ExtVideoAdPod{
- AdPod: &openrtb_ext.VideoAdPod{},
- },
- Bid: &types.AdPodBid{
- Bids: []*types.Bid{},
- },
+ ID: "imp1",
+ Video: &openrtb2.Video{},
},
},
- request: &openrtb2.BidRequest{
- Imp: []openrtb2.Imp{
- {ID: "imp1"},
- },
+ }
+
+ videoExt := openrtb_ext.ExtVideoAdPod{
+ AdPod: &openrtb_ext.VideoAdPod{},
+ }
+ dynamicAdpod := adpod.NewDynamicAdpod("test-pub", req.Imp[0], videoExt, &metrics.MetricsEngineMock{}, nil)
+
+ deps := ctvEndpointDeps{
+ podCtx: map[string]adpod.Adpod{
+ "imp1": dynamicAdpod,
},
+ request: req,
}
actual := deps.getBidResponseExt(tt.args.resp)
assert.Equal(t, string(tt.want.data), string(actual))
@@ -766,122 +377,220 @@ func TestGetAdPodExt(t *testing.T) {
}
}
-func TestRecordAdPodRejectedBids(t *testing.T) {
-
- type args struct {
- bids types.AdPodBid
+func TestGetAdpodConfigFromExtension(t *testing.T) {
+ type fields struct {
+ endpointDeps endpointDeps
+ request *openrtb2.BidRequest
+ reqExt *openrtb_ext.ExtRequestAdPod
+ videoSeats []*openrtb2.SeatBid
+ impsExtPrebidBidder map[string]map[string]map[string]interface{}
+ impPartnerBlockedTagIDMap map[string]map[string][]string
+ podCtx map[string]adpod.Adpod
+ labels metrics.Labels
}
-
- type want struct {
- expectedCalls int
+ type args struct {
+ imp openrtb2.Imp
}
-
tests := []struct {
- description string
- args args
- want want
+ name string
+ fields fields
+ args args
+ want openrtb_ext.ExtVideoAdPod
+ wantErr bool
}{
{
- description: "multiple rejected bids",
+ name: "Adpod_Config_available_in_the_impression_extension",
+ fields: fields{
+ endpointDeps: endpointDeps{},
+ request: &openrtb2.BidRequest{},
+ },
args: args{
- bids: types.AdPodBid{
- Bids: []*types.Bid{
- {
- Bid: &openrtb2.Bid{},
- Status: constant.StatusCategoryExclusion,
- Seat: "pubmatic",
- },
- {
- Bid: &openrtb2.Bid{},
- Status: constant.StatusWinningBid,
- Seat: "pubmatic",
- },
- {
- Bid: &openrtb2.Bid{},
- Status: constant.StatusOK,
- Seat: "pubmatic",
- },
- {
- Bid: &openrtb2.Bid{},
- Status: 100,
- Seat: "pubmatic",
- },
+ imp: openrtb2.Imp{
+ ID: "imp1",
+ TagID: "/Test/unit",
+ Video: &openrtb2.Video{
+ MinDuration: 10,
+ MaxDuration: 30,
+ Ext: json.RawMessage(`{"offset":20,"adpod":{"minads":2,"maxads":3,"adminduration":30,"admaxduration":40,"excladv":100,"excliabcat":100}}`),
},
},
},
- want: want{
- expectedCalls: 2,
+ want: openrtb_ext.ExtVideoAdPod{
+ Offset: ptrutil.ToPtr(20),
+ AdPod: &openrtb_ext.VideoAdPod{
+ MinAds: ptrutil.ToPtr(2),
+ MaxAds: ptrutil.ToPtr(3),
+ MinDuration: ptrutil.ToPtr(30),
+ MaxDuration: ptrutil.ToPtr(40),
+ AdvertiserExclusionPercent: ptrutil.ToPtr(100),
+ IABCategoryExclusionPercent: ptrutil.ToPtr(100),
+ },
},
},
- }
-
- for _, test := range tests {
- me := &metrics.MetricsEngineMock{}
- me.On("RecordRejectedBids", mock.Anything, mock.Anything, mock.Anything).Return()
-
- deps := ctvEndpointDeps{
- endpointDeps: endpointDeps{
- metricsEngine: me,
+ {
+ name: "video_extension_contains_values_other_than_adpod",
+ fields: fields{
+ endpointDeps: endpointDeps{},
+ request: &openrtb2.BidRequest{},
},
- impData: []*types.ImpData{
- {
- Bid: &test.args.bids,
+ args: args{
+ imp: openrtb2.Imp{
+ ID: "imp1",
+ TagID: "/Test/unit",
+ Video: &openrtb2.Video{
+ MinDuration: 10,
+ MaxDuration: 30,
+ Ext: json.RawMessage(`{"random":20}`),
+ },
},
},
- }
-
- deps.recordRejectedAdPodBids("pub_001")
- me.AssertNumberOfCalls(t, "RecordRejectedBids", test.want.expectedCalls)
- }
-}
-
-func TestGetAdPodBidCreative(t *testing.T) {
- type args struct {
- adpod *types.AdPodBid
- generatedBidID bool
- }
- tests := []struct {
- name string
- args args
- want string
- }{
+ want: openrtb_ext.ExtVideoAdPod{},
+ },
{
- name: "VAST_element_missing_in_adm",
+ name: "adpod_configuration_present_in_request_extension",
+ fields: fields{
+ endpointDeps: endpointDeps{},
+ request: &openrtb2.BidRequest{},
+ reqExt: &openrtb_ext.ExtRequestAdPod{
+ VideoAdPod: &openrtb_ext.VideoAdPod{
+ MinAds: ptrutil.ToPtr(1),
+ MaxAds: ptrutil.ToPtr(3),
+ MinDuration: ptrutil.ToPtr(10),
+ MaxDuration: ptrutil.ToPtr(30),
+ AdvertiserExclusionPercent: ptrutil.ToPtr(100),
+ IABCategoryExclusionPercent: ptrutil.ToPtr(100),
+ },
+ },
+ },
args: args{
- adpod: &types.AdPodBid{
- Bids: []*types.Bid{
- {
- Bid: &openrtb2.Bid{
- AdM: "any_creative_without_vast",
- },
- },
+ imp: openrtb2.Imp{
+ ID: "imp1",
+ TagID: "/Test/unit",
+ Video: &openrtb2.Video{
+ MinDuration: 10,
+ MaxDuration: 30,
},
},
- generatedBidID: false,
},
- want: "",
+ want: openrtb_ext.ExtVideoAdPod{
+ Offset: ptrutil.ToPtr(0),
+ AdPod: &openrtb_ext.VideoAdPod{
+ MinAds: ptrutil.ToPtr(1),
+ MaxAds: ptrutil.ToPtr(3),
+ MinDuration: ptrutil.ToPtr(5),
+ MaxDuration: ptrutil.ToPtr(15),
+ AdvertiserExclusionPercent: ptrutil.ToPtr(100),
+ IABCategoryExclusionPercent: ptrutil.ToPtr(100),
+ },
+ },
},
{
- name: "VAST_element_present_in_adm",
+ name: "adpod_configuration_not_availbale_in_any_location",
+ fields: fields{
+ endpointDeps: endpointDeps{},
+ request: &openrtb2.BidRequest{},
+ },
args: args{
- adpod: &types.AdPodBid{
- Bids: []*types.Bid{
- {
- Bid: &openrtb2.Bid{
- AdM: "url_creative",
- },
- },
+ imp: openrtb2.Imp{
+ ID: "imp1",
+ TagID: "/Test/unit",
+ Video: &openrtb2.Video{
+ MinDuration: 10,
+ MaxDuration: 30,
},
},
- generatedBidID: false,
},
- want: "",
+ want: openrtb_ext.ExtVideoAdPod{},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ deps := &ctvEndpointDeps{
+ endpointDeps: tt.fields.endpointDeps,
+ request: tt.fields.request,
+ reqExt: tt.fields.reqExt,
+ videoSeats: tt.fields.videoSeats,
+ impsExtPrebidBidder: tt.fields.impsExtPrebidBidder,
+ impPartnerBlockedTagIDMap: tt.fields.impPartnerBlockedTagIDMap,
+ podCtx: tt.fields.podCtx,
+ labels: tt.fields.labels,
+ }
+ got, err := deps.readVideoAdPodExt(tt.args.imp)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("ctvEndpointDeps.getAdpodConfigFromExtension() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ assert.Equal(t, tt.want, got, "Adpod config does not match")
+ })
+ }
+}
+
+func TestCTVAuctionEndpointAdpod(t *testing.T) {
+ type args struct {
+ w http.ResponseWriter
+ r *http.Request
+ params httprouter.Params
+ }
+ tests := []struct {
+ name string
+ directory string
+ fileName string
+ args args
+ modifyResponse func(resp1, resp2 json.RawMessage) (json.RawMessage, error)
+ }{
+ {
+ name: "dynamic_adpod_request",
+ args: args{},
+ directory: "sample-requests/ctv/valid-requests/",
+ fileName: "dynamic-adpod.json",
+ },
+ {
+ name: "structured_adpod_request",
+ args: args{},
+ directory: "sample-requests/ctv/valid-requests/",
+ fileName: "structured-adpod.json",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got := getAdPodBidCreative(tt.args.adpod, tt.args.generatedBidID)
- assert.Equalf(t, tt.want, *got, "found incorrect creative")
+ // Read test case and unmarshal
+ fileJsonData, err := os.ReadFile(tt.directory + tt.fileName)
+ assert.NoError(t, err, "Failed to fetch a valid request: %v. Test file: %s", err, tt.fileName)
+
+ test := ctvtestCase{}
+ assert.NoError(t, json.Unmarshal(fileJsonData, &test), "Failed to unmarshal data from file: %s. Error: %v", tt.fileName, err)
+
+ tt.args.r = httptest.NewRequest("POST", "/video/json", bytes.NewReader(test.BidRequest))
+ recorder := httptest.NewRecorder()
+
+ cfg := &config.Configuration{
+ MaxRequestSize: maxSize,
+ GDPR: config.GDPR{Enabled: true},
+ }
+ if test.Config != nil {
+ cfg.BlacklistedApps = test.Config.BlacklistedApps
+ cfg.BlacklistedAppMap = test.Config.getBlacklistedAppMap()
+ cfg.AccountRequired = test.Config.AccountRequired
+ }
+
+ CTVAuctionEndpoint, _, mockBidServers, mockCurrencyRatesServer, err := ctvTestEndpoint(test, cfg)
+ assert.NoError(t, err, "Error while calling ctv auction endpoint %v", err)
+
+ CTVAuctionEndpoint(recorder, tt.args.r, tt.args.params)
+
+ // Close servers
+ for _, mockBidServer := range mockBidServers {
+ mockBidServer.Close()
+ }
+ mockCurrencyRatesServer.Close()
+
+ // if assert.Equal(t, test.ExpectedReturnCode, recorder.Code, "Expected status %d. Got %d. CTV test file: %s", http.StatusOK, recorder.Code, tt.fileName) {
+ // if test.ExpectedReturnCode == http.StatusOK {
+ // assert.JSONEq(t, string(test.ExpectedBidResponse), recorder.Body.String(), "Not the expected response. Test file: %s", tt.fileName)
+ // } else {
+ // assert.Equal(t, test.ExpectedErrorMessage, recorder.Body.String(), tt.fileName)
+ // }
+ // }
})
}
}
diff --git a/endpoints/openrtb2/ctv_test_utils.go b/endpoints/openrtb2/ctv_test_utils.go
new file mode 100644
index 00000000000..492f2f728df
--- /dev/null
+++ b/endpoints/openrtb2/ctv_test_utils.go
@@ -0,0 +1,317 @@
+package openrtb2
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "strconv"
+ "time"
+
+ "github.com/julienschmidt/httprouter"
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v2/adapters"
+ analyticsBuild "github.com/prebid/prebid-server/v2/analytics/build"
+ "github.com/prebid/prebid-server/v2/config"
+ "github.com/prebid/prebid-server/v2/currency"
+ "github.com/prebid/prebid-server/v2/errortypes"
+ "github.com/prebid/prebid-server/v2/exchange"
+ "github.com/prebid/prebid-server/v2/experiment/adscert"
+ "github.com/prebid/prebid-server/v2/floors"
+ "github.com/prebid/prebid-server/v2/hooks"
+ "github.com/prebid/prebid-server/v2/macros"
+ "github.com/prebid/prebid-server/v2/metrics"
+ metricsConfig "github.com/prebid/prebid-server/v2/metrics/config"
+ "github.com/prebid/prebid-server/v2/openrtb_ext"
+ "github.com/prebid/prebid-server/v2/stored_requests"
+ "github.com/prebid/prebid-server/v2/stored_requests/backends/empty_fetcher"
+)
+
+type ctvtestCase struct {
+ Description string `json:"description"`
+ Config *ctvtestConfigValues `json:"config"`
+ BidRequest json.RawMessage `json:"mockBidRequest"`
+ ExpectedValidatedBidReq json.RawMessage `json:"expectedValidatedBidRequest"`
+ ExpectedReturnCode int `json:"expectedReturnCode,omitempty"`
+ ExpectedErrorMessage string `json:"expectedErrorMessage"`
+ Query string `json:"query"`
+ planBuilder hooks.ExecutionPlanBuilder
+ ExpectedBidResponse json.RawMessage `json:"expectedBidResponse"`
+}
+
+type ctvtestConfigValues struct {
+ AccountRequired bool `json:"accountRequired"`
+ AliasJSON string `json:"aliases"`
+ BlacklistedApps []string `json:"blacklistedApps"`
+ DisabledAdapters []string `json:"disabledAdapters"`
+ CurrencyRates map[string]map[string]float64 `json:"currencyRates"`
+ MockBidders []ctvMockBidderHandler `json:"mockBidders"`
+ RealParamsValidator bool `json:"realParamsValidator"`
+ AssertBidExt bool `json:"assertbidext"`
+}
+
+func (tc *ctvtestConfigValues) getBlacklistedAppMap() map[string]bool {
+ var blacklistedAppMap map[string]bool
+
+ if len(tc.BlacklistedApps) > 0 {
+ blacklistedAppMap = make(map[string]bool, len(tc.BlacklistedApps))
+ for _, app := range tc.BlacklistedApps {
+ blacklistedAppMap[app] = true
+ }
+ }
+ return blacklistedAppMap
+}
+
+type ctvMockBidderHandler struct {
+ BidderName string `json:"bidderName"`
+ Currency string `json:"currency"`
+ Bids []mockBid `json:"bids"`
+}
+
+type mockBid struct {
+ ImpId string `json:"impid"`
+ Price float64 `json:"price"`
+ DealID string `json:"dealid,omitempty"`
+ Cat []string `json:"cat,omitempty"`
+ ADomain []string `json:"adomain,omitempty"`
+}
+
+func (b ctvMockBidderHandler) bid(w http.ResponseWriter, req *http.Request) {
+ // Read request Body
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(req.Body)
+
+ // Unmarshal exit if error
+ var openrtb2Request openrtb2.BidRequest
+ if err := json.Unmarshal(buf.Bytes(), &openrtb2Request); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ var openrtb2ImpExt map[string]json.RawMessage
+ if err := json.Unmarshal(openrtb2Request.Imp[0].Ext, &openrtb2ImpExt); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ _, exists := openrtb2ImpExt["bidder"]
+ if !exists {
+ http.Error(w, "This request is not meant for this bidder", http.StatusBadRequest)
+ return
+ }
+
+ var bids []openrtb2.Bid
+ var seq int
+ for _, bid := range b.Bids {
+ bids = append(bids, openrtb2.Bid{
+ ID: b.BidderName + "-bid-" + strconv.Itoa(seq),
+ ImpID: bid.ImpId,
+ Price: bid.Price,
+ AdM: "",
+ DealID: bid.DealID,
+ Cat: bid.Cat,
+ ADomain: bid.ADomain,
+ })
+ seq++
+
+ }
+
+ // Create bid service openrtb2.BidResponse with one bid according to JSON test file values
+ var serverResponseObject = openrtb2.BidResponse{
+ ID: openrtb2Request.ID,
+ Cur: b.Currency,
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: bids,
+ Seat: b.BidderName,
+ },
+ },
+ }
+
+ // Marshal the response and http write it
+ serverJsonResponse, err := json.Marshal(&serverResponseObject)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write(serverJsonResponse)
+ return
+}
+
+func ctvTestEndpoint(test ctvtestCase, cfg *config.Configuration) (httprouter.Handle, *exchangeTestWrapper, []*httptest.Server, *httptest.Server, error) {
+ if test.Config == nil {
+ test.Config = &ctvtestConfigValues{}
+ }
+
+ var paramValidator openrtb_ext.BidderParamValidator
+ if test.Config.RealParamsValidator {
+ var err error
+ paramValidator, err = openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+ } else {
+ paramValidator = mockBidderParamValidator{}
+ }
+
+ bidderInfos := getBidderInfos(test.Config.DisabledAdapters, openrtb_ext.CoreBidderNames())
+ bidderMap := exchange.GetActiveBidders(bidderInfos)
+ disabledBidders := exchange.GetDisabledBidderWarningMessages(bidderInfos)
+ met := &metricsConfig.NilMetricsEngine{}
+ mockFetcher := empty_fetcher.EmptyFetcher{}
+
+ // Adapter map with mock adapters needed to run JSON test cases
+ adapterMap := make(map[openrtb_ext.BidderName]exchange.AdaptedBidder, 0)
+ mockBidServersArray := make([]*httptest.Server, 0, 3)
+
+ // Mock prebid Server's currency converter, instantiate and start
+ mockCurrencyConversionService := mockCurrencyRatesClient{
+ currencyInfo{
+ Conversions: test.Config.CurrencyRates,
+ },
+ }
+ mockCurrencyRatesServer := httptest.NewServer(http.HandlerFunc(mockCurrencyConversionService.handle))
+
+ testExchange, mockBidServersArray := testCTVExchange(test.Config, adapterMap, mockBidServersArray, mockCurrencyRatesServer, bidderInfos, cfg, met, mockFetcher)
+
+ planBuilder := test.planBuilder
+ if planBuilder == nil {
+ planBuilder = hooks.EmptyPlanBuilder{}
+ }
+
+ endpoint, err := NewCTVEndpoint(
+ testExchange,
+ paramValidator,
+ &mockStoredReqFetcher{},
+ &mockStoredReqFetcher{},
+ &mockAccountFetcher{},
+ cfg,
+ met,
+ analyticsBuild.New(&config.Analytics{}),
+ disabledBidders,
+ []byte(test.Config.AliasJSON),
+ bidderMap,
+ planBuilder,
+ nil,
+ )
+
+ return endpoint, testExchange.(*exchangeTestWrapper), mockBidServersArray, mockCurrencyRatesServer, err
+}
+
+func testCTVExchange(testCfg *ctvtestConfigValues, adapterMap map[openrtb_ext.BidderName]exchange.AdaptedBidder, mockBidServersArray []*httptest.Server, mockCurrencyRatesServer *httptest.Server, bidderInfos config.BidderInfos, cfg *config.Configuration, met metrics.MetricsEngine, mockFetcher stored_requests.CategoryFetcher) (exchange.Exchange, []*httptest.Server) {
+ if len(testCfg.MockBidders) == 0 {
+ testCfg.MockBidders = append(testCfg.MockBidders, ctvMockBidderHandler{BidderName: "pubmatic", Currency: "USD", Bids: []mockBid{
+ {
+ ImpId: "imp1",
+ Price: 0,
+ },
+ }})
+ }
+ for _, mockBidder := range testCfg.MockBidders {
+ bidServer := httptest.NewServer(http.HandlerFunc(mockBidder.bid))
+ bidderAdapter := ctvMockAdapter{mockServerURL: bidServer.URL}
+ bidderName := openrtb_ext.BidderName(mockBidder.BidderName)
+
+ adapterMap[bidderName] = exchange.AdaptBidder(bidderAdapter, bidServer.Client(), &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, bidderName, nil, "")
+ mockBidServersArray = append(mockBidServersArray, bidServer)
+ }
+
+ mockCurrencyConverter := currency.NewRateConverter(mockCurrencyRatesServer.Client(), mockCurrencyRatesServer.URL, time.Second)
+ mockCurrencyConverter.Run()
+
+ gdprPermsBuilder := fakePermissionsBuilder{
+ permissions: &fakePermissions{},
+ }.Builder
+
+ testExchange := exchange.NewExchange(adapterMap,
+ &wellBehavedCache{},
+ cfg,
+ nil,
+ met,
+ bidderInfos,
+ gdprPermsBuilder,
+ mockCurrencyConverter,
+ mockFetcher,
+ &adscert.NilSigner{},
+ macros.NewStringIndexBasedReplacer(),
+ &floors.PriceFloorFetcher{},
+ )
+
+ testExchange = &exchangeTestWrapper{
+ ex: testExchange,
+ }
+
+ return testExchange, mockBidServersArray
+}
+
+type ctvMockAdapter struct {
+ mockServerURL string
+ Server config.Server
+}
+
+func CTVBuilder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+ adapter := &ctvMockAdapter{
+ mockServerURL: config.Endpoint,
+ Server: server,
+ }
+ return adapter, nil
+}
+
+func (a ctvMockAdapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+ var requests []*adapters.RequestData
+ var errors []error
+
+ requestJSON, err := json.Marshal(request)
+ if err != nil {
+ errors = append(errors, err)
+ return nil, errors
+ }
+
+ requestData := &adapters.RequestData{
+ Method: "POST",
+ Uri: a.mockServerURL,
+ Body: requestJSON,
+ }
+ requests = append(requests, requestData)
+ return requests, errors
+}
+
+func (a ctvMockAdapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+ if responseData.StatusCode != http.StatusOK {
+ switch responseData.StatusCode {
+ case http.StatusNoContent:
+ return nil, nil
+ case http.StatusBadRequest:
+ return nil, []error{&errortypes.BadInput{
+ Message: "Unexpected status code: 400. Bad request from publisher. Run with request.debug = 1 for more info.",
+ }}
+ default:
+ return nil, []error{&errortypes.BadServerResponse{
+ Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", responseData.StatusCode),
+ }}
+ }
+ }
+
+ var publisherResponse openrtb2.BidResponse
+ if err := json.Unmarshal(responseData.Body, &publisherResponse); err != nil {
+ return nil, []error{err}
+ }
+
+ rv := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+ rv.Currency = publisherResponse.Cur
+ for _, seatBid := range publisherResponse.SeatBid {
+ for i, bid := range seatBid.Bid {
+ for _, imp := range request.Imp {
+ if imp.ID == bid.ImpID {
+ b := &adapters.TypedBid{
+ Bid: &seatBid.Bid[i],
+ BidType: openrtb_ext.BidTypeVideo,
+ }
+ rv.Bids = append(rv.Bids, b)
+ }
+ }
+ }
+ }
+ return rv, nil
+}
diff --git a/endpoints/openrtb2/sample-requests/ctv/valid-requests/dynamic-adpod.json b/endpoints/openrtb2/sample-requests/ctv/valid-requests/dynamic-adpod.json
new file mode 100644
index 00000000000..5955d33cd0e
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/ctv/valid-requests/dynamic-adpod.json
@@ -0,0 +1,466 @@
+{
+ "description": "Dynamic adpod request",
+ "config": {
+ "mockBidders": [
+ {
+ "bidderName": "pubmatic",
+ "currency": "USD",
+ "bids": [
+ {
+ "impid": "pod::imp_1",
+ "price": 2,
+ "duration": 30
+ },
+ {
+ "impid": "pod::imp_2",
+ "price": 3,
+ "duration": 30
+ },
+ {
+ "impid": "pod::imp_3",
+ "price": 5,
+ "duration": 30
+ }
+ ]
+ },
+ {
+ "bidderName": "appnexus",
+ "currency": "USD",
+ "bids": [
+ {
+ "impid": "pod::imp_1",
+ "price": 4,
+ "duration": 30
+ },
+ {
+ "impid": "pod::imp_2",
+ "price": 2,
+ "duration": 30
+ },
+ {
+ "impid": "pod::imp_3",
+ "price": 5,
+ "duration": 30
+ }
+ ]
+ }
+ ]
+ },
+ "mockBidRequest": {
+ "id": "1559039248176",
+ "cur": [
+ "USD"
+ ],
+ "imp": [
+ {
+ "id": "pod::imp",
+ "video": {
+ "mimes": [
+ "video/3gpp",
+ "video/mp4",
+ "video/webm"
+ ],
+ "startdelay": 0,
+ "protocols": [
+ 2,
+ 3,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "placement": 5,
+ "linearity": 1,
+ "skip": 1,
+ "skipmin": 10,
+ "skipafter": 15,
+ "battr": [
+ 6,
+ 7
+ ],
+ "maxbitrate": 2000,
+ "boxingallowed": 1,
+ "playbackmethod": [
+ 1
+ ],
+ "playbackend": 1,
+ "delivery": [
+ 2
+ ],
+ "pos": 7,
+ "podid": "pod1",
+ "podseq": 0,
+ "slotinpod": 0,
+ "maxseq": 3,
+ "poddur": 90,
+ "minduration": 30,
+ "maxduration": 30
+ },
+ "tagid": "/15671365/MG_VideoAdUnit",
+ "secure": 0,
+ "ext": {
+ "data": {
+ "pbadslot": "/15671365/MG_VideoAdUnit"
+ },
+ "prebid": {
+ "bidder": {
+ "pubmatic": {
+ "publisherId": "5890",
+ "adSlot": "/15671365/MG_VideoAdUnit@0x0"
+ },
+ "appnexus": {
+ "placementId": 12883451
+ }
+ }
+ }
+ }
+ }
+ ],
+ "app": {
+ "name": "OpenWrapperSample",
+ "bundle": "com.pubmatic.openbid.app",
+ "storeurl": "https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?videobid=10",
+ "ver": "1.0",
+ "publisher": {
+ "id": "5890"
+ }
+ },
+ "device": {
+ "ua": "PostmanRuntime/7.36.3",
+ "ip": "::1"
+ },
+ "at": 1,
+ "tmax": 194999,
+ "source": {
+ "tid": "1559039248176"
+ },
+ "ext": {
+ "prebid": {
+ "aliases": {
+ "adg": "adgeneration",
+ "andbeyond": "adkernel",
+ "districtm": "appnexus",
+ "districtmDMX": "dmx",
+ "mediafuse": "appnexus",
+ "pubmatic2": "pubmatic"
+ },
+ "bidadjustmentfactors": {
+ "pubmatic": 0.9
+ },
+ "bidderparams": {
+ "appnexus": {
+ "placementId": 12883451
+ },
+ "pubmatic": {
+ "wiid": "34d54ecd-fc14-4a52-a6dd-30dcba780a0d"
+ }
+ },
+ "debug": true,
+ "floors": {
+ "enforcement": {
+ "enforcepbs": true
+ },
+ "enabled": true
+ },
+ "targeting": {
+ "pricegranularity": {
+ "precision": 2,
+ "ranges": [
+ {
+ "min": 0,
+ "max": 100,
+ "increment": 0.5
+ }
+ ]
+ },
+ "mediatypepricegranularity": {},
+ "includewinners": true,
+ "includebidderkeys": true,
+ "includebrandcategory": {
+ "primaryadserver": 0,
+ "publisher": "",
+ "withcategory": false,
+ "translatecategories": false,
+ "skipdedup": true
+ }
+ },
+ "macros": {
+ "[PLATFORM]": "3",
+ "[PROFILE_ID]": "81367",
+ "[PROFILE_VERSION]": "1",
+ "[UNIX_TIMESTAMP]": "1711103402",
+ "[WRAPPER_IMPRESSION_ID]": "34d54ecd-fc14-4a52-a6dd-30dcba780a0d"
+ },
+ "returnallbidstatus": true
+ }
+ }
+ },
+ "expectedBidResponse": {
+ "id": "1559039248176",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "ecb93ab7-4b90-47ac-9414-c7f54a49cc69",
+ "impid": "pod::imp",
+ "price": 13.5,
+ "adm": "",
+ "ext": {
+ "prebid": {
+ "type": "video",
+ "video": {
+ "duration": 90,
+ "primary_category": "",
+ "vasttagid": ""
+ }
+ },
+ "adpod": {
+ "refbids": [
+ "appnexus-bid-2",
+ "pubmatic-bid-2",
+ "appnexus-bid-0"
+ ]
+ }
+ }
+ }
+ ],
+ "seat": "prebid_ctv"
+ }
+ ],
+ "cur": "USD",
+ "ext": {
+ "warnings": {
+ "general": [
+ {
+ "code": 10002,
+ "message": "debug turned off for account"
+ }
+ ]
+ },
+ "responsetimemillis": {
+ "appnexus": 1,
+ "pubmatic": 1
+ },
+ "tmaxrequest": 194999,
+ "prebid": {
+ "auctiontimestamp": 1712519355306
+ },
+ "adpod": {
+ "bidresponse": {
+ "id": "1559039248176",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "appnexus-bid-0",
+ "impid": "pod::imp_1",
+ "price": 4,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 4,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "appnexus"
+ },
+ "targeting": {
+ "hb_bidder": "appnexus",
+ "hb_bidder_appnexus": "appnexus",
+ "hb_cache_host": "www.pbcserver.com",
+ "hb_cache_host_appnex": "www.pbcserver.com",
+ "hb_cache_path": "/pbcache/endpoint",
+ "hb_cache_path_appnex": "/pbcache/endpoint",
+ "hb_env": "mobile-app",
+ "hb_env_appnexus": "mobile-app",
+ "hb_pb": "4.00",
+ "hb_pb_appnexus": "4.00",
+ "hb_pb_cat_dur": "4.00_30s",
+ "hb_pb_cat_dur_appnex": "4.00_30s"
+ },
+ "type": "video"
+ }
+ }
+ },
+ {
+ "id": "appnexus-bid-1",
+ "impid": "pod::imp_2",
+ "price": 2,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 2,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "appnexus"
+ },
+ "targeting": {
+ "hb_bidder_appnexus": "appnexus",
+ "hb_cache_host_appnex": "www.pbcserver.com",
+ "hb_cache_path_appnex": "/pbcache/endpoint",
+ "hb_env_appnexus": "mobile-app",
+ "hb_pb_appnexus": "2.00",
+ "hb_pb_cat_dur_appnex": "2.00_30s"
+ },
+ "type": "video"
+ }
+ }
+ },
+ {
+ "id": "appnexus-bid-2",
+ "impid": "pod::imp_3",
+ "price": 5,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 5,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "appnexus"
+ },
+ "targeting": {
+ "hb_bidder": "appnexus",
+ "hb_bidder_appnexus": "appnexus",
+ "hb_cache_host": "www.pbcserver.com",
+ "hb_cache_host_appnex": "www.pbcserver.com",
+ "hb_cache_path": "/pbcache/endpoint",
+ "hb_cache_path_appnex": "/pbcache/endpoint",
+ "hb_env": "mobile-app",
+ "hb_env_appnexus": "mobile-app",
+ "hb_pb": "5.00",
+ "hb_pb_appnexus": "5.00",
+ "hb_pb_cat_dur": "5.00_30s",
+ "hb_pb_cat_dur_appnex": "5.00_30s"
+ },
+ "type": "video"
+ }
+ }
+ }
+ ],
+ "seat": "appnexus"
+ },
+ {
+ "bid": [
+ {
+ "id": "pubmatic-bid-0",
+ "impid": "pod::imp_1",
+ "price": 1.8,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 2,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "pubmatic"
+ },
+ "targeting": {
+ "hb_bidder_pubmatic": "pubmatic",
+ "hb_cache_host_pubmat": "www.pbcserver.com",
+ "hb_cache_path_pubmat": "/pbcache/endpoint",
+ "hb_env_pubmatic": "mobile-app",
+ "hb_pb_cat_dur_pubmat": "1.50_30s",
+ "hb_pb_pubmatic": "1.50"
+ },
+ "type": "video"
+ }
+ }
+ },
+ {
+ "id": "pubmatic-bid-1",
+ "impid": "pod::imp_2",
+ "price": 2.7,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 3,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "pubmatic"
+ },
+ "targeting": {
+ "hb_bidder": "pubmatic",
+ "hb_bidder_pubmatic": "pubmatic",
+ "hb_cache_host": "www.pbcserver.com",
+ "hb_cache_host_pubmat": "www.pbcserver.com",
+ "hb_cache_path": "/pbcache/endpoint",
+ "hb_cache_path_pubmat": "/pbcache/endpoint",
+ "hb_env": "mobile-app",
+ "hb_env_pubmatic": "mobile-app",
+ "hb_pb": "2.50",
+ "hb_pb_cat_dur": "2.50_30s",
+ "hb_pb_cat_dur_pubmat": "2.50_30s",
+ "hb_pb_pubmatic": "2.50"
+ },
+ "type": "video"
+ }
+ }
+ },
+ {
+ "id": "pubmatic-bid-2",
+ "impid": "pod::imp_3",
+ "price": 4.5,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 5,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "pubmatic"
+ },
+ "targeting": {
+ "hb_bidder_pubmatic": "pubmatic",
+ "hb_cache_host_pubmat": "www.pbcserver.com",
+ "hb_cache_path_pubmat": "/pbcache/endpoint",
+ "hb_env_pubmatic": "mobile-app",
+ "hb_pb_cat_dur_pubmat": "4.50_30s",
+ "hb_pb_pubmatic": "4.50"
+ },
+ "type": "video"
+ }
+ }
+ }
+ ],
+ "seat": "pubmatic"
+ }
+ ],
+ "cur": "USD"
+ },
+ "config": {
+ "pod1": {
+ "vidext": {
+ "offset": 0,
+ "adpod": {
+ "minads": 1,
+ "maxads": 3,
+ "adminduration": 30,
+ "admaxduration": 30,
+ "excladv": 100,
+ "excliabcat": 100
+ }
+ },
+ "imp": [
+ {
+ "id": "pod::imp_1",
+ "seq": 1,
+ "minduration": 30,
+ "maxduration": 30
+ },
+ {
+ "id": "pod::imp_2",
+ "seq": 2,
+ "minduration": 30,
+ "maxduration": 30
+ },
+ {
+ "id": "pod::imp_3",
+ "seq": 3,
+ "minduration": 30,
+ "maxduration": 30
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/ctv/valid-requests/hybid-adpod.json b/endpoints/openrtb2/sample-requests/ctv/valid-requests/hybid-adpod.json
new file mode 100644
index 00000000000..5312129768c
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/ctv/valid-requests/hybid-adpod.json
@@ -0,0 +1,338 @@
+{
+ "description": "Hybrid adpod request",
+ "config": {},
+ "mockBidRequest": {
+ "id": "1559039248176",
+ "cur": [
+ "USD"
+ ],
+ "imp": [
+ {
+ "id": "pod1::imp1",
+ "video": {
+ "mimes": [
+ "video/3gpp",
+ "video/mp4",
+ "video/webm"
+ ],
+ "startdelay": 0,
+ "protocols": [
+ 2,
+ 3,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "placement": 5,
+ "linearity": 1,
+ "skip": 1,
+ "skipmin": 10,
+ "skipafter": 15,
+ "battr": [
+ 6,
+ 7
+ ],
+ "maxbitrate": 2000,
+ "boxingallowed": 1,
+ "playbackmethod": [
+ 1
+ ],
+ "playbackend": 1,
+ "delivery": [
+ 2
+ ],
+ "pos": 7,
+ "podid": "pod1",
+ "podseq": 1,
+ "slotinpod": 1,
+ "minduration": 15,
+ "maxduration": 30
+ },
+ "tagid": "/15671365/MG_VideoAdUnit",
+ "secure": 0,
+ "ext": {
+ "data": {
+ "pbadslot": "/15671365/MG_VideoAdUnit"
+ },
+ "prebid": {
+ "bidder": {
+ "pubmatic": {
+ "publisherId": "5890",
+ "adSlot": "/15671365/MG_VideoAdUnit@0x0"
+ },
+ "appnexus": {
+ "placementId": 12883451
+ }
+ }
+ }
+ }
+ },
+ {
+ "id": "pod1::imp2",
+ "video": {
+ "mimes": [
+ "video/3gpp",
+ "video/mp4",
+ "video/webm"
+ ],
+ "startdelay": 0,
+ "protocols": [
+ 2,
+ 3,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "placement": 5,
+ "linearity": 1,
+ "skip": 1,
+ "skipmin": 10,
+ "skipafter": 15,
+ "battr": [
+ 6,
+ 7
+ ],
+ "maxbitrate": 2000,
+ "boxingallowed": 1,
+ "playbackmethod": [
+ 1
+ ],
+ "playbackend": 1,
+ "delivery": [
+ 2
+ ],
+ "pos": 7,
+ "podid": "pod1",
+ "podseq": 1,
+ "slotinpod": 0,
+ "minduration": 10,
+ "maxduration": 30
+ },
+ "tagid": "/15671365/MG_VideoAdUnit",
+ "secure": 0,
+ "ext": {
+ "data": {
+ "pbadslot": "/15671365/MG_VideoAdUnit"
+ },
+ "prebid": {
+ "bidder": {
+ "pubmatic": {
+ "publisherId": "5890",
+ "adSlot": "/15671365/MG_VideoAdUnit@0x0"
+ },
+ "appnexus": {
+ "placementId": 12883451
+ }
+ }
+ }
+ }
+ },
+ {
+ "id": "pod1::imp3",
+ "video": {
+ "mimes": [
+ "video/3gpp",
+ "video/mp4",
+ "video/webm"
+ ],
+ "startdelay": 0,
+ "protocols": [
+ 2,
+ 3,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "placement": 5,
+ "linearity": 1,
+ "skip": 1,
+ "skipmin": 10,
+ "skipafter": 15,
+ "battr": [
+ 6,
+ 7
+ ],
+ "maxbitrate": 2000,
+ "boxingallowed": 1,
+ "playbackmethod": [
+ 1
+ ],
+ "playbackend": 1,
+ "delivery": [
+ 2
+ ],
+ "pos": 7,
+ "podid": "pod1",
+ "podseq": 1,
+ "slotinpod": 0,
+ "minduration": 30,
+ "maxduration": 30
+ },
+ "tagid": "/15671365/MG_VideoAdUnit",
+ "secure": 0,
+ "ext": {
+ "data": {
+ "pbadslot": "/15671365/MG_VideoAdUnit"
+ },
+ "prebid": {
+ "bidder": {
+ "pubmatic": {
+ "publisherId": "5890",
+ "adSlot": "/15671365/MG_VideoAdUnit@0x0"
+ },
+ "appnexus": {
+ "placementId": 12883451
+ }
+ }
+ }
+ }
+ },
+ {
+ "id": "pod2::imp",
+ "video": {
+ "mimes": [
+ "video/3gpp",
+ "video/mp4",
+ "video/webm"
+ ],
+ "startdelay": 0,
+ "protocols": [
+ 2,
+ 3,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "placement": 5,
+ "linearity": 1,
+ "skip": 1,
+ "skipmin": 10,
+ "skipafter": 15,
+ "battr": [
+ 6,
+ 7
+ ],
+ "maxbitrate": 2000,
+ "boxingallowed": 1,
+ "playbackmethod": [
+ 1
+ ],
+ "playbackend": 1,
+ "delivery": [
+ 2
+ ],
+ "pos": 7,
+ "podid": "pod1",
+ "podseq": 0,
+ "slotinpod": 0,
+ "maxseq": 3,
+ "poddur": 90,
+ "minduration": 30,
+ "maxduration": 30
+ },
+ "tagid": "/15671365/MG_VideoAdUnit",
+ "secure": 0,
+ "ext": {
+ "data": {
+ "pbadslot": "/15671365/MG_VideoAdUnit"
+ },
+ "prebid": {
+ "bidder": {
+ "pubmatic": {
+ "publisherId": "5890",
+ "adSlot": "/15671365/MG_VideoAdUnit@0x0"
+ },
+ "appnexus": {
+ "placementId": 12883451
+ }
+ }
+ }
+ }
+ }
+ ],
+ "app": {
+ "name": "OpenWrapperSample",
+ "bundle": "com.pubmatic.openbid.app",
+ "storeurl": "https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?videobid=10",
+ "ver": "1.0",
+ "publisher": {
+ "id": "5890"
+ }
+ },
+ "device": {
+ "ua": "PostmanRuntime/7.36.3",
+ "ip": "::1"
+ },
+ "at": 1,
+ "tmax": 194999,
+ "source": {
+ "tid": "1559039248176"
+ },
+ "ext": {
+ "prebid": {
+ "aliases": {
+ "adg": "adgeneration",
+ "andbeyond": "adkernel",
+ "districtm": "appnexus",
+ "districtmDMX": "dmx",
+ "mediafuse": "appnexus",
+ "pubmatic2": "pubmatic"
+ },
+ "bidadjustmentfactors": {
+ "pubmatic": 0.9
+ },
+ "bidderparams": {
+ "appnexus": {
+ "placementId": 12883451
+ },
+ "pubmatic": {
+ "wiid": "34d54ecd-fc14-4a52-a6dd-30dcba780a0d"
+ }
+ },
+ "debug": true,
+ "floors": {
+ "enforcement": {
+ "enforcepbs": true
+ },
+ "enabled": true
+ },
+ "targeting": {
+ "pricegranularity": {
+ "precision": 2,
+ "ranges": [
+ {
+ "min": 0,
+ "max": 100,
+ "increment": 0.5
+ }
+ ]
+ },
+ "mediatypepricegranularity": {},
+ "includewinners": true,
+ "includebidderkeys": true,
+ "includebrandcategory": {
+ "primaryadserver": 0,
+ "publisher": "",
+ "withcategory": false,
+ "translatecategories": false,
+ "skipdedup": true
+ }
+ },
+ "macros": {
+ "[PLATFORM]": "3",
+ "[PROFILE_ID]": "81367",
+ "[PROFILE_VERSION]": "1",
+ "[UNIX_TIMESTAMP]": "1711103402",
+ "[WRAPPER_IMPRESSION_ID]": "34d54ecd-fc14-4a52-a6dd-30dcba780a0d"
+ },
+ "returnallbidstatus": true
+ }
+ }
+ },
+ "expectedBidResponse": {},
+ "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/ctv/valid-requests/structured-adpod.json b/endpoints/openrtb2/sample-requests/ctv/valid-requests/structured-adpod.json
new file mode 100644
index 00000000000..f26a57f4e95
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/ctv/valid-requests/structured-adpod.json
@@ -0,0 +1,620 @@
+{
+ "description": "Structured adpod request",
+ "config": {
+ "mockBidders": [
+ {
+ "bidderName": "pubmatic",
+ "currency": "USD",
+ "bids": [
+ {
+ "impid": "imp1",
+ "price": 2
+ },
+ {
+ "impid": "imp2",
+ "price": 3
+ },
+ {
+ "impid": "imp3",
+ "price": 5
+ }
+ ]
+ },
+ {
+ "bidderName": "appnexus",
+ "currency": "USD",
+ "bids": [
+ {
+ "impid": "imp1",
+ "price": 4
+ },
+ {
+ "impid": "imp2",
+ "price": 2
+ },
+ {
+ "impid": "imp3",
+ "price": 5
+ }
+ ]
+ }
+ ]
+ },
+ "mockBidRequest": {
+ "id": "1559039248176",
+ "cur": [
+ "USD"
+ ],
+ "imp": [
+ {
+ "id": "imp1",
+ "video": {
+ "mimes": [
+ "video/3gpp",
+ "video/mp4",
+ "video/webm"
+ ],
+ "startdelay": 0,
+ "protocols": [
+ 2,
+ 3,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "placement": 5,
+ "linearity": 1,
+ "skip": 1,
+ "skipmin": 10,
+ "skipafter": 15,
+ "battr": [
+ 6,
+ 7
+ ],
+ "maxbitrate": 2000,
+ "boxingallowed": 1,
+ "playbackmethod": [
+ 1
+ ],
+ "playbackend": 1,
+ "delivery": [
+ 2
+ ],
+ "pos": 7,
+ "podid": "pod_1",
+ "podseq": 1,
+ "slotinpod": 1,
+ "minduration": 15,
+ "maxduration": 30
+ },
+ "tagid": "/15671365/MG_VideoAdUnit",
+ "secure": 0,
+ "ext": {
+ "data": {
+ "pbadslot": "/15671365/MG_VideoAdUnit"
+ },
+ "prebid": {
+ "bidder": {
+ "pubmatic": {
+ "publisherId": "5890",
+ "adSlot": "/15671365/MG_VideoAdUnit@0x0"
+ },
+ "appnexus": {
+ "placementId": 12883451
+ }
+ }
+ }
+ }
+ },
+ {
+ "id": "imp2",
+ "video": {
+ "mimes": [
+ "video/3gpp",
+ "video/mp4",
+ "video/webm"
+ ],
+ "startdelay": 0,
+ "protocols": [
+ 2,
+ 3,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "placement": 5,
+ "linearity": 1,
+ "skip": 1,
+ "skipmin": 10,
+ "skipafter": 15,
+ "battr": [
+ 6,
+ 7
+ ],
+ "maxbitrate": 2000,
+ "boxingallowed": 1,
+ "playbackmethod": [
+ 1
+ ],
+ "playbackend": 1,
+ "delivery": [
+ 2
+ ],
+ "pos": 7,
+ "podid": "pod_1",
+ "podseq": 1,
+ "slotinpod": 0,
+ "minduration": 10,
+ "maxduration": 30
+ },
+ "tagid": "/15671365/MG_VideoAdUnit",
+ "secure": 0,
+ "ext": {
+ "data": {
+ "pbadslot": "/15671365/MG_VideoAdUnit"
+ },
+ "prebid": {
+ "bidder": {
+ "pubmatic": {
+ "publisherId": "5890",
+ "adSlot": "/15671365/MG_VideoAdUnit@0x0"
+ },
+ "appnexus": {
+ "placementId": 12883451
+ }
+ }
+ }
+ }
+ },
+ {
+ "id": "imp3",
+ "video": {
+ "mimes": [
+ "video/3gpp",
+ "video/mp4",
+ "video/webm"
+ ],
+ "startdelay": 0,
+ "protocols": [
+ 2,
+ 3,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "placement": 5,
+ "linearity": 1,
+ "skip": 1,
+ "skipmin": 10,
+ "skipafter": 15,
+ "battr": [
+ 6,
+ 7
+ ],
+ "maxbitrate": 2000,
+ "boxingallowed": 1,
+ "playbackmethod": [
+ 1
+ ],
+ "playbackend": 1,
+ "delivery": [
+ 2
+ ],
+ "pos": 7,
+ "podid": "pod_1",
+ "podseq": 1,
+ "slotinpod": 0,
+ "minduration": 30,
+ "maxduration": 30
+ },
+ "tagid": "/15671365/MG_VideoAdUnit",
+ "secure": 0,
+ "ext": {
+ "data": {
+ "pbadslot": "/15671365/MG_VideoAdUnit"
+ },
+ "prebid": {
+ "bidder": {
+ "pubmatic": {
+ "publisherId": "5890",
+ "adSlot": "/15671365/MG_VideoAdUnit@0x0"
+ },
+ "appnexus": {
+ "placementId": 12883451
+ }
+ }
+ }
+ }
+ }
+ ],
+ "app": {
+ "name": "OpenWrapperSample",
+ "bundle": "com.pubmatic.openbid.app",
+ "storeurl": "https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?videobid=10",
+ "ver": "1.0",
+ "publisher": {
+ "id": "5890"
+ }
+ },
+ "device": {
+ "ua": "PostmanRuntime/7.36.3",
+ "ip": "::1"
+ },
+ "at": 1,
+ "tmax": 194999,
+ "source": {
+ "tid": "1559039248176"
+ },
+ "ext": {
+ "prebid": {
+ "aliases": {
+ "adg": "adgeneration",
+ "andbeyond": "adkernel",
+ "districtm": "appnexus",
+ "districtmDMX": "dmx",
+ "mediafuse": "appnexus",
+ "pubmatic2": "pubmatic"
+ },
+ "bidadjustmentfactors": {
+ "pubmatic": 0.9
+ },
+ "bidderparams": {
+ "pubmatic": {
+ "wiid": "34d54ecd-fc14-4a52-a6dd-30dcba780a0d"
+ },
+ "appnexus": {
+ "placementId": 12883451
+ }
+ },
+ "debug": true,
+ "floors": {
+ "enforcement": {
+ "enforcepbs": true
+ },
+ "enabled": true
+ },
+ "targeting": {
+ "pricegranularity": {
+ "precision": 2,
+ "ranges": [
+ {
+ "min": 0,
+ "max": 100,
+ "increment": 0.5
+ }
+ ]
+ },
+ "mediatypepricegranularity": {},
+ "includewinners": true,
+ "includebidderkeys": true,
+ "includebrandcategory": {
+ "primaryadserver": 0,
+ "publisher": "",
+ "withcategory": false,
+ "translatecategories": false,
+ "skipdedup": true
+ }
+ },
+ "macros": {
+ "[PLATFORM]": "3",
+ "[PROFILE_ID]": "81367",
+ "[PROFILE_VERSION]": "1",
+ "[UNIX_TIMESTAMP]": "1711103402",
+ "[WRAPPER_IMPRESSION_ID]": "34d54ecd-fc14-4a52-a6dd-30dcba780a0d"
+ },
+ "returnallbidstatus": true
+ }
+ }
+ },
+ "expectedBidResponse": {
+ "id": "1559039248176",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "pubmatic-bid-1",
+ "impid": "imp2",
+ "price": 2.7,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 3,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "pubmatic"
+ },
+ "targeting": {
+ "hb_bidder": "pubmatic",
+ "hb_bidder_pubmatic": "pubmatic",
+ "hb_cache_host": "www.pbcserver.com",
+ "hb_cache_host_pubmat": "www.pbcserver.com",
+ "hb_cache_path": "/pbcache/endpoint",
+ "hb_cache_path_pubmat": "/pbcache/endpoint",
+ "hb_env": "mobile-app",
+ "hb_env_pubmatic": "mobile-app",
+ "hb_pb": "2.50",
+ "hb_pb_cat_dur": "2.50_30s",
+ "hb_pb_cat_dur_pubmat": "2.50_30s",
+ "hb_pb_pubmatic": "2.50"
+ },
+ "type": "video"
+ }
+ }
+ }
+ ],
+ "seat": "pubmatic"
+ },
+ {
+ "bid": [
+ {
+ "id": "appnexus-bid-2",
+ "impid": "imp3",
+ "price": 5,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 5,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "appnexus"
+ },
+ "targeting": {
+ "hb_bidder": "appnexus",
+ "hb_bidder_appnexus": "appnexus",
+ "hb_cache_host": "www.pbcserver.com",
+ "hb_cache_host_appnex": "www.pbcserver.com",
+ "hb_cache_path": "/pbcache/endpoint",
+ "hb_cache_path_appnex": "/pbcache/endpoint",
+ "hb_env": "mobile-app",
+ "hb_env_appnexus": "mobile-app",
+ "hb_pb": "5.00",
+ "hb_pb_appnexus": "5.00",
+ "hb_pb_cat_dur": "5.00_30s",
+ "hb_pb_cat_dur_appnex": "5.00_30s"
+ },
+ "type": "video"
+ }
+ }
+ },
+ {
+ "id": "appnexus-bid-0",
+ "impid": "imp1",
+ "price": 4,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 4,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "appnexus"
+ },
+ "targeting": {
+ "hb_bidder": "appnexus",
+ "hb_bidder_appnexus": "appnexus",
+ "hb_cache_host": "www.pbcserver.com",
+ "hb_cache_host_appnex": "www.pbcserver.com",
+ "hb_cache_path": "/pbcache/endpoint",
+ "hb_cache_path_appnex": "/pbcache/endpoint",
+ "hb_env": "mobile-app",
+ "hb_env_appnexus": "mobile-app",
+ "hb_pb": "4.00",
+ "hb_pb_appnexus": "4.00",
+ "hb_pb_cat_dur": "4.00_30s",
+ "hb_pb_cat_dur_appnex": "4.00_30s"
+ },
+ "type": "video"
+ }
+ }
+ }
+ ],
+ "seat": "appnexus"
+ }
+ ],
+ "cur": "USD",
+ "ext": {
+ "warnings": {
+ "general": [
+ {
+ "code": 10002,
+ "message": "debug turned off for account"
+ }
+ ]
+ },
+ "responsetimemillis": {
+ "appnexus": 5920,
+ "pubmatic": 5920
+ },
+ "tmaxrequest": 194999,
+ "prebid": {
+ "auctiontimestamp": 1712568761543
+ },
+ "adpod": {
+ "bidresponse": {
+ "id": "1559039248176",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "appnexus-bid-0",
+ "impid": "imp1",
+ "price": 4,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 4,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "appnexus"
+ },
+ "targeting": {
+ "hb_bidder": "appnexus",
+ "hb_bidder_appnexus": "appnexus",
+ "hb_cache_host": "www.pbcserver.com",
+ "hb_cache_host_appnex": "www.pbcserver.com",
+ "hb_cache_path": "/pbcache/endpoint",
+ "hb_cache_path_appnex": "/pbcache/endpoint",
+ "hb_env": "mobile-app",
+ "hb_env_appnexus": "mobile-app",
+ "hb_pb": "4.00",
+ "hb_pb_appnexus": "4.00",
+ "hb_pb_cat_dur": "4.00_30s",
+ "hb_pb_cat_dur_appnex": "4.00_30s"
+ },
+ "type": "video"
+ }
+ }
+ },
+ {
+ "id": "appnexus-bid-1",
+ "impid": "imp2",
+ "price": 2,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 2,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "appnexus"
+ },
+ "targeting": {
+ "hb_bidder_appnexus": "appnexus",
+ "hb_cache_host_appnex": "www.pbcserver.com",
+ "hb_cache_path_appnex": "/pbcache/endpoint",
+ "hb_env_appnexus": "mobile-app",
+ "hb_pb_appnexus": "2.00",
+ "hb_pb_cat_dur_appnex": "2.00_30s"
+ },
+ "type": "video"
+ }
+ }
+ },
+ {
+ "id": "appnexus-bid-2",
+ "impid": "imp3",
+ "price": 5,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 5,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "appnexus"
+ },
+ "targeting": {
+ "hb_bidder": "appnexus",
+ "hb_bidder_appnexus": "appnexus",
+ "hb_cache_host": "www.pbcserver.com",
+ "hb_cache_host_appnex": "www.pbcserver.com",
+ "hb_cache_path": "/pbcache/endpoint",
+ "hb_cache_path_appnex": "/pbcache/endpoint",
+ "hb_env": "mobile-app",
+ "hb_env_appnexus": "mobile-app",
+ "hb_pb": "5.00",
+ "hb_pb_appnexus": "5.00",
+ "hb_pb_cat_dur": "5.00_30s",
+ "hb_pb_cat_dur_appnex": "5.00_30s"
+ },
+ "type": "video"
+ }
+ }
+ }
+ ],
+ "seat": "appnexus"
+ },
+ {
+ "bid": [
+ {
+ "id": "pubmatic-bid-0",
+ "impid": "imp1",
+ "price": 1.8,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 2,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "pubmatic"
+ },
+ "targeting": {
+ "hb_bidder_pubmatic": "pubmatic",
+ "hb_cache_host_pubmat": "www.pbcserver.com",
+ "hb_cache_path_pubmat": "/pbcache/endpoint",
+ "hb_env_pubmatic": "mobile-app",
+ "hb_pb_cat_dur_pubmat": "1.50_30s",
+ "hb_pb_pubmatic": "1.50"
+ },
+ "type": "video"
+ }
+ }
+ },
+ {
+ "id": "pubmatic-bid-1",
+ "impid": "imp2",
+ "price": 2.7,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 3,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "pubmatic"
+ },
+ "targeting": {
+ "hb_bidder": "pubmatic",
+ "hb_bidder_pubmatic": "pubmatic",
+ "hb_cache_host": "www.pbcserver.com",
+ "hb_cache_host_pubmat": "www.pbcserver.com",
+ "hb_cache_path": "/pbcache/endpoint",
+ "hb_cache_path_pubmat": "/pbcache/endpoint",
+ "hb_env": "mobile-app",
+ "hb_env_pubmatic": "mobile-app",
+ "hb_pb": "2.50",
+ "hb_pb_cat_dur": "2.50_30s",
+ "hb_pb_cat_dur_pubmat": "2.50_30s",
+ "hb_pb_pubmatic": "2.50"
+ },
+ "type": "video"
+ }
+ }
+ },
+ {
+ "id": "pubmatic-bid-2",
+ "impid": "imp3",
+ "price": 4.5,
+ "adm": "",
+ "ext": {
+ "origbidcpm": 5,
+ "origbidcur": "USD",
+ "prebid": {
+ "meta": {
+ "adaptercode": "pubmatic"
+ },
+ "targeting": {
+ "hb_bidder_pubmatic": "pubmatic",
+ "hb_cache_host_pubmat": "www.pbcserver.com",
+ "hb_cache_path_pubmat": "/pbcache/endpoint",
+ "hb_env_pubmatic": "mobile-app",
+ "hb_pb_cat_dur_pubmat": "4.50_30s",
+ "hb_pb_pubmatic": "4.50"
+ },
+ "type": "video"
+ }
+ }
+ }
+ ],
+ "seat": "pubmatic"
+ }
+ ],
+ "cur": "USD"
+ },
+ "config": {
+ "pod_1": null
+ }
+ }
+ }
+ },
+ "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/ctv/valid-requests/video-request.json b/endpoints/openrtb2/sample-requests/ctv/valid-requests/video-request.json
new file mode 100644
index 00000000000..9fef21eb747
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/ctv/valid-requests/video-request.json
@@ -0,0 +1,264 @@
+{
+ "description": "Pure Video request",
+ "config": {
+ },
+ "mockBidRequest": {
+ "id": "1559039248176",
+ "cur": [
+ "USD"
+ ],
+ "imp": [
+ {
+ "id": "imp1",
+ "video": {
+ "mimes": [
+ "video/3gpp",
+ "video/mp4",
+ "video/webm"
+ ],
+ "startdelay": 0,
+ "protocols": [
+ 2,
+ 3,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "placement": 5,
+ "linearity": 1,
+ "skip": 1,
+ "skipmin": 10,
+ "skipafter": 15,
+ "battr": [
+ 6,
+ 7
+ ],
+ "maxbitrate": 2000,
+ "boxingallowed": 1,
+ "playbackmethod": [
+ 1
+ ],
+ "playbackend": 1,
+ "delivery": [
+ 2
+ ],
+ "pos": 7,
+ "podid": "pod_1",
+ "podseq": 1,
+ "slotinpod": 1,
+ "minduration": 15,
+ "maxduration": 30
+ },
+ "tagid": "/15671365/MG_VideoAdUnit",
+ "secure": 0,
+ "ext": {
+ "data": {
+ "pbadslot": "/15671365/MG_VideoAdUnit"
+ },
+ "prebid": {
+ "bidder": {
+ "pubmatic": {
+ "publisherId": "5890",
+ "adSlot": "/15671365/MG_VideoAdUnit@0x0"
+ }
+ }
+ }
+ }
+ },
+ {
+ "id": "imp2",
+ "video": {
+ "mimes": [
+ "video/3gpp",
+ "video/mp4",
+ "video/webm"
+ ],
+ "startdelay": 0,
+ "protocols": [
+ 2,
+ 3,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "placement": 5,
+ "linearity": 1,
+ "skip": 1,
+ "skipmin": 10,
+ "skipafter": 15,
+ "battr": [
+ 6,
+ 7
+ ],
+ "maxbitrate": 2000,
+ "boxingallowed": 1,
+ "playbackmethod": [
+ 1
+ ],
+ "playbackend": 1,
+ "delivery": [
+ 2
+ ],
+ "pos": 7,
+ "podid": "pod_1",
+ "podseq": 1,
+ "slotinpod": 0,
+ "minduration": 10,
+ "maxduration": 30
+ },
+ "tagid": "/15671365/MG_VideoAdUnit",
+ "secure": 0,
+ "ext": {
+ "data": {
+ "pbadslot": "/15671365/MG_VideoAdUnit"
+ },
+ "prebid": {
+ "bidder": {
+ "pubmatic": {
+ "publisherId": "5890",
+ "adSlot": "/15671365/MG_VideoAdUnit@0x0"
+ }
+ }
+ }
+ }
+ },
+ {
+ "id": "imp3",
+ "video": {
+ "mimes": [
+ "video/3gpp",
+ "video/mp4",
+ "video/webm"
+ ],
+ "startdelay": 0,
+ "protocols": [
+ 2,
+ 3,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "placement": 5,
+ "linearity": 1,
+ "skip": 1,
+ "skipmin": 10,
+ "skipafter": 15,
+ "battr": [
+ 6,
+ 7
+ ],
+ "maxbitrate": 2000,
+ "boxingallowed": 1,
+ "playbackmethod": [
+ 1
+ ],
+ "playbackend": 1,
+ "delivery": [
+ 2
+ ],
+ "pos": 7,
+ "podid": "pod_1",
+ "podseq": 1,
+ "slotinpod": 0,
+ "minduration": 30,
+ "maxduration": 30
+ },
+ "tagid": "/15671365/MG_VideoAdUnit",
+ "secure": 0,
+ "ext": {
+ "data": {
+ "pbadslot": "/15671365/MG_VideoAdUnit"
+ },
+ "prebid": {
+ "bidder": {
+ "pubmatic": {
+ "publisherId": "5890",
+ "adSlot": "/15671365/MG_VideoAdUnit@0x0"
+ }
+ }
+ }
+ }
+ }
+ ],
+ "app": {
+ "name": "OpenWrapperSample",
+ "bundle": "com.pubmatic.openbid.app",
+ "storeurl": "https://itunes.apple.com/us/app/pubmatic-sdk-app/id1175273098?videobid=10",
+ "ver": "1.0",
+ "publisher": {
+ "id": "5890"
+ }
+ },
+ "device": {
+ "ua": "PostmanRuntime/7.36.3",
+ "ip": "::1"
+ },
+ "at": 1,
+ "tmax": 194999,
+ "source": {
+ "tid": "1559039248176"
+ },
+ "ext": {
+ "prebid": {
+ "aliases": {
+ "adg": "adgeneration",
+ "andbeyond": "adkernel",
+ "districtm": "appnexus",
+ "districtmDMX": "dmx",
+ "mediafuse": "appnexus",
+ "pubmatic2": "pubmatic"
+ },
+ "bidadjustmentfactors": {
+ "pubmatic": 0.9
+ },
+ "bidderparams": {
+ "pubmatic": {
+ "wiid": "34d54ecd-fc14-4a52-a6dd-30dcba780a0d"
+ }
+ },
+ "debug": true,
+ "floors": {
+ "enforcement": {
+ "enforcepbs": true
+ },
+ "enabled": true
+ },
+ "targeting": {
+ "pricegranularity": {
+ "precision": 2,
+ "ranges": [
+ {
+ "min": 0,
+ "max": 100,
+ "increment": 0.5
+ }
+ ]
+ },
+ "mediatypepricegranularity": {},
+ "includewinners": true,
+ "includebidderkeys": true,
+ "includebrandcategory": {
+ "primaryadserver": 0,
+ "publisher": "",
+ "withcategory": false,
+ "translatecategories": false,
+ "skipdedup": true
+ }
+ },
+ "macros": {
+ "[PLATFORM]": "3",
+ "[PROFILE_ID]": "81367",
+ "[PROFILE_VERSION]": "1",
+ "[UNIX_TIMESTAMP]": "1711103402",
+ "[WRAPPER_IMPRESSION_ID]": "34d54ecd-fc14-4a52-a6dd-30dcba780a0d"
+ },
+ "returnallbidstatus": true
+ }
+ }
+ },
+ "expectedBidResponse": {},
+ "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index fe72226cf1e..e3f4d9717ea 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -135,6 +135,7 @@ import (
"github.com/prebid/prebid-server/v2/adapters/openx"
"github.com/prebid/prebid-server/v2/adapters/operaads"
"github.com/prebid/prebid-server/v2/adapters/orbidder"
+ "github.com/prebid/prebid-server/v2/adapters/ortbbidder"
"github.com/prebid/prebid-server/v2/adapters/outbrain"
"github.com/prebid/prebid-server/v2/adapters/ownadx"
"github.com/prebid/prebid-server/v2/adapters/pangle"
@@ -341,6 +342,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
openrtb_ext.BidderOpenx: openx.Builder,
openrtb_ext.BidderOperaads: operaads.Builder,
openrtb_ext.BidderOrbidder: orbidder.Builder,
+ openrtb_ext.BidderORTBTestBidder: ortbbidder.Builder, // OW specific : testbidder (oRTB integration)
openrtb_ext.BidderOutbrain: outbrain.Builder,
openrtb_ext.BidderOwnAdx: ownadx.Builder,
openrtb_ext.BidderPangle: pangle.Builder,
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 02afe83a620..2e42e685bf2 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -759,6 +759,7 @@ func (e *exchange) getAllBids(
reqInfo := adapters.NewExtraRequestInfo(conversions)
reqInfo.PbsEntryPoint = bidderRequest.BidderLabels.RType
reqInfo.GlobalPrivacyControlHeader = globalPrivacyControlHeader
+ reqInfo.BidderCoreName = bidderRequest.BidderCoreName // OW specific: required for oRTB bidder
bidReqOptions := bidRequestOptions{
accountDebugAllowed: accountDebugAllowed,
diff --git a/go.mod b/go.mod
index 1d04b994a2c..63d2c1b7d29 100644
--- a/go.mod
+++ b/go.mod
@@ -82,6 +82,7 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
+ github.com/yudai/pp v2.0.1+incompatible // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
diff --git a/go.sum b/go.sum
index b0558e61c90..e08f20c6e26 100644
--- a/go.sum
+++ b/go.sum
@@ -508,6 +508,7 @@ github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FB
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
+github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
diff --git a/main.go b/main.go
index 9599783492e..f84c42961fc 100644
--- a/main.go
+++ b/main.go
@@ -43,6 +43,7 @@ func Main() {
if err != nil {
glog.Exitf("Configuration could not be loaded or did not pass validation: %v", err)
}
+ main_ow()
// Create a soft memory limit on the total amount of memory that PBS uses to tune the behavior
// of the Go garbage collector. In summary, `cfg.GarbageCollectorThreshold` serves as a fixed cost
diff --git a/main_ow.go b/main_ow.go
new file mode 100644
index 00000000000..ae0d3d2c353
--- /dev/null
+++ b/main_ow.go
@@ -0,0 +1,16 @@
+package main_ow
+
+import (
+ "github.com/golang/glog"
+ "github.com/prebid/prebid-server/v2/adapters/ortbbidder"
+)
+
+const paramsDirectory = "./static/bidder-params"
+
+// main_ow will perform the openwrap specific initialisation tasks
+func main_ow() {
+ err := ortbbidder.InitBidderParamsConfig(paramsDirectory)
+ if err != nil {
+ glog.Exitf("Unable to initialise bidder-param mapper for oRTB bidders: %v", err)
+ }
+}
diff --git a/modules/pubmatic/openwrap/adapters/bidder_alias.go b/modules/pubmatic/openwrap/adapters/bidder_alias.go
index 569d3099515..c07e8c31841 100644
--- a/modules/pubmatic/openwrap/adapters/bidder_alias.go
+++ b/modules/pubmatic/openwrap/adapters/bidder_alias.go
@@ -1,6 +1,8 @@
package adapters
import (
+ "strings"
+
"github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/models"
"github.com/prebid/prebid-server/v2/openrtb_ext"
)
@@ -21,7 +23,8 @@ func Alias() map[string]string {
//ResolveOWBidder it resolves hardcoded bidder alias names
func ResolveOWBidder(bidderName string) string {
- var coreBidderName string
+ var coreBidderName = bidderName
+ bidderName = strings.TrimSuffix(bidderName, "_deprecated")
switch bidderName {
case models.BidderAdGenerationAlias:
@@ -42,8 +45,6 @@ func ResolveOWBidder(bidderName string) string {
coreBidderName = string(openrtb_ext.BidderImds)
case models.BidderViewDeos:
coreBidderName = string(openrtb_ext.BidderAdtelligent)
- default:
- coreBidderName = bidderName
}
return coreBidderName
}
diff --git a/modules/pubmatic/openwrap/adapters/bidder_alias_test.go b/modules/pubmatic/openwrap/adapters/bidder_alias_test.go
index 17af57cc804..8605884c216 100644
--- a/modules/pubmatic/openwrap/adapters/bidder_alias_test.go
+++ b/modules/pubmatic/openwrap/adapters/bidder_alias_test.go
@@ -23,6 +23,8 @@ func TestAlias(t *testing.T) {
func TestResolveOWBidder(t *testing.T) {
assert.Equal(t, "", ResolveOWBidder(""))
assert.Equal(t, models.BidderPubMatic, ResolveOWBidder(models.BidderPubMatic))
+ assert.Equal(t, string(openrtb_ext.BidderAdf), ResolveOWBidder("adform_deprecated")) // deprecated custom alias
+ assert.Equal(t, "tpmn_deprecated", ResolveOWBidder("tpmn_deprecated")) // any other deprecated bidder
for alias, coreBidder := range Alias() {
assert.Equal(t, coreBidder, ResolveOWBidder(alias))
}
diff --git a/modules/pubmatic/openwrap/adapters/default_bidder_parameter_test.go b/modules/pubmatic/openwrap/adapters/default_bidder_parameter_test.go
index a1a6bb5968f..89c6e38212f 100644
--- a/modules/pubmatic/openwrap/adapters/default_bidder_parameter_test.go
+++ b/modules/pubmatic/openwrap/adapters/default_bidder_parameter_test.go
@@ -111,7 +111,7 @@ func TestGetType(t *testing.T) {
func TestParseBidderParams(t *testing.T) {
parseBidderParams("../../static/bidder-params")
- assert.Equal(t, 160, len(adapterParams), "Length of expected entries should match")
+ assert.Equal(t, 161, len(adapterParams), "Length of expected entries should match")
// calculate this number using X-Y
// where X is calculated using command - `ls -l | wc -l` (substract 1 from result)
// Y is calculated using command `grep -EinR 'oneof|not|anyof|dependenc' static/bidder-params | grep -v "description" | grep -oE './.*.json' | uniq | wc -l`
@@ -119,7 +119,7 @@ func TestParseBidderParams(t *testing.T) {
func TestParseBidderSchemaDefinitions(t *testing.T) {
schemaDefinitions, _ := parseBidderSchemaDefinitions("../../../../static/bidder-params")
- assert.Equal(t, 198, len(schemaDefinitions), "Length of expected entries should match")
+ assert.Equal(t, 199, len(schemaDefinitions), "Length of expected entries should match")
// calculate this number using command - `ls -l | wc -l` (substract 1 from result)
}
diff --git a/modules/pubmatic/openwrap/applovinmax.go b/modules/pubmatic/openwrap/applovinmax.go
index 4d90d385c45..751a4753414 100644
--- a/modules/pubmatic/openwrap/applovinmax.go
+++ b/modules/pubmatic/openwrap/applovinmax.go
@@ -9,14 +9,20 @@ import (
"github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/models"
)
-func getSignalData(requestBody []byte) *openrtb2.BidRequest {
+func getSignalData(requestBody []byte, rctx models.RequestCtx) *openrtb2.BidRequest {
signal, err := jsonparser.GetString(requestBody, "user", "data", "[0]", "segment", "[0]", "signal")
if err != nil {
+ signalType := models.InvalidSignal
+ if err == jsonparser.KeyPathNotFoundError {
+ signalType = models.MissingSignal
+ }
+ rctx.MetricsEngine.RecordSignalDataStatus(getAppPublisherID(requestBody), getProfileID(requestBody), signalType)
return nil
}
signalData := &openrtb2.BidRequest{}
if err := json.Unmarshal([]byte(signal), signalData); err != nil {
+ rctx.MetricsEngine.RecordSignalDataStatus(getAppPublisherID(requestBody), getProfileID(requestBody), models.InvalidSignal)
return nil
}
return signalData
@@ -218,10 +224,10 @@ func updateRequestWrapper(signalExt json.RawMessage, maxRequest *openrtb2.BidReq
}
}
-func updateAppLovinMaxRequest(requestBody []byte) []byte {
- signalData := getSignalData(requestBody)
+func updateAppLovinMaxRequest(requestBody []byte, rctx models.RequestCtx) []byte {
+ signalData := getSignalData(requestBody, rctx)
if signalData == nil {
- return requestBody
+ return modifyRequestBody(requestBody)
}
maxRequest := &openrtb2.BidRequest{}
@@ -285,3 +291,30 @@ func applyAppLovinMaxResponse(rctx models.RequestCtx, bidResponse *openrtb2.BidR
}
return bidResponse
}
+
+func getAppPublisherID(requestBody []byte) string {
+ if pubId, err := jsonparser.GetString(requestBody, "app", "publisher", "id"); err == nil && len(pubId) > 0 {
+ return pubId
+ }
+ return ""
+}
+
+func getProfileID(requestBody []byte) string {
+ if profileId, err := jsonparser.GetInt(requestBody, "ext", "prebid", "bidderparams", "pubmatic", "wrapper", "profileid"); err == nil {
+ a := strconv.Itoa(int(profileId))
+ return a
+ }
+ return ""
+}
+
+// modifyRequestBody modifies displaymanger and banner object in req if signal is missing/invalid
+func modifyRequestBody(requestBody []byte) []byte {
+ if body, err := jsonparser.Set(requestBody, []byte(strconv.Quote("PubMatic_OpenWrap_SDK")), "imp", "[0]", "displaymanager"); err == nil {
+ requestBody = jsonparser.Delete(body, "imp", "[0]", "displaymanagerver")
+ }
+
+ if bannertype, err := jsonparser.GetString(requestBody, "imp", "[0]", "banner", "ext", "bannertype"); err == nil && bannertype == models.TypeRewarded {
+ requestBody = jsonparser.Delete(requestBody, "imp", "[0]", "banner")
+ }
+ return requestBody
+}
diff --git a/modules/pubmatic/openwrap/applovinmax_test.go b/modules/pubmatic/openwrap/applovinmax_test.go
index f33a55a6a52..fb9709d1281 100644
--- a/modules/pubmatic/openwrap/applovinmax_test.go
+++ b/modules/pubmatic/openwrap/applovinmax_test.go
@@ -4,8 +4,10 @@ import (
"encoding/json"
"testing"
+ "github.com/golang/mock/gomock"
"github.com/prebid/openrtb/v20/adcom1"
"github.com/prebid/openrtb/v20/openrtb2"
+ mock_metrics "github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/metrics/mock"
"github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/models"
"github.com/prebid/prebid-server/v2/modules/pubmatic/openwrap/models/nbr"
"github.com/prebid/prebid-server/v2/util/ptrutil"
@@ -554,32 +556,55 @@ func TestAddSignalDataInRequest(t *testing.T) {
}
func TestGetSignalData(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ mockEngine := mock_metrics.NewMockMetricsEngine(ctrl)
type args struct {
requestBody []byte
+ rctx models.RequestCtx
}
tests := []struct {
- name string
- args args
- want *openrtb2.BidRequest
+ name string
+ args args
+ setup func()
+ want *openrtb2.BidRequest
}{
{
- name: "incorrect body",
+ name: "incorrect json body",
args: args{
- requestBody: []byte(`{"id":"123","user":Passed","segment":[{"signal":{BIDDING_SIGNA}]}],"ext":{"gdpr":0}}}`),
+ requestBody: []byte(`{"id":"123","user":Passed","segment":[{"signal":{BIDDING_SIGNA}]}],"ext":{"prebid":{"bidderparams":{"pubmatic":{"wrapper":{"profileid":1234}}}}}}}`),
+ rctx: models.RequestCtx{
+ MetricsEngine: mockEngine,
+ },
+ },
+ setup: func() {
+ mockEngine.EXPECT().RecordSignalDataStatus("", "", models.MissingSignal)
},
want: nil,
},
{
name: "signal parsing fail",
args: args{
- requestBody: []byte(`{"id":"123","user":{"data":[{"id":"1","name":"Publisher Passed","segment":[{"signal":"{BIDDING_SIGNA}"]}],"ext":{"gdpr":0}}}`),
+ requestBody: []byte(`{"id":"123","app":{"publisher":{"id":"5890"}},"user":{"data":[{"id":"1","name":"Publisher Passed","segment":[{"signal":"{BIDDING_SIGNAL}"}]}]},"ext":{"prebid":{"bidderparams":{"pubmatic":{"wrapper":{"profileid":1234}}}}}}`),
+ rctx: models.RequestCtx{
+ MetricsEngine: mockEngine,
+ },
+ },
+ setup: func() {
+ mockEngine.EXPECT().RecordSignalDataStatus("5890", "1234", models.InvalidSignal)
},
want: nil,
},
{
name: "single user.data with signal with incorrect signal",
args: args{
- requestBody: []byte(`{"id":"123","user":{"data":[{"id":"1","name":"Publisher Passed","segment":[{"signal":{BIDDING_SIGNA}]}],"ext":{"gdpr":0}}}`),
+ requestBody: []byte(`{"id":"123","user":{"data":[{"id":"1","name":"Publisher Passed","segment":[{"signal":{}}]}]},"ext":{"prebid":{"bidderparams":{"pubmatic":{"wrapper":{"profileid":1234}}}}}}`),
+ rctx: models.RequestCtx{
+ MetricsEngine: mockEngine,
+ },
+ },
+ setup: func() {
+ mockEngine.EXPECT().RecordSignalDataStatus("", "1234", models.InvalidSignal)
},
want: nil,
},
@@ -587,6 +612,9 @@ func TestGetSignalData(t *testing.T) {
name: "single user.data with signal",
args: args{
requestBody: []byte(`{"id":"123","user":{"data":[{"id":"1","name":"Publisher Passed","segment":[{"signal":"{\"device\":{\"devicetype\":4,\"w\":393,\"h\":852}}"}]}],"ext":{"gdpr":0}}}`),
+ rctx: models.RequestCtx{
+ MetricsEngine: mockEngine,
+ },
},
want: &openrtb2.BidRequest{
Device: &openrtb2.Device{
@@ -599,27 +627,41 @@ func TestGetSignalData(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got := getSignalData(tt.args.requestBody)
+ if tt.setup != nil {
+ tt.setup()
+ }
+ got := getSignalData(tt.args.requestBody, tt.args.rctx)
assert.Equal(t, tt.want, got, tt.name)
})
}
}
func TestUpdateMaxAppLovinRequest(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+ mockEngine := mock_metrics.NewMockMetricsEngine(ctrl)
type args struct {
requestBody []byte
+ rctx models.RequestCtx
}
tests := []struct {
- name string
- args args
- want []byte
+ name string
+ args args
+ setup func()
+ want []byte
}{
{
name: "signal not present",
args: args{
- requestBody: []byte(``),
+ requestBody: []byte(`{"id":"1","app":{"publisher":{"id":"5890"}},"user":{"data":[{"segment":[{}]}]},"imp":[{"displaymanager":"applovin_mediation","displaymanagerver":"2.3"}],"ext":{"prebid":{"bidderparams":{"pubmatic":{"wrapper":{"profileid":1234}}}}}}`),
+ rctx: models.RequestCtx{
+ MetricsEngine: mockEngine,
+ },
},
- want: []byte(``),
+ setup: func() {
+ mockEngine.EXPECT().RecordSignalDataStatus("5890", "1234", models.MissingSignal)
+ },
+ want: []byte(`{"id":"1","app":{"publisher":{"id":"5890"}},"user":{"data":[{"segment":[{}]}]},"imp":[{"displaymanager":"PubMatic_OpenWrap_SDK"}],"ext":{"prebid":{"bidderparams":{"pubmatic":{"wrapper":{"profileid":1234}}}}}}`),
},
{
name: "invalid request body",
@@ -638,7 +680,10 @@ func TestUpdateMaxAppLovinRequest(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got := updateAppLovinMaxRequest(tt.args.requestBody)
+ if tt.setup != nil {
+ tt.setup()
+ }
+ got := updateAppLovinMaxRequest(tt.args.requestBody, tt.args.rctx)
assert.Equal(t, tt.want, got, tt.name)
})
}
@@ -961,3 +1006,42 @@ func TestApplyMaxAppLovinResponse(t *testing.T) {
})
}
}
+
+func TestModifyRequestBody(t *testing.T) {
+ type args struct {
+ requestBody []byte
+ }
+ tests := []struct {
+ name string
+ args args
+ want []byte
+ }{
+ {
+ name: "empty requestbody",
+ args: args{
+ requestBody: []byte(``),
+ },
+ want: []byte(``),
+ },
+ {
+ name: "applovinmax displaymanager",
+ args: args{
+ requestBody: []byte(`{"imp":[{"displaymanager":"applovin_mediation","displaymanagerver":"91.1"}]}`),
+ },
+ want: []byte(`{"imp":[{"displaymanager":"PubMatic_OpenWrap_SDK"}]}`),
+ },
+ {
+ name: "applovinmax displaymanager and bannertype rewarded",
+ args: args{
+ requestBody: []byte(`{"imp":[{"displaymanager":"applovin_mediation","displaymanagerver":"91.1","banner":{"ext":{"bannertype":"rewarded"},"format":[{"w":728,"h":90},{"w":300,"h":250}],"w":700,"h":900,"api":[5,6,7]}}]}`),
+ },
+ want: []byte(`{"imp":[{"displaymanager":"PubMatic_OpenWrap_SDK"}]}`),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := modifyRequestBody(tt.args.requestBody)
+ assert.Equal(t, tt.want, got, tt.name)
+ })
+ }
+}
diff --git a/modules/pubmatic/openwrap/auctionresponsehook_test.go b/modules/pubmatic/openwrap/auctionresponsehook_test.go
index c61e8f8dd82..8301a850f85 100644
--- a/modules/pubmatic/openwrap/auctionresponsehook_test.go
+++ b/modules/pubmatic/openwrap/auctionresponsehook_test.go
@@ -1784,7 +1784,7 @@ func TestOpenWrap_handleAuctionResponseHook(t *testing.T) {
want: want{
result: hookstage.HookResult[hookstage.AuctionResponsePayload]{},
err: nil,
- bidResponse: json.RawMessage(`{"id":"12345","seatbid":[{"bid":[{"id":"bid-id-1","impid":"Div1","price":5,"adm":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cImpression\u003e\u003c![CDATA[https:?adv=\u0026af=video\u0026aps=0\u0026au=\u0026bc=pubmatic\u0026bidid=bb57a9e3-fdc2-4772-8071-112dd7f50a6a\u0026di=-1\u0026eg=0\u0026en=0\u0026ft=0\u0026iid=\u0026kgpv=\u0026orig=\u0026origbidid=bid-id-1\u0026pdvid=0\u0026pid=0\u0026plt=0\u0026pn=pubmatic\u0026psz=0x0\u0026pubid=5890\u0026purl=\u0026sl=1\u0026slot=\u0026ss=1\u0026tgid=0\u0026tst=0]]\u003e\u003c/Impression\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e","ext":{"prebid":{"meta":{"adaptercode":"pubmatic","advertiserId":4098,"agencyId":4098,"demandSource":"6","mediaType":"banner","networkId":6},"type":"video","bidid":"bb57a9e3-fdc2-4772-8071-112dd7f50a6a"},"refreshInterval":30,"crtype":"video","video":{"minduration":10,"maxduration":20,"skip":1,"skipmin":1,"skipafter":2,"battr":[1],"playbackmethod":[1]},"dspid":6,"netecpm":5,"origbidcpm":8,"origbidcur":"USD"}}],"seat":"pubmatic"}],"ext":{"responsetimemillis":{"pubmatic":8},"matchedimpression":{"pubmatic":0},"loginfo":{"tracker":"?adv=\u0026af=\u0026aps=0\u0026au=%24%7BADUNIT%7D\u0026bc=%24%7BBIDDER_CODE%7D\u0026bidid=%24%7BBID_ID%7D\u0026di=\u0026eg=%24%7BG_ECPM%7D\u0026en=%24%7BN_ECPM%7D\u0026ft=0\u0026iid=\u0026kgpv=%24%7BKGPV%7D\u0026orig=\u0026origbidid=%24%7BORIGBID_ID%7D\u0026pdvid=0\u0026pid=0\u0026plt=0\u0026pn=%24%7BPARTNER_NAME%7D\u0026psz=\u0026pubid=5890\u0026purl=\u0026rwrd=%24%7BREWARDED%7D\u0026sl=1\u0026slot=%24%7BSLOT_ID%7D\u0026ss=0\u0026tgid=0\u0026tst=0"}}}`),
+ bidResponse: json.RawMessage(`{"id":"12345","seatbid":[{"bid":[{"id":"bid-id-1","impid":"Div1","price":5,"adm":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cImpression\u003e\u003c![CDATA[https:?adv=\u0026af=video\u0026aps=0\u0026au=\u0026bc=pubmatic\u0026bidid=bb57a9e3-fdc2-4772-8071-112dd7f50a6a\u0026di=-1\u0026eg=0\u0026en=0\u0026ft=0\u0026iid=\u0026kgpv=\u0026orig=\u0026origbidid=bid-id-1\u0026pdvid=0\u0026pid=0\u0026plt=0\u0026pn=pubmatic\u0026psz=0x0\u0026pubid=5890\u0026purl=\u0026sl=1\u0026slot=\u0026ss=1\u0026tgid=0\u0026tst=0]]\u003e\u003c/Impression\u003e\u003cExtensions\u003e\u003cExtension\u003e\u003cPricing model=\"CPM\" currency=\"USD\"\u003e\u003c![CDATA[5]]\u003e\u003c/Pricing\u003e\u003c/Extension\u003e\u003c/Extensions\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e","ext":{"prebid":{"meta":{"adaptercode":"pubmatic","advertiserId":4098,"agencyId":4098,"demandSource":"6","mediaType":"banner","networkId":6},"type":"video","bidid":"bb57a9e3-fdc2-4772-8071-112dd7f50a6a"},"refreshInterval":30,"crtype":"video","video":{"minduration":10,"maxduration":20,"skip":1,"skipmin":1,"skipafter":2,"battr":[1],"playbackmethod":[1]},"dspid":6,"netecpm":5,"origbidcpm":8,"origbidcur":"USD"}}],"seat":"pubmatic"}],"ext":{"responsetimemillis":{"pubmatic":8},"matchedimpression":{"pubmatic":0},"loginfo":{"tracker":"?adv=\u0026af=\u0026aps=0\u0026au=%24%7BADUNIT%7D\u0026bc=%24%7BBIDDER_CODE%7D\u0026bidid=%24%7BBID_ID%7D\u0026di=\u0026eg=%24%7BG_ECPM%7D\u0026en=%24%7BN_ECPM%7D\u0026ft=0\u0026iid=\u0026kgpv=%24%7BKGPV%7D\u0026orig=\u0026origbidid=%24%7BORIGBID_ID%7D\u0026pdvid=0\u0026pid=0\u0026plt=0\u0026pn=%24%7BPARTNER_NAME%7D\u0026psz=\u0026pubid=5890\u0026purl=\u0026rwrd=%24%7BREWARDED%7D\u0026sl=1\u0026slot=%24%7BSLOT_ID%7D\u0026ss=0\u0026tgid=0\u0026tst=0"}}}`),
},
},
}
diff --git a/modules/pubmatic/openwrap/entrypointhook.go b/modules/pubmatic/openwrap/entrypointhook.go
index 215289f698d..b9e8d4f6b51 100644
--- a/modules/pubmatic/openwrap/entrypointhook.go
+++ b/modules/pubmatic/openwrap/entrypointhook.go
@@ -62,8 +62,9 @@ func (m OpenWrap) handleEntrypointHook(
}
if endpoint == models.EndpointAppLovinMax {
+ rCtx.MetricsEngine = m.metricEngine
// updating body locally to access updated fields from signal
- payload.Body = updateAppLovinMaxRequest(payload.Body)
+ payload.Body = updateAppLovinMaxRequest(payload.Body, rCtx)
result.ChangeSet.AddMutation(func(ep hookstage.EntrypointPayload) (hookstage.EntrypointPayload, error) {
ep.Body = payload.Body
return ep, nil
diff --git a/modules/pubmatic/openwrap/metrics/config/multimetrics.go b/modules/pubmatic/openwrap/metrics/config/multimetrics.go
index fe6788e28c1..a84ca53047a 100644
--- a/modules/pubmatic/openwrap/metrics/config/multimetrics.go
+++ b/modules/pubmatic/openwrap/metrics/config/multimetrics.go
@@ -494,3 +494,24 @@ func (me *MultiMetricsEngine) RecordAnalyticsTrackingThrottled(pubid, profileid,
thisME.RecordAnalyticsTrackingThrottled(pubid, profileid, analyticsType)
}
}
+
+// RecordAdruleEnabled across all engines
+func (me *MultiMetricsEngine) RecordAdruleEnabled(publisher, profile string) {
+ for _, thisME := range *me {
+ thisME.RecordAdruleEnabled(publisher, profile)
+ }
+}
+
+// RecordAdruleValidationFailure across all engines
+func (me *MultiMetricsEngine) RecordAdruleValidationFailure(publisher, profile string) {
+ for _, thisME := range *me {
+ thisME.RecordAdruleValidationFailure(publisher, profile)
+ }
+}
+
+// RecordSignalDataStatus record signaldata status(invalid,missing) at publisher level
+func (me *MultiMetricsEngine) RecordSignalDataStatus(pubid, profileid, signalType string) {
+ for _, thisME := range *me {
+ thisME.RecordSignalDataStatus(pubid, profileid, signalType)
+ }
+}
diff --git a/modules/pubmatic/openwrap/metrics/config/multimetrics_test.go b/modules/pubmatic/openwrap/metrics/config/multimetrics_test.go
index 7a677de5c41..2b206de8715 100644
--- a/modules/pubmatic/openwrap/metrics/config/multimetrics_test.go
+++ b/modules/pubmatic/openwrap/metrics/config/multimetrics_test.go
@@ -207,7 +207,10 @@ func TestRecordFunctionForMultiMetricsEngine(t *testing.T) {
mockEngine.EXPECT().RecordSendLoggerDataTime(sendTime)
mockEngine.EXPECT().RecordDBQueryFailure(queryType, publisher, profile)
mockEngine.EXPECT().RecordAnalyticsTrackingThrottled(publisher, profile, "logger")
+ mockEngine.EXPECT().RecordSignalDataStatus(publisher, profile, models.MissingSignal)
mockEngine.EXPECT().Shutdown()
+ mockEngine.EXPECT().RecordAdruleEnabled(publisher, profile)
+ mockEngine.EXPECT().RecordAdruleValidationFailure(publisher, profile)
mockEngine.EXPECT().RecordRequest(metrics.Labels{RType: "video", RequestStatus: "success"})
mockEngine.EXPECT().RecordLurlSent(metrics.LurlStatusLabels{PublisherID: "pubid", Partner: "p", Status: "success"})
@@ -272,7 +275,10 @@ func TestRecordFunctionForMultiMetricsEngine(t *testing.T) {
multiMetricEngine.RecordSendLoggerDataTime(sendTime)
multiMetricEngine.RecordDBQueryFailure(queryType, publisher, profile)
multiMetricEngine.RecordAnalyticsTrackingThrottled(publisher, profile, "logger")
+ multiMetricEngine.RecordSignalDataStatus(publisher, profile, models.MissingSignal)
multiMetricEngine.Shutdown()
+ multiMetricEngine.RecordAdruleEnabled(publisher, profile)
+ multiMetricEngine.RecordAdruleValidationFailure(publisher, profile)
multiMetricEngine.RecordRequest(metrics.Labels{RType: "video", RequestStatus: "success"})
multiMetricEngine.RecordLurlSent(metrics.LurlStatusLabels{PublisherID: "pubid", Partner: "p", Status: "success"})
diff --git a/modules/pubmatic/openwrap/metrics/metrics.go b/modules/pubmatic/openwrap/metrics/metrics.go
index 24c75136846..f08bf76c450 100644
--- a/modules/pubmatic/openwrap/metrics/metrics.go
+++ b/modules/pubmatic/openwrap/metrics/metrics.go
@@ -75,10 +75,15 @@ type MetricsEngine interface {
RecordAmpVideoRequests(pubid, profileid string)
RecordAmpVideoResponses(pubid, profileid string)
RecordAnalyticsTrackingThrottled(pubid, profileid, analyticsType string)
+ RecordSignalDataStatus(pubid, profileid, signalType string)
// VAST Unwrap metrics
RecordUnwrapRequestStatus(accountId, bidder, status string)
RecordUnwrapWrapperCount(accountId, bidder string, wrapper_count string)
RecordUnwrapRequestTime(accountId, bidder string, respTime time.Duration)
RecordUnwrapRespTime(accountId, wraperCnt string, respTime time.Duration)
+
+ //VMAP-adrule
+ RecordAdruleEnabled(pubId, profId string)
+ RecordAdruleValidationFailure(pubId, profId string)
}
diff --git a/modules/pubmatic/openwrap/metrics/mock/mock.go b/modules/pubmatic/openwrap/metrics/mock/mock.go
index 1d600c020cd..1f2e5bb6062 100644
--- a/modules/pubmatic/openwrap/metrics/mock/mock.go
+++ b/modules/pubmatic/openwrap/metrics/mock/mock.go
@@ -94,6 +94,18 @@ func (mr *MockMetricsEngineMockRecorder) RecordAnalyticsTrackingThrottled(arg0,
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordAnalyticsTrackingThrottled", reflect.TypeOf((*MockMetricsEngine)(nil).RecordAnalyticsTrackingThrottled), arg0, arg1, arg2)
}
+// RecordSignalDataStatus mocks base method.
+func (m *MockMetricsEngine) RecordSignalDataStatus(arg0, arg1, arg2 string) {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "RecordSignalDataStatus", arg0, arg1, arg2)
+}
+
+// RecordSignalDataStatus indicates an expected call of RecordSignalDataStatus.
+func (mr *MockMetricsEngineMockRecorder) RecordSignalDataStatus(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordSignalDataStatus", reflect.TypeOf((*MockMetricsEngine)(nil).RecordSignalDataStatus), arg0, arg1, arg2)
+}
+
// RecordBadRequests mocks base method.
func (m *MockMetricsEngine) RecordBadRequests(arg0 string, arg1 int) {
m.ctrl.T.Helper()
@@ -765,3 +777,27 @@ func (mr *MockMetricsEngineMockRecorder) Shutdown() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockMetricsEngine)(nil).Shutdown))
}
+
+// RecordAdruleEnabled mocks base method
+func (m *MockMetricsEngine) RecordAdruleEnabled(arg0, arg1 string) {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "RecordAdruleEnabled", arg0, arg1)
+}
+
+// RecordAdruleEnabled indicates an expected call of RecordAdruleEnabled
+func (mr *MockMetricsEngineMockRecorder) RecordAdruleEnabled(arg0, arg1 string) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordAdruleEnabled", reflect.TypeOf((*MockMetricsEngine)(nil).RecordAdruleEnabled), arg0, arg1)
+}
+
+// RecordAdruleValidationFailure mocks base method
+func (m *MockMetricsEngine) RecordAdruleValidationFailure(arg0, arg1 string) {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "RecordAdruleValidationFailure", arg0, arg1)
+}
+
+// RecordAdruleValidationFailure indicates an expected call of RecordAdruleValidationFailure
+func (mr *MockMetricsEngineMockRecorder) RecordAdruleValidationFailure(arg0, arg1 string) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordAdruleValidationFailure", reflect.TypeOf((*MockMetricsEngine)(nil).RecordAdruleValidationFailure), arg0, arg1)
+}
diff --git a/modules/pubmatic/openwrap/metrics/prometheus/prometheus.go b/modules/pubmatic/openwrap/metrics/prometheus/prometheus.go
index 4e68a07102e..4c3f1f6bf94 100644
--- a/modules/pubmatic/openwrap/metrics/prometheus/prometheus.go
+++ b/modules/pubmatic/openwrap/metrics/prometheus/prometheus.go
@@ -70,12 +70,17 @@ type Metrics struct {
ampVideoRequests *prometheus.CounterVec
ampVideoResponses *prometheus.CounterVec
analyticsThrottle *prometheus.CounterVec
+ signalStatus *prometheus.CounterVec
// VAST Unwrap
requests *prometheus.CounterVec
wrapperCount *prometheus.CounterVec
requestTime *prometheus.HistogramVec
unwrapRespTime *prometheus.HistogramVec
+
+ //VMAP adrule
+ pubProfAdruleEnabled *prometheus.CounterVec
+ pubProfAdruleValidationfailure *prometheus.CounterVec
}
const (
@@ -94,6 +99,7 @@ const (
methodLabel = "method"
queryTypeLabel = "query_type"
analyticsTypeLabel = "an_type"
+ signalTypeLabel = "signal_status"
)
var standardTimeBuckets = []float64{0.1, 0.3, 0.75, 1}
@@ -262,6 +268,11 @@ func newMetrics(cfg *config.PrometheusMetrics, promRegistry *prometheus.Registry
"Count of throttled analytics logger and tracker requestss",
[]string{pubIDLabel, profileIDLabel, analyticsTypeLabel})
+ metrics.signalStatus = newCounter(cfg, promRegistry,
+ "signal_status",
+ "Count signal status for applovinmax requests",
+ []string{pubIDLabel, profileIDLabel, signalTypeLabel})
+
metrics.requests = newCounter(cfg, promRegistry,
"vastunwrap_status",
"Count of vast unwrap requests labeled by status",
@@ -500,6 +511,15 @@ func (m *Metrics) RecordAnalyticsTrackingThrottled(pubid, profileid, analyticsTy
}).Inc()
}
+// RecordSignalDataStatus record signaldata status(invalid,missing) at publisher level
+func (m *Metrics) RecordSignalDataStatus(pubid, profileid, signalType string) {
+ m.signalStatus.With(prometheus.Labels{
+ pubIDLabel: pubid,
+ profileIDLabel: profileid,
+ signalTypeLabel: signalType,
+ }).Inc()
+}
+
// TODO - really need ?
func (m *Metrics) RecordPBSAuctionRequestsStats() {}
diff --git a/modules/pubmatic/openwrap/metrics/prometheus/prometheus_sshb.go b/modules/pubmatic/openwrap/metrics/prometheus/prometheus_sshb.go
index d8b9e7506fe..3930ed6867e 100644
--- a/modules/pubmatic/openwrap/metrics/prometheus/prometheus_sshb.go
+++ b/modules/pubmatic/openwrap/metrics/prometheus/prometheus_sshb.go
@@ -118,6 +118,16 @@ func newSSHBMetrics(metrics *Metrics, cfg *config.PrometheusMetrics, promRegistr
"Counts the AMP video responses labeled by pub id and profile id.",
[]string{pubIDLabel, profileIDLabel})
+ metrics.pubProfAdruleEnabled = newCounter(cfg, promRegistry,
+ "sshb_adrule_enable",
+ "Count of request where adRule is present",
+ []string{pubIdLabel, profileLabel})
+
+ metrics.pubProfAdruleValidationfailure = newCounter(cfg, promRegistry,
+ "sshb_invalid_adrule",
+ "Count of request where adRule is invalid",
+ []string{pubIdLabel, profileLabel})
+
preloadLabelValues(metrics)
}
@@ -252,6 +262,22 @@ func (m *Metrics) RecordUnwrapRespTime(accountId, wraperCnt string, respTime tim
}).Observe(float64(respTime.Milliseconds()))
}
+// RecordAdruleEnabled records count of request in which adrule is present based on pubid and profileid
+func (m *Metrics) RecordAdruleEnabled(pubid, profileid string) {
+ m.pubProfAdruleEnabled.With(prometheus.Labels{
+ pubIdLabel: pubid,
+ profileLabel: profileid,
+ }).Inc()
+}
+
+// RecordAdruleValidationFailure records count of request in which adrule validation fails based on pubid and profileid
+func (m *Metrics) RecordAdruleValidationFailure(pubid, profileid string) {
+ m.pubProfAdruleValidationfailure.With(prometheus.Labels{
+ pubIdLabel: pubid,
+ profileLabel: profileid,
+ }).Inc()
+}
+
func preloadLabelValues(m *Metrics) {
var (
requestStatusValues = requestStatusesAsString()
diff --git a/modules/pubmatic/openwrap/metrics/prometheus/prometheus_test.go b/modules/pubmatic/openwrap/metrics/prometheus/prometheus_test.go
index 59e81b9c010..03078309cae 100644
--- a/modules/pubmatic/openwrap/metrics/prometheus/prometheus_test.go
+++ b/modules/pubmatic/openwrap/metrics/prometheus/prometheus_test.go
@@ -325,6 +325,21 @@ func TestRecordDBQueryFailure(t *testing.T) {
})
}
+func TestRecordSignalDataStatus(t *testing.T) {
+ m := createMetricsForTesting()
+
+ m.RecordSignalDataStatus("5890", "1234", models.MissingSignal)
+
+ expectedCount := float64(1)
+ assertCounterVecValue(t, "", "signal_status", m.signalStatus,
+ expectedCount,
+ prometheus.Labels{
+ pubIDLabel: "5890",
+ profileIDLabel: "1234",
+ signalTypeLabel: models.MissingSignal,
+ })
+}
+
func getHistogramFromHistogram(histogram prometheus.Histogram) dto.Histogram {
var result dto.Histogram
processMetrics(histogram, func(m dto.Metric) {
@@ -333,6 +348,32 @@ func getHistogramFromHistogram(histogram prometheus.Histogram) dto.Histogram {
return result
}
+func TestRecordAdruleEnabled(t *testing.T) {
+ m := createMetricsForTesting()
+
+ m.RecordAdruleEnabled("5890", "123")
+
+ expectedCount := float64(1)
+ assertCounterVecValue(t, "", "pubProfAdruleEnabled", m.pubProfAdruleEnabled,
+ expectedCount, prometheus.Labels{
+ pubIdLabel: "5890",
+ profileLabel: "123",
+ })
+}
+
+func TestRecordAdruleValidationFailure(t *testing.T) {
+ m := createMetricsForTesting()
+
+ m.RecordAdruleValidationFailure("5890", "123")
+
+ expectedCount := float64(1)
+ assertCounterVecValue(t, "", "pubProfAdruleValidationfailure", m.pubProfAdruleValidationfailure,
+ expectedCount, prometheus.Labels{
+ pubIdLabel: "5890",
+ profileLabel: "123",
+ })
+}
+
func getHistogramFromHistogramVec(histogram *prometheus.HistogramVec, labelKey, labelValue string) dto.Histogram {
var result dto.Histogram
processMetrics(histogram, func(m dto.Metric) {
diff --git a/modules/pubmatic/openwrap/metrics/stats/tcp_stats.go b/modules/pubmatic/openwrap/metrics/stats/tcp_stats.go
index 0003f7f8c07..e5c88574561 100644
--- a/modules/pubmatic/openwrap/metrics/stats/tcp_stats.go
+++ b/modules/pubmatic/openwrap/metrics/stats/tcp_stats.go
@@ -346,3 +346,6 @@ func (st *StatsTCP) RecordUnwrapWrapperCount(accountId, bidder, wrapper_count st
func (st *StatsTCP) RecordUnwrapRequestTime(accountId, bidder string, respTime time.Duration) {}
func (st *StatsTCP) RecordUnwrapRespTime(accountId, wraperCnt string, respTime time.Duration) {}
func (st *StatsTCP) RecordAnalyticsTrackingThrottled(pubid, profileid, analyticsType string) {}
+func (st *StatsTCP) RecordAdruleEnabled(pubId, profId string) {}
+func (st *StatsTCP) RecordAdruleValidationFailure(pubId, profId string) {}
+func (st *StatsTCP) RecordSignalDataStatus(pubid, profileid, signalType string) {}
diff --git a/modules/pubmatic/openwrap/models/adunitconfig/adunitconfig.go b/modules/pubmatic/openwrap/models/adunitconfig/adunitconfig.go
index 9a6ef9c0590..302ce9b316e 100644
--- a/modules/pubmatic/openwrap/models/adunitconfig/adunitconfig.go
+++ b/modules/pubmatic/openwrap/models/adunitconfig/adunitconfig.go
@@ -73,14 +73,15 @@ type AdConfig struct {
BidFloorCur *string `json:"bidfloorcur,omitempty"`
Floors *openrtb_ext.PriceFloorRules `json:"floors,omitempty"`
- Exp *int `json:"exp,omitempty"`
- Banner *Banner `json:"banner,omitempty"`
- Native *Native `json:"native,omitempty"`
- Video *Video `json:"video,omitempty"`
- App *openrtb2.App `json:"app,omitempty"`
- Device *openrtb2.Device `json:"device,omitempty"`
- Transparency *Transparency `json:"transparency,omitempty"`
- Regex *bool `json:"regex,omitempty"`
- UniversalPixel []UniversalPixel `json:"universalpixel,omitempty"`
- EnableGAMUrlLookup bool `json:"enablegamurllookup,omitempty"`
+ Exp *int `json:"exp,omitempty"`
+ Banner *Banner `json:"banner,omitempty"`
+ Native *Native `json:"native,omitempty"`
+ Video *Video `json:"video,omitempty"`
+ App *openrtb2.App `json:"app,omitempty"`
+ Device *openrtb2.Device `json:"device,omitempty"`
+ Transparency *Transparency `json:"transparency,omitempty"`
+ Regex *bool `json:"regex,omitempty"`
+ UniversalPixel []UniversalPixel `json:"universalpixel,omitempty"`
+ EnableGAMUrlLookup bool `json:"enablegamurllookup,omitempty"`
+ Adrule []*openrtb2.Video `json:"adrule,omitempty"`
}
diff --git a/modules/pubmatic/openwrap/models/constants.go b/modules/pubmatic/openwrap/models/constants.go
index 0d0d56ba230..9793f16a5d7 100755
--- a/modules/pubmatic/openwrap/models/constants.go
+++ b/modules/pubmatic/openwrap/models/constants.go
@@ -541,4 +541,6 @@ const (
TypeRewarded = "rewarded"
SignalData = "signaldata"
OwSspBurl = "owsspburl"
+ MissingSignal = "missing"
+ InvalidSignal = "invalid"
)
diff --git a/modules/pubmatic/openwrap/models/video.go b/modules/pubmatic/openwrap/models/video.go
index fa87aff6a2d..8689db333fb 100644
--- a/modules/pubmatic/openwrap/models/video.go
+++ b/modules/pubmatic/openwrap/models/video.go
@@ -7,6 +7,8 @@ const (
VideoVASTVersion = "version"
//VideoVASTVersion2_0 video version 2.0 parameter constant
VideoVASTVersion2_0 = "2.0"
+ //VideoVASTVersion3_0 video version 3.0 parameter constant
+ VideoVASTVersion3_0 = "3.0"
//VideoVASTAdWrapperTag video ad/wrapper element constant
VideoVASTAdWrapperTag = "./Ad/Wrapper"
//VideoVASTAdInLineTag video ad/inline element constant
diff --git a/modules/pubmatic/openwrap/tracker/inject_test.go b/modules/pubmatic/openwrap/tracker/inject_test.go
index 97c054f5f0c..0eed8b3e37a 100644
--- a/modules/pubmatic/openwrap/tracker/inject_test.go
+++ b/modules/pubmatic/openwrap/tracker/inject_test.go
@@ -652,6 +652,265 @@ func TestInjectTrackers(t *testing.T) {
},
wantErr: false,
},
+ {
+ name: "VAST_3_0_Wrapper",
+ args: args{
+ rctx: models.RequestCtx{
+ Platform: "",
+ Trackers: map[string]models.OWTracker{
+ "12345": {
+ BidType: "video",
+ TrackerURL: `Tracking URL`,
+ ErrorURL: `Error URL`,
+ Price: 1.2,
+ },
+ },
+ },
+ bidResponse: &openrtb2.BidResponse{
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "12345",
+ AdM: `prebid.org wrapper`,
+ Price: 1.2,
+ },
+ },
+ },
+ },
+ },
+ },
+ want: &openrtb2.BidResponse{
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "12345",
+ AdM: ``,
+ Price: 1.2,
+ },
+ },
+ },
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "VAST_3_0_Inline",
+ args: args{
+ rctx: models.RequestCtx{
+ Platform: "",
+ Trackers: map[string]models.OWTracker{
+ "12345": {
+ BidType: "video",
+ TrackerURL: `Tracking URL`,
+ ErrorURL: `Error URL`,
+ Price: 1.5,
+ },
+ },
+ },
+ bidResponse: &openrtb2.BidResponse{
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "12345",
+ AdM: `iabtechlabiabtechlab video adhttp://example.com/errorhttp://example.com/track/impression00:00:16http://example.com/tracking/starthttp://example.com/tracking/firstQuartilehttp://example.com/tracking/midpointhttp://example.com/tracking/thirdQuartilehttp://example.com/tracking/completehttp://example.com/tracking/progress-10http://iabtechlab.com`,
+ Price: 1.5,
+ },
+ },
+ },
+ },
+ },
+ },
+ want: &openrtb2.BidResponse{
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "12345",
+ AdM: ``,
+ Price: 1.5,
+ },
+ },
+ },
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "VAST_2_0_Wrapper",
+ args: args{
+ rctx: models.RequestCtx{
+ Platform: "",
+ Trackers: map[string]models.OWTracker{
+ "12345": {
+ BidType: "video",
+ TrackerURL: `Tracking URL`,
+ ErrorURL: `Error URL`,
+ Price: 1.9,
+ },
+ },
+ },
+ bidResponse: &openrtb2.BidResponse{
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "12345",
+ AdM: `TestSystemhttp://demo.test.com/proddev/vast/vast_inline_linear.xml http://test.com/wrapper/errorhttp://test.com/trackingurl/wrapper/impressionhttp://test.com/trackingurl/wrapper/creativeViewhttp://test.com/trackingurl/wrapper/starthttp://test.com/trackingurl/wrapper/midpointhttp://test.com/trackingurl/wrapper/firstQuartilehttp://test.com/trackingurl/wrapper/thirdQuartilehttp://test.com/trackingurl/wrapper/completehttp://test.com/trackingurl/wrapper/mutehttp://test.com/trackingurl/wrapper/unmutehttp://test.com/trackingurl/wrapper/pausehttp://test.com/trackingurl/wrapper/resumehttp://test.com/trackingurl/wrapper/fullscreenhttp://test.com/trackingurl/wrapper/click`,
+ Price: 1.9,
+ },
+ },
+ },
+ },
+ },
+ },
+ want: &openrtb2.BidResponse{
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "12345",
+ AdM: ``,
+ Price: 1.9,
+ },
+ },
+ },
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "VAST_2_0_Inline",
+ args: args{
+ rctx: models.RequestCtx{
+ Platform: "",
+ Trackers: map[string]models.OWTracker{
+ "12345": {
+ BidType: "video",
+ TrackerURL: `Tracking URL`,
+ ErrorURL: `Error URL`,
+ Price: 2.5,
+ },
+ },
+ },
+ bidResponse: &openrtb2.BidResponse{
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "12345",
+ AdM: `2.01234500:00:15]]>`,
+ Price: 2.5,
+ },
+ },
+ },
+ },
+ },
+ },
+ want: &openrtb2.BidResponse{
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "12345",
+ AdM: `]]>`,
+ Price: 2.5,
+ },
+ },
+ },
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "VAST_4_0_Wrapper",
+ args: args{
+ rctx: models.RequestCtx{
+ Platform: "",
+ Trackers: map[string]models.OWTracker{
+ "12345": {
+ BidType: "video",
+ TrackerURL: `Tracking URL`,
+ ErrorURL: `Error URL`,
+ Price: 12.5,
+ },
+ },
+ },
+ bidResponse: &openrtb2.BidResponse{
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "12345",
+ AdM: `iabtechlabhttp://example.com/errorhttp://example.com/track/impression`,
+ Price: 12.5,
+ },
+ },
+ },
+ },
+ },
+ },
+ want: &openrtb2.BidResponse{
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "12345",
+ AdM: ``,
+ Price: 12.5,
+ },
+ },
+ },
+ },
+ },
+ wantErr: false,
+ },
+
+ {
+ name: "VAST_4_0_Inline",
+ args: args{
+ rctx: models.RequestCtx{
+ Platform: "",
+ Trackers: map[string]models.OWTracker{
+ "12345": {
+ BidType: "video",
+ TrackerURL: `Tracking URL`,
+ ErrorURL: `Error URL`,
+ Price: 15.7,
+ },
+ },
+ },
+ bidResponse: &openrtb2.BidResponse{
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "12345",
+ AdM: `iabtechlabhttp://example.com/errorhttp://example.com/track/impressioniabtechlab video adAD CONTENT description category8465http://example.com/tracking/starthttp://example.com/tracking/firstQuartilehttp://example.com/tracking/midpointhttp://example.com/tracking/thirdQuartilehttp://example.com/tracking/completehttp://example.com/tracking/progress-1000:00:16`,
+ Price: 15.7,
+ },
+ },
+ },
+ },
+ },
+ },
+ want: &openrtb2.BidResponse{
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "12345",
+ AdM: ``,
+ Price: 15.7,
+ },
+ },
+ },
+ },
+ },
+ wantErr: false,
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
diff --git a/modules/pubmatic/openwrap/tracker/video.go b/modules/pubmatic/openwrap/tracker/video.go
index 09f75afb3a8..f2542f9bf44 100644
--- a/modules/pubmatic/openwrap/tracker/video.go
+++ b/modules/pubmatic/openwrap/tracker/video.go
@@ -80,11 +80,11 @@ func injectVideoCreativeTrackers(rctx models.RequestCtx, bid openrtb2.Bid, video
element.InsertChild(element.SelectElement(models.ErrorElement), newElement)
}
- if !isWrapper && videoParams[i].Price != 0 {
- if models.VideoVASTVersion2_0 == version {
- injectPricingNodeVAST20(element, videoParams[i].Price, videoParams[i].PriceModel, videoParams[i].PriceCurrency)
+ if videoParams[i].Price != 0 {
+ if (version == models.VideoVASTVersion2_0) || (isWrapper && version == models.VideoVASTVersion3_0) {
+ InjectPricingNodeInExtension(element, videoParams[i].Price, videoParams[i].PriceModel, videoParams[i].PriceCurrency)
} else {
- injectPricingNodeVAST3x(element, videoParams[i].Price, videoParams[i].PriceModel, videoParams[i].PriceCurrency)
+ InjectPricingNodeInVAST(element, videoParams[i].Price, videoParams[i].PriceModel, videoParams[i].PriceCurrency)
}
}
}
@@ -99,7 +99,7 @@ func injectVideoCreativeTrackers(rctx models.RequestCtx, bid openrtb2.Bid, video
return bid.AdM, bid.BURL, nil
}
-func injectPricingNodeVAST20(parent *etree.Element, price float64, model string, currency string) {
+func InjectPricingNodeInExtension(parent *etree.Element, price float64, model string, currency string) {
extensions := parent.FindElement(models.VideoTagLookupStart + models.VideoExtensionsTag)
if nil == extensions {
extensions = parent.CreateElement(models.VideoExtensionsTag)
@@ -115,7 +115,7 @@ func injectPricingNodeVAST20(parent *etree.Element, price float64, model string,
}
}
-func injectPricingNodeVAST3x(parent *etree.Element, price float64, model string, currency string) {
+func InjectPricingNodeInVAST(parent *etree.Element, price float64, model string, currency string) {
//Insert into Wrapper Elements
pricing := parent.FindElement(models.VideoTagLookupStart + models.VideoPricingTag)
if nil != pricing {
diff --git a/modules/pubmatic/openwrap/tracker/video_test.go b/modules/pubmatic/openwrap/tracker/video_test.go
index 1b3c188b3b9..7a4599335d5 100644
--- a/modules/pubmatic/openwrap/tracker/video_test.go
+++ b/modules/pubmatic/openwrap/tracker/video_test.go
@@ -185,6 +185,42 @@ func TestInjectVideoCreativeTrackers(t *testing.T) {
wantAdm: ``,
wantErr: false,
},
+ {
+ name: "wrapper_vast_4.0",
+ args: args{
+
+ bid: openrtb2.Bid{
+ AdM: `iabtechlabhttp://example.com/errorhttp://example.com/track/impression`,
+ },
+ videoParams: []models.OWTracker{
+ {
+ TrackerURL: `Tracker URL`,
+ ErrorURL: `Error URL`,
+ Price: 1.2,
+ },
+ },
+ },
+ wantAdm: ``,
+ wantErr: false,
+ },
+ {
+ name: "inline_vast_4.0",
+ args: args{
+
+ bid: openrtb2.Bid{
+ AdM: `iabtechlabhttp://example.com/errorhttp://example.com/track/impressioniabtechlab video adAD CONTENT description category8465http://example.com/tracking/starthttp://example.com/tracking/firstQuartilehttp://example.com/tracking/midpointhttp://example.com/tracking/thirdQuartilehttp://example.com/tracking/completehttp://example.com/tracking/progress-1000:00:16`,
+ },
+ videoParams: []models.OWTracker{
+ {
+ TrackerURL: `Tracker URL`,
+ ErrorURL: `Error URL`,
+ Price: 1.2,
+ },
+ },
+ },
+ wantAdm: ``,
+ wantErr: false,
+ },
{
name: "wrapper_vast_2.0",
args: args{
@@ -200,7 +236,7 @@ func TestInjectVideoCreativeTrackers(t *testing.T) {
},
},
},
- wantAdm: ``,
+ wantAdm: ``,
wantErr: false,
},
{
@@ -236,7 +272,7 @@ func TestInjectVideoCreativeTrackers(t *testing.T) {
},
},
},
- wantAdm: ``,
+ wantAdm: ``,
wantErr: false,
},
{
@@ -345,7 +381,7 @@ func TestInjectVideoCreativeTrackers(t *testing.T) {
},
},
wantBurl: "Tracker URL&owsspburl=https://burl.com",
- wantAdm: ``,
+ wantAdm: ``,
wantErr: false,
},
{
@@ -391,7 +427,7 @@ func TestInjectVideoCreativeTrackers(t *testing.T) {
},
},
wantBurl: "Tracker URL&owsspburl=https://burl.com",
- wantAdm: ``,
+ wantAdm: ``,
wantErr: false,
},
{
@@ -439,7 +475,7 @@ func getXMLDocument(tag string) *etree.Document {
return doc
}
-func Test_injectPricingNodeVAST20(t *testing.T) {
+func Test_injectPricingNodeInExtension0(t *testing.T) {
type args struct {
doc *etree.Document
price float64
@@ -524,14 +560,14 @@ func Test_injectPricingNodeVAST20(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- injectPricingNodeVAST20(&tt.args.doc.Element, tt.args.price, tt.args.model, tt.args.currency)
+ InjectPricingNodeInExtension(&tt.args.doc.Element, tt.args.price, tt.args.model, tt.args.currency)
actual, _ := tt.args.doc.WriteToString()
assert.Equal(t, tt.want, actual)
})
}
}
-func Test_injectPricingNodeVAST3x(t *testing.T) {
+func Test_injectPricingNodeInVAST(t *testing.T) {
type args struct {
doc *etree.Document
price float64
@@ -596,7 +632,7 @@ func Test_injectPricingNodeVAST3x(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- injectPricingNodeVAST3x(&tt.args.doc.Element, tt.args.price, tt.args.model, tt.args.currency)
+ InjectPricingNodeInVAST(&tt.args.doc.Element, tt.args.price, tt.args.model, tt.args.currency)
actual, _ := tt.args.doc.WriteToString()
assert.Equal(t, tt.want, actual)
})
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 163c893415d..f36e7d0e10f 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -147,6 +147,7 @@ var coreBidderNames []BidderName = []BidderName{
BidderMotorik,
BidderNextMillennium,
BidderNoBid,
+ BidderORTBTestBidder, // maintained by OW
BidderOms,
BidderOneTag,
BidderOpenWeb,
@@ -466,7 +467,6 @@ const (
BidderSovrn BidderName = "sovrn"
BidderSovrnXsp BidderName = "sovrnXsp"
BidderSspBC BidderName = "sspBC"
- BidderSpotX BidderName = "spotx"
BidderStreamkey BidderName = "streamkey"
BidderStroeerCore BidderName = "stroeerCore"
BidderTaboola BidderName = "taboola"
@@ -482,7 +482,6 @@ const (
BidderUnicorn BidderName = "unicorn"
BidderUnruly BidderName = "unruly"
BidderValueImpression BidderName = "valueimpression"
- BidderVASTBidder BidderName = "vastbidder"
BidderVideoByte BidderName = "videobyte"
BidderVideoHeroes BidderName = "videoheroes"
BidderVidoomy BidderName = "vidoomy"
diff --git a/openrtb_ext/bidders_ow.go b/openrtb_ext/bidders_ow.go
new file mode 100644
index 00000000000..4251ebd8caa
--- /dev/null
+++ b/openrtb_ext/bidders_ow.go
@@ -0,0 +1,8 @@
+package openrtb_ext
+
+// constant of OW specific bidders
+const (
+ BidderORTBTestBidder BidderName = "owortb_testbidder"
+ BidderSpotX BidderName = "spotx"
+ BidderVASTBidder BidderName = "vastbidder"
+)
diff --git a/openrtb_ext/openwrap.go b/openrtb_ext/openwrap.go
index 47670d30cd8..2b37f513953 100644
--- a/openrtb_ext/openwrap.go
+++ b/openrtb_ext/openwrap.go
@@ -72,7 +72,7 @@ type ExtVideoAdPod struct {
// ExtRequestAdPod holds AdPod specific extension parameters at request level
type ExtRequestAdPod struct {
- VideoAdPod
+ *VideoAdPod
CrossPodAdvertiserExclusionPercent *int `json:"crosspodexcladv,omitempty"` //Percent Value - Across multiple impression there will be no ads from same advertiser. Note: These cross pod rule % values can not be more restrictive than per pod
CrossPodIABCategoryExclusionPercent *int `json:"crosspodexcliabcat,omitempty"` //Percent Value - Across multiple impression there will be no ads from same advertiser
IABCategoryExclusionWindow *int `json:"excliabcatwindow,omitempty"` //Duration in minute between pods where exclusive IAB rule needs to be applied
@@ -118,35 +118,39 @@ func getIntPtr(v int) *int {
// Validate will validate AdPod object
func (pod *VideoAdPod) Validate() (err []error) {
- if nil != pod.MinAds && *pod.MinAds <= 0 {
+ if pod == nil {
+ return
+ }
+
+ if pod.MinAds != nil && *pod.MinAds <= 0 {
err = append(err, errInvalidMinAds)
}
- if nil != pod.MaxAds && *pod.MaxAds <= 0 {
+ if pod.MaxAds != nil && *pod.MaxAds <= 0 {
err = append(err, errInvalidMaxAds)
}
- if nil != pod.MinDuration && *pod.MinDuration <= 0 {
+ if pod.MinDuration != nil && *pod.MinDuration < 0 {
err = append(err, errInvalidMinDuration)
}
- if nil != pod.MaxDuration && *pod.MaxDuration <= 0 {
+ if pod.MaxDuration != nil && *pod.MaxDuration <= 0 {
err = append(err, errInvalidMaxDuration)
}
- if nil != pod.AdvertiserExclusionPercent && (*pod.AdvertiserExclusionPercent < 0 || *pod.AdvertiserExclusionPercent > 100) {
+ if pod.AdvertiserExclusionPercent != nil && (*pod.AdvertiserExclusionPercent < 0 || *pod.AdvertiserExclusionPercent > 100) {
err = append(err, errInvalidAdvertiserExclusionPercent)
}
- if nil != pod.IABCategoryExclusionPercent && (*pod.IABCategoryExclusionPercent < 0 || *pod.IABCategoryExclusionPercent > 100) {
+ if pod.IABCategoryExclusionPercent != nil && (*pod.IABCategoryExclusionPercent < 0 || *pod.IABCategoryExclusionPercent > 100) {
err = append(err, errInvalidIABCategoryExclusionPercent)
}
- if nil != pod.MinAds && nil != pod.MaxAds && *pod.MinAds > *pod.MaxAds {
+ if pod.MinAds != nil && pod.MaxAds != nil && *pod.MinAds > *pod.MaxAds {
err = append(err, errInvalidMinMaxAds)
}
- if nil != pod.MinDuration && nil != pod.MaxDuration && *pod.MinDuration > *pod.MaxDuration {
+ if pod.MinDuration != nil && pod.MaxDuration != nil && *pod.MinDuration > *pod.MaxDuration {
err = append(err, errInvalidMinMaxDuration)
}
@@ -192,6 +196,10 @@ func (ext *ExtRequestAdPod) Validate() (err []error) {
// Validate will validate video extension object
func (ext *ExtVideoAdPod) Validate() (err []error) {
+ if ext == nil {
+ return
+ }
+
if nil != ext.Offset && *ext.Offset < 0 {
err = append(err, errInvalidAdPodOffset)
}
@@ -209,6 +217,10 @@ func (ext *ExtVideoAdPod) Validate() (err []error) {
// SetDefaultValue will set default values if not present
func (pod *VideoAdPod) SetDefaultValue() {
+ if pod == nil {
+ return
+ }
+
//pod.MinAds setting default value
if nil == pod.MinAds {
pod.MinAds = getIntPtr(1)
@@ -272,6 +284,10 @@ func (ext *ExtVideoAdPod) SetDefaultValue() {
// SetDefaultAdDuration will set default pod ad slot durations
func (pod *VideoAdPod) SetDefaultAdDurations(podMinDuration, podMaxDuration int64) {
+ if pod == nil {
+ return
+ }
+
//pod.MinDuration setting default adminduration
if nil == pod.MinDuration {
duration := int(podMinDuration / 2)
@@ -287,6 +303,10 @@ func (pod *VideoAdPod) SetDefaultAdDurations(podMinDuration, podMaxDuration int6
// Merge VideoAdPod Values
func (pod *VideoAdPod) Merge(parent *VideoAdPod) {
+ if pod == nil {
+ return
+ }
+
//pod.MinAds setting default value
if nil == pod.MinAds {
pod.MinAds = parent.MinAds
@@ -297,6 +317,8 @@ func (pod *VideoAdPod) Merge(parent *VideoAdPod) {
pod.MaxAds = parent.MaxAds
}
+ // Add Min and Max duration from request
+
//pod.AdvertiserExclusionPercent setting default value
if nil == pod.AdvertiserExclusionPercent {
pod.AdvertiserExclusionPercent = parent.AdvertiserExclusionPercent
diff --git a/openrtb_ext/openwrap_test.go b/openrtb_ext/openwrap_test.go
index ecd2ebefee9..dceb053b271 100644
--- a/openrtb_ext/openwrap_test.go
+++ b/openrtb_ext/openwrap_test.go
@@ -55,13 +55,6 @@ func TestVideoAdPod_Validate(t *testing.T) {
},
wantErr: []error{errInvalidMinDuration},
},
- {
- name: "ZeroMinDuration",
- fields: fields{
- MinDuration: getIntPtr(0),
- },
- wantErr: []error{errInvalidMinDuration},
- },
{
name: "ErrInvalidMaxDuration",
fields: fields{
@@ -246,7 +239,7 @@ func TestExtRequestAdPod_Validate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ext := &ExtRequestAdPod{
- VideoAdPod: tt.fields.VideoAdPod,
+ VideoAdPod: &tt.fields.VideoAdPod,
CrossPodAdvertiserExclusionPercent: tt.fields.CrossPodAdvertiserExclusionPercent,
CrossPodIABCategoryExclusionPercent: tt.fields.CrossPodIABCategoryExclusionPercent,
IABCategoryExclusionWindow: tt.fields.IABCategoryExclusionWindow,
diff --git a/router/router_test.go b/router/router_test.go
index 866a8440f3f..d023f4f864b 100644
--- a/router/router_test.go
+++ b/router/router_test.go
@@ -61,7 +61,8 @@ func TestNewJsonDirectoryServer(t *testing.T) {
}
for _, adapterFile := range adapterFiles {
- if adapterFile.IsDir() && adapterFile.Name() != "adapterstest" {
+ // OW specific: ignore ortbbidder because we are maintaining single adapterFile for multiple bidders
+ if adapterFile.IsDir() && adapterFile.Name() != "adapterstest" && adapterFile.Name() != "ortbbidder" {
ensureHasKey(t, data, adapterFile.Name())
}
}
diff --git a/static/bidder-info/owortb_testbidder.yaml b/static/bidder-info/owortb_testbidder.yaml
new file mode 100644
index 00000000000..a8175136b10
--- /dev/null
+++ b/static/bidder-info/owortb_testbidder.yaml
@@ -0,0 +1,11 @@
+# sample bidder-info yaml for testbidder (oRTB Integration)
+maintainer:
+ email: "header-bidding@pubmatic.com"
+endpoint: "http://test.endpoint.com"
+capabilities:
+ app:
+ mediaTypes:
+ - video
+ site:
+ mediaTypes:
+ - video
diff --git a/static/bidder-params/owortb_testbidder.json b/static/bidder-params/owortb_testbidder.json
new file mode 100644
index 00000000000..b0ce3c53aed
--- /dev/null
+++ b/static/bidder-params/owortb_testbidder.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "testbidder (oRTB Integration) Adapter Params",
+ "description": "A schema which validates params accepted by the testbidder (oRTB Integration)",
+ "type": "object",
+ "properties": {
+ "adunitID": {
+ "type": "string",
+ "description": "adunitID param",
+ "location": "ext.adunit.id"
+ }
+ },
+ "required": ["adunitID"]
+}
\ No newline at end of file