Repository: prebid/prebid-server
Pull request #2417 (2022-10-12 11:23:21)

Pubmatic: Pass imp.ext.data to SSP
Pubmatic adapter: Read imp.ext.data and pass all the key-values to SSP at imp.ext.key_val

bsardo: @pm-isha-bharti just a friendly reminder. @guscarreon left a few comments to address when you get some time.
bsardo: Hi @pm-isha-bharti, can you merge with master when you get a chance? Your code is failing the go 1.19 validation checks due to `go fmt` changes that were introduced with the recent upgrade to go 1.19 on master.
guscarreon: @pm-isha-bharti our pipeline tests seem to still be failing. We recently updated to Go 1.19 and `gofmt` needed to be updated on my local computer as well. I think that might be the issue with the fourth test:
```
Run ./validate.sh --nofmt --cov --race 10
gofmt needs to be run, 1 files have issues.  Below is a list of files to review:
adapters/pubmatic/pubmatic.go
Error: Process completed with exit code 1.
```

This will probably will go away if you update `gofmt` and run it on `adapters/pubmatic/pubmatic.go`

Patch:
diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go
index f00d0d24..5040622d 100644
--- a/adapters/pubmatic/pubmatic.go
+++ b/adapters/pubmatic/pubmatic.go
@@ -41,12 +41,7 @@ type pubmaticBidExtVideo struct {
 
 type ExtImpBidderPubmatic struct {
 	adapters.ExtImpBidder
-	Data *ExtData `json:"data,omitempty"`
-}
-
-type ExtData struct {
-	AdServer *ExtAdServer `json:"adserver"`
-	PBAdSlot string       `json:"pbadslot"`
+	Data json.RawMessage `json:"data,omitempty"`
 }
 
 type ExtAdServer struct {
@@ -71,6 +66,8 @@ const (
 	pmZoneIDKeyNameOld = "pmZoneID"
 	ImpExtAdUnitKey    = "dfp_ad_unit_code"
 	AdServerGAM        = "gam"
+	AdServerKey        = "adserver"
+	PBAdslotKey        = "pbadslot"
 )
 
 func (a *PubmaticAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
@@ -303,12 +300,8 @@ func parseImpressionObject(imp *openrtb2.Imp, extractWrapperExtFromImp, extractP
 		extMap[pmZoneIDKeyName] = pubmaticExt.PmZoneID
 	}
 
-	if bidderExt.Data != nil {
-		if bidderExt.Data.AdServer != nil && bidderExt.Data.AdServer.Name == AdServerGAM && bidderExt.Data.AdServer.AdSlot != "" {
-			extMap[ImpExtAdUnitKey] = bidderExt.Data.AdServer.AdSlot
-		} else if bidderExt.Data.PBAdSlot != "" {
-			extMap[ImpExtAdUnitKey] = bidderExt.Data.PBAdSlot
-		}
+	if len(bidderExt.Data) > 0 {
+		populateFirstPartyDataImpAttributes(bidderExt.Data, extMap)
 	}
 
 	imp.Ext = nil
@@ -497,6 +490,109 @@ func getNativeAdm(adm string) (string, error) {
 	return adm, nil
 }
 
+// getMapFromJSON converts JSON to map
+func getMapFromJSON(source json.RawMessage) map[string]interface{} {
+	if source != nil {
+		dataMap := make(map[string]interface{})
+		err := json.Unmarshal(source, &dataMap)
+		if err == nil {
+			return dataMap
+		}
+	}
+	return nil
+}
+
+// populateFirstPartyDataImpAttributes will parse imp.ext.data and populate imp extMap
+func populateFirstPartyDataImpAttributes(data json.RawMessage, extMap map[string]interface{}) {
+
+	dataMap := getMapFromJSON(data)
+
+	if dataMap == nil {
+		return
+	}
+
+	populateAdUnitKey(data, dataMap, extMap)
+	populateDctrKey(dataMap, extMap)
+}
+
+// populateAdUnitKey parses data object to read and populate DFP adunit key
+func populateAdUnitKey(data json.RawMessage, dataMap, extMap map[string]interface{}) {
+
+	if name, err := jsonparser.GetString(data, "adserver", "name"); err == nil && name == AdServerGAM {
+		if adslot, err := jsonparser.GetString(data, "adserver", "adslot"); err == nil && adslot != "" {
+			extMap[ImpExtAdUnitKey] = adslot
+		}
+	}
+
+	//imp.ext.dfp_ad_unit_code is not set, then check pbadslot in imp.ext.data
+	if extMap[ImpExtAdUnitKey] == nil && dataMap[PBAdslotKey] != nil {
+		extMap[ImpExtAdUnitKey] = dataMap[PBAdslotKey].(string)
+	}
+}
+
+// populateDctrKey reads key-val pairs from imp.ext.data and add it in imp.ext.key_val
+func populateDctrKey(dataMap, extMap map[string]interface{}) {
+	var dctr strings.Builder
+
+	//append dctr key if already present in extMap
+	if extMap[dctrKeyName] != nil {
+		dctr.WriteString(extMap[dctrKeyName].(string))
+	}
+
+	for key, val := range dataMap {
+
+		//ignore 'pbaslot' and 'adserver' key as they are not targeting keys
+		if key == PBAdslotKey || key == AdServerKey {
+			continue
+		}
+
+		//separate key-val pairs in dctr string by pipe(|)
+		if dctr.Len() > 0 {
+			dctr.WriteString("|")
+		}
+
+		//trimming spaces from key
+		key = strings.TrimSpace(key)
+
+		switch typedValue := val.(type) {
+		case string:
+			if _, err := fmt.Fprintf(&dctr, "%s=%s", key, strings.TrimSpace(typedValue)); err != nil {
+				continue
+			}
+
+		case float64, bool:
+			if _, err := fmt.Fprintf(&dctr, "%s=%v", key, typedValue); err != nil {
+				continue
+			}
+
+		case []interface{}:
+			if valStrArr := getStringArray(typedValue); len(valStrArr) > 0 {
+				valStr := strings.Join(valStrArr[:], ",")
+				if _, err := fmt.Fprintf(&dctr, "%s=%s", key, valStr); err != nil {
+					continue
+				}
+			}
+		}
+	}
+
+	if dctrStr := dctr.String(); dctrStr != "" {
+		extMap[dctrKeyName] = strings.TrimSuffix(dctrStr, "|")
+	}
+}
+
+// getStringArray converts interface of type string array to string array
+func getStringArray(array []interface{}) []string {
+	aString := make([]string, len(array))
+	for i, v := range array {
+		if str, ok := v.(string); ok {
+			aString[i] = strings.TrimSpace(str)
+		} else {
+			return nil
+		}
+	}
+	return aString
+}
+
 // getBidType returns the bid type specified in the response bid.ext
 func getBidType(bidExt *pubmaticBidExt) openrtb_ext.BidType {
 	// setting "banner" as the default bid type


Test patch:
diff --git a/adapters/pubmatic/pubmatic_test.go b/adapters/pubmatic/pubmatic_test.go
index 96aef7a9..9e7a97d2 100644
--- a/adapters/pubmatic/pubmatic_test.go
+++ b/adapters/pubmatic/pubmatic_test.go
@@ -3,6 +3,8 @@ package pubmatic
 import (
 	"encoding/json"
 	"net/http"
+	"sort"
+	"strings"
 	"testing"
 
 	"github.com/prebid/openrtb/v17/openrtb2"
@@ -495,3 +497,211 @@ func Test_getAlternateBidderCodesFromRequest(t *testing.T) {
 		})
 	}
 }
+
+func TestPopulateFirstPartyDataImpAttributes(t *testing.T) {
+	type args struct {
+		data      json.RawMessage
+		impExtMap map[string]interface{}
+	}
+	tests := []struct {
+		name           string
+		args           args
+		expectedImpExt map[string]interface{}
+	}{
+		{
+			name: "Only Targeting present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"sport":["rugby","cricket"]}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "sport=rugby,cricket",
+			},
+		},
+		{
+			name: "Targeting and adserver object present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"adserver": {"name": "gam","adslot": "/1111/home"},"pbadslot": "/2222/home","sport":["rugby","cricket"]}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"dfp_ad_unit_code": "/1111/home",
+				"key_val":          "sport=rugby,cricket",
+			},
+		},
+		{
+			name: "Targeting and pbadslot key present in imp.ext.data ",
+			args: args{
+				data:      json.RawMessage(`{"pbadslot": "/2222/home","sport":["rugby","cricket"]}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"dfp_ad_unit_code": "/2222/home",
+				"key_val":          "sport=rugby,cricket",
+			},
+		},
+		{
+			name: "Targeting and Invalid Adserver object in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"adserver": "invalid","sport":["rugby","cricket"]}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "sport=rugby,cricket",
+			},
+		},
+		{
+			name: "key_val already present in imp.ext.data",
+			args: args{
+				data: json.RawMessage(`{"sport":["rugby","cricket"]}`),
+				impExtMap: map[string]interface{}{
+					"key_val": "k1=v1|k2=v2",
+				},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "k1=v1|k2=v2|sport=rugby,cricket",
+			},
+		},
+		{
+			name: "int data present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"age": 25}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "age=25",
+			},
+		},
+		{
+			name: "float data present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"floor": 0.15}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "floor=0.15",
+			},
+		},
+		{
+			name: "bool data present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"k1": true}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "k1=true",
+			},
+		},
+		{
+			name: "imp.ext.data is not present",
+			args: args{
+				data:      nil,
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{},
+		},
+		{
+			name: "string with spaces present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"  category  ": "   cinema  "}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "category=cinema",
+			},
+		},
+		{
+			name: "string array with spaces present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"  country\t": ["  India", "\tChina  "]}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "country=India,China",
+			},
+		},
+		{
+			name: "Invalid data present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"country": [1, "India"],"category":"movies"}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "category=movies",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			populateFirstPartyDataImpAttributes(tt.args.data, tt.args.impExtMap)
+			assert.Equal(t, tt.expectedImpExt, tt.args.impExtMap)
+		})
+	}
+}
+
+func TestPopulateFirstPartyDataImpAttributesForMultipleAttributes(t *testing.T) {
+	impExtMap := map[string]interface{}{
+		"key_val": "k1=v1|k2=v2",
+	}
+	data := json.RawMessage(`{"sport":["rugby","cricket"],"pageType":"article","age":30,"floor":1.25}`)
+	expectedKeyValArr := []string{"age=30", "floor=1.25", "k1=v1", "k2=v2", "pageType=article", "sport=rugby,cricket"}
+
+	populateFirstPartyDataImpAttributes(data, impExtMap)
+
+	//read dctr value and split on "|" for comparison
+	actualKeyValArr := strings.Split(impExtMap[dctrKeyName].(string), "|")
+	sort.Strings(actualKeyValArr)
+	assert.Equal(t, expectedKeyValArr, actualKeyValArr)
+}
+
+func TestGetStringArray(t *testing.T) {
+	tests := []struct {
+		name   string
+		input  []interface{}
+		output []string
+	}{
+		{
+			name:   "Valid String Array",
+			input:  append(make([]interface{}, 0), "hello", "world"),
+			output: []string{"hello", "world"},
+		},
+		{
+			name:   "Invalid String Array",
+			input:  append(make([]interface{}, 0), 1, 2),
+			output: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := getStringArray(tt.input)
+			assert.Equal(t, tt.output, got)
+		})
+	}
+}
+
+func TestGetMapFromJSON(t *testing.T) {
+	tests := []struct {
+		name   string
+		input  json.RawMessage
+		output map[string]interface{}
+	}{
+		{
+			name:  "Valid JSON",
+			input: json.RawMessage(`{"buyid":"testBuyId"}`),
+			output: map[string]interface{}{
+				"buyid": "testBuyId",
+			},
+		},
+		{
+			name:   "Invalid JSON",
+			input:  json.RawMessage(`{"buyid":}`),
+			output: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := getMapFromJSON(tt.input)
+			assert.Equal(t, tt.output, got)
+		})
+	}
+}
diff --git a/adapters/pubmatic/pubmatictest/supplemental/impExtData.json b/adapters/pubmatic/pubmatictest/supplemental/impExtData.json
new file mode 100644
index 00000000..de346fc6
--- /dev/null
+++ b/adapters/pubmatic/pubmatictest/supplemental/impExtData.json
@@ -0,0 +1,156 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "adSlot": "AdTag_Div1@300x250",
+                        "publisherId": "999",
+                        "wrapper": {
+                            "version": 1,
+                            "profile": 5123
+                        },
+                        "dctr": "k1=v1|k2=v2"
+                    },
+                    "data": {
+                        "adserver": {
+                            "name": "gam",
+                            "adslot": "/1111/home"
+                        },
+                        "pbadslot": "/2222/home",
+                        "sport": [
+                            "rugby",
+                            "cricket"
+                        ]
+                    }
+                }
+            }
+        ],
+        "device": {
+            "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+        },
+        "site": {
+            "id": "siteID",
+            "publisher": {
+                "id": "1234"
+            }
+        }
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "AdTag_Div1",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    }
+                                ],
+                                "h": 250,
+                                "w": 300
+                            },
+                            "ext": {
+                                "dfp_ad_unit_code": "/1111/home",
+                                "key_val": "k1=v1|k2=v2|sport=rugby,cricket"
+                            }
+                        }
+                    ],
+                    "device": {
+                        "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+                    },
+                    "site": {
+                        "id": "siteID",
+                        "publisher": {
+                            "id": "999"
+                        }
+                    },
+                    "ext": {
+                        "prebid": {},
+                        "wrapper": {
+                            "profile": 5123,
+                            "version": 1
+                        }
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "seat": "958",
+                            "bid": [
+                                {
+                                    "id": "7706636740145184841",
+                                    "impid": "test-imp-id",
+                                    "price": 0.500000,
+                                    "adid": "29681110",
+                                    "adm": "some-test-ad",
+                                    "adomain": [
+                                        "pubmatic.com"
+                                    ],
+                                    "crid": "29681110",
+                                    "h": 250,
+                                    "w": 300,
+                                    "dealid": "test deal",
+                                    "ext": {
+                                        "dspid": 6,
+                                        "deal_channel": 1
+                                    }
+                                }
+                            ]
+                        }
+                    ],
+                    "bidid": "5778926625248726496",
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "currency": "USD",
+            "bids": [
+                {
+                    "bid": {
+                        "id": "7706636740145184841",
+                        "impid": "test-imp-id",
+                        "price": 0.5,
+                        "adid": "29681110",
+                        "adm": "some-test-ad",
+                        "adomain": [
+                            "pubmatic.com"
+                        ],
+                        "crid": "29681110",
+                        "w": 300,
+                        "h": 250,
+                        "dealid": "test deal",
+                        "ext": {
+                            "dspid": 6,
+                            "deal_channel": 1
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file