Repository: prebid/prebid-server
Pull request #2197 (2022-03-22 05:22:09)

First party data fix for user.data and user.ext.data


Patch:
diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go
index f855f917..79f07200 100644
--- a/adapters/pubmatic/pubmatic.go
+++ b/adapters/pubmatic/pubmatic.go
@@ -271,6 +271,14 @@ func parseImpressionObject(imp *openrtb2.Imp, extractWrapperExtFromImp, extractP
 		imp.Banner = bannerCopy
 	}
 
+	if pubmaticExt.Kadfloor != "" {
+		bidfloor, err := strconv.ParseFloat(strings.TrimSpace(pubmaticExt.Kadfloor), 64)
+		if err == nil {
+			//do not overwrite existing value if kadfloor is invalid
+			imp.BidFloor = bidfloor
+		}
+	}
+
 	extMap := make(map[string]interface{}, 0)
 	if pubmaticExt.Keywords != nil && len(pubmaticExt.Keywords) != 0 {
 		addKeywordsToExt(pubmaticExt.Keywords, extMap)
diff --git a/analytics/event.go b/analytics/event.go
index 56b75769..0b0e65f0 100644
--- a/analytics/event.go
+++ b/analytics/event.go
@@ -5,8 +5,9 @@ type EventType string
 
 // Possible values of events Prebid Server can receive for an ad.
 const (
-	Win EventType = "win"
-	Imp EventType = "imp"
+	Win  EventType = "win"
+	Imp  EventType = "imp"
+	Vast EventType = "vast"
 )
 
 // ResponseFormat enumerates the values of a Prebid Server event.
@@ -19,6 +20,18 @@ const (
 	Image ResponseFormat = "i"
 )
 
+// VastType enumerates the values of vast type events Prebid Server can receive
+type VastType string
+
+// Possible value of VastType event prebid server can receive.
+const (
+	Start         VastType = "start"
+	FirstQuartile VastType = "firstQuartile"
+	MidPoint      VastType = "midPoint"
+	ThirdQuartile VastType = "thirdQuartile"
+	Complete      VastType = "complete"
+)
+
 // Analytics indicates if the notification event should be handled or not
 type Analytics string
 
@@ -36,4 +49,5 @@ type EventRequest struct {
 	Bidder      string         `json:"bidder,omitempty"`
 	Timestamp   int64          `json:"timestamp,omitempty"`
 	Integration string         `json:"integration,omitempty"`
+	VType       VastType       `json:"vtype,omitempty"`
 }
diff --git a/endpoints/events/event.go b/endpoints/events/event.go
index fd47683a..8c9c252a 100644
--- a/endpoints/events/event.go
+++ b/endpoints/events/event.go
@@ -23,6 +23,7 @@ const (
 	// Required
 	TemplateUrl        = "%v/event?t=%v&b=%v&a=%v"
 	TypeParameter      = "t"
+	VTypeParameter     = "vtype"
 	BidIdParameter     = "b"
 	AccountIdParameter = "a"
 
@@ -144,6 +145,16 @@ func ParseEventRequest(r *http.Request) (*analytics.EventRequest, []error) {
 		errs = append(errs, err)
 	}
 
+	if event.Type == analytics.Vast {
+		if err := readVType(event, r); err != nil {
+			errs = append(errs, err)
+		}
+	} else {
+		if t := r.URL.Query().Get(VTypeParameter); t != "" {
+			errs = append(errs, &errortypes.BadInput{Message: "parameter 'vtype' is only required for t=vast"})
+		}
+	}
+
 	// validate bidid
 	if bidid, err := checkRequiredParameter(r, BidIdParameter); err != nil {
 		errs = append(errs, err)
@@ -261,11 +272,40 @@ func readType(er *analytics.EventRequest, httpRequest *http.Request) error {
 	case string(analytics.Win):
 		er.Type = analytics.Win
 		return nil
+	case string(analytics.Vast):
+		er.Type = analytics.Vast
+		return nil
 	default:
 		return &errortypes.BadInput{Message: fmt.Sprintf("unknown type: '%s'", t)}
 	}
 }
 
+// readVType validates analytics.EventRequest vtype
+func readVType(er *analytics.EventRequest, httpRequest *http.Request) error {
+	vtype, err := checkRequiredParameter(httpRequest, VTypeParameter)
+
+	if err != nil {
+		return err
+	}
+
+	switch vtype {
+	case string(analytics.Start):
+		er.VType = analytics.Start
+	case string(analytics.FirstQuartile):
+		er.VType = analytics.FirstQuartile
+	case string(analytics.MidPoint):
+		er.VType = analytics.MidPoint
+	case string(analytics.ThirdQuartile):
+		er.VType = analytics.ThirdQuartile
+	case string(analytics.Complete):
+		er.VType = analytics.Complete
+	default:
+		return &errortypes.BadInput{Message: fmt.Sprintf("unknown vtype: '%s'", vtype)}
+	}
+
+	return nil
+}
+
 // readFormat validates analytics.EventRequest format attribute
 func readFormat(er *analytics.EventRequest, httpRequest *http.Request) error {
 	f := httpRequest.URL.Query().Get(FormatParameter)
diff --git a/firstpartydata/first_party_data.go b/firstpartydata/first_party_data.go
index d1940448..78ab5076 100644
--- a/firstpartydata/first_party_data.go
+++ b/firstpartydata/first_party_data.go
@@ -221,20 +221,26 @@ func unmarshalJSONToInt64(input json.RawMessage) (int64, error) {
 	if err != nil {
 		return -1, err
 	}
-	resNum, err := num.Int64()
-	return resNum, err
+	result, err := num.Int64()
+	return result, err
 }
 
 func unmarshalJSONToString(input json.RawMessage) (string, error) {
-	var inputString string
-	err := json.Unmarshal(input, &inputString)
-	return inputString, err
+	var result string
+	err := json.Unmarshal(input, &result)
+	return result, err
+}
+
+func unmarshalJSONToData(input json.RawMessage) ([]openrtb2.Data, error) {
+	var result []openrtb2.Data
+	err := json.Unmarshal(input, &result)
+	return result, err
 }
 
 func unmarshalJSONToStringArray(input json.RawMessage) ([]string, error) {
-	var inputString []string
-	err := json.Unmarshal(input, &inputString)
-	return inputString, err
+	var result []string
+	err := json.Unmarshal(input, &result)
+	return result, err
 }
 
 //resolveExtension inserts remaining {site/app/user} attributes back to {site/app/user}.ext.data
@@ -294,6 +300,15 @@ func mergeUsers(original *openrtb2.User, fpdConfigUser map[string]json.RawMessag
 		delete(fpdConfigUser, yobKey)
 	}
 
+	if userData, present := fpdConfigUser[dataKey]; present {
+		newUserData, err := unmarshalJSONToData(userData)
+		if err != nil {
+			return newUser, err
+		}
+		newUser.Data = append(newUser.Data, newUserData...)
+		delete(fpdConfigUser, dataKey)
+	}
+
 	if len(fpdConfigUser) > 0 {
 		newUser.Ext, err = resolveExtension(fpdConfigUser, original.Ext)
 	}
diff --git a/openrtb_ext/imp_pubmatic.go b/openrtb_ext/imp_pubmatic.go
index cf5093a2..db96ca51 100644
--- a/openrtb_ext/imp_pubmatic.go
+++ b/openrtb_ext/imp_pubmatic.go
@@ -15,6 +15,7 @@ type ExtImpPubmatic struct {
 	PmZoneID    string                  `json:"pmzoneid"`
 	WrapExt     json.RawMessage         `json:"wrapper,omitempty"`
 	Keywords    []*ExtImpPubmaticKeyVal `json:"keywords,omitempty"`
+	Kadfloor    string                  `json:"kadfloor,omitempty"`
 }
 
 // ExtImpPubmaticKeyVal defines the contract for bidrequest.imp[i].ext.pubmatic.keywords[i]
diff --git a/static/bidder-params/pubmatic.json b/static/bidder-params/pubmatic.json
index 5d41b9fc..a13b5906 100644
--- a/static/bidder-params/pubmatic.json
+++ b/static/bidder-params/pubmatic.json
@@ -16,6 +16,10 @@
 			"type": "string",
 			"description": "Comma separated zone id. Used im deal targeting & site section targeting. e.g drama,sport"
 		},
+		"kadfloor": {
+			"type": "string",
+			"description": "bid floor value set to imp.bidfloor"
+		},
 		"dctr": {
 			"type": "string",
 			"description": "Deals Custom Targeting, pipe separated key-value pairs e.g key1=V1,V2,V3|key2=v1|key3=v3,v5"


Test patch:
diff --git a/adapters/pubmatic/pubmatic_test.go b/adapters/pubmatic/pubmatic_test.go
index ac7dbdb7..da5f632e 100644
--- a/adapters/pubmatic/pubmatic_test.go
+++ b/adapters/pubmatic/pubmatic_test.go
@@ -1,11 +1,14 @@
 package pubmatic
 
 import (
+	"encoding/json"
 	"testing"
 
+	"github.com/mxmCherry/openrtb/v15/openrtb2"
 	"github.com/prebid/prebid-server/adapters/adapterstest"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestJsonSamples(t *testing.T) {
@@ -67,3 +70,83 @@ func TestGetBidTypeForUnsupportedCode(t *testing.T) {
 		t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeBanner, actualBidTypeValue)
 	}
 }
+
+func TestParseImpressionObject(t *testing.T) {
+	type args struct {
+		imp                      *openrtb2.Imp
+		extractWrapperExtFromImp bool
+		extractPubIDFromImp      bool
+	}
+	tests := []struct {
+		name                string
+		args                args
+		expectedWrapperExt  *pubmaticWrapperExt
+		expectedPublisherId string
+		wantErr             bool
+		expectedBidfloor    float64
+	}{
+		{
+			name: "imp.bidfloor empty and kadfloor set",
+			args: args{
+				imp: &openrtb2.Imp{
+					Video: &openrtb2.Video{},
+					Ext:   json.RawMessage(`{"bidder":{"kadfloor":"0.12"}}`),
+				},
+			},
+			expectedBidfloor: 0.12,
+		},
+		{
+			name: "imp.bidfloor set and kadfloor empty",
+			args: args{
+				imp: &openrtb2.Imp{
+					BidFloor: 0.12,
+					Video:    &openrtb2.Video{},
+					Ext:      json.RawMessage(`{"bidder":{}}`),
+				},
+			},
+			expectedBidfloor: 0.12,
+		},
+		{
+			name: "imp.bidfloor set and kadfloor invalid",
+			args: args{
+				imp: &openrtb2.Imp{
+					BidFloor: 0.12,
+					Video:    &openrtb2.Video{},
+					Ext:      json.RawMessage(`{"bidder":{"kadfloor":"aaa"}}`),
+				},
+			},
+			expectedBidfloor: 0.12,
+		},
+		{
+			name: "imp.bidfloor set and kadfloor set, preference to kadfloor",
+			args: args{
+				imp: &openrtb2.Imp{
+					BidFloor: 0.12,
+					Video:    &openrtb2.Video{},
+					Ext:      json.RawMessage(`{"bidder":{"kadfloor":"0.11"}}`),
+				},
+			},
+			expectedBidfloor: 0.11,
+		},
+		{
+			name: "kadfloor string set with whitespace",
+			args: args{
+				imp: &openrtb2.Imp{
+					BidFloor: 0.12,
+					Video:    &openrtb2.Video{},
+					Ext:      json.RawMessage(`{"bidder":{"kadfloor":" \t  0.13  "}}`),
+				},
+			},
+			expectedBidfloor: 0.13,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			receivedWrapperExt, receivedPublisherId, err := parseImpressionObject(tt.args.imp, tt.args.extractWrapperExtFromImp, tt.args.extractPubIDFromImp)
+			assert.Equal(t, tt.wantErr, err != nil)
+			assert.Equal(t, tt.expectedWrapperExt, receivedWrapperExt)
+			assert.Equal(t, tt.expectedPublisherId, receivedPublisherId)
+			assert.Equal(t, tt.expectedBidfloor, tt.args.imp.BidFloor)
+		})
+	}
+}
diff --git a/adapters/pubmatic/pubmatictest/exemplary/banner.json b/adapters/pubmatic/pubmatictest/exemplary/banner.json
index f525673c..313099f2 100644
--- a/adapters/pubmatic/pubmatictest/exemplary/banner.json
+++ b/adapters/pubmatic/pubmatictest/exemplary/banner.json
@@ -22,6 +22,7 @@
                          "value": ["sports", "movies"]
                       }
                     ],
+                    "kadfloor": "0.12",
                     "wrapper": {
                         "version": 1,
                         "profile": 5123
@@ -56,10 +57,11 @@
                       "w": 300,
                       "h": 250
                     }
-                ],
-                "h": 250,
-                "w": 300
-            },
+                  ],
+                  "h": 250,
+                  "w": 300
+                },
+                "bidfloor": 0.12,
                 "ext": {
                     "pmZoneId": "Zone1,Zone2",
                     "preference": "sports,movies"
diff --git a/adapters/pubmatic/pubmatictest/exemplary/video.json b/adapters/pubmatic/pubmatictest/exemplary/video.json
index 1715e477..693ee3eb 100644
--- a/adapters/pubmatic/pubmatictest/exemplary/video.json
+++ b/adapters/pubmatic/pubmatictest/exemplary/video.json
@@ -28,6 +28,7 @@
                         "value": ["Zone1", "Zone2"]
                       }
                     ],
+                    "kadfloor": "0.12",
                     "wrapper": {
                         "version": 1,
                         "profile": 5123
@@ -72,6 +73,7 @@
                     "minbitrate": 10,
                     "maxbitrate": 10
                 },
+                "bidfloor": 0.12,
                 "ext": {
                     "pmZoneId": "Zone1,Zone2"
                 }
diff --git a/adapters/pubmatic/pubmatictest/supplemental/app.json b/adapters/pubmatic/pubmatictest/supplemental/app.json
index e9f1048e..c6ed97a5 100644
--- a/adapters/pubmatic/pubmatictest/supplemental/app.json
+++ b/adapters/pubmatic/pubmatictest/supplemental/app.json
@@ -22,6 +22,7 @@
                             "value": ["sports", "movies"]
                         }
                     ],
+                    "kadfloor": "0.12",
                     "wrapper": {
                         "version": 1,
                         "profile": 5123
@@ -56,10 +57,11 @@
                       "w": 300,
                       "h": 250
                     }
-                ],
-                "h": 250,
-                "w": 300
-            },
+                  ],
+                  "h": 250,
+                  "w": 300
+                },
+                "bidfloor": 0.12,
                 "ext": {
                     "pmZoneId": "Zone1,Zone2",
                     "preference": "sports,movies"
diff --git a/endpoints/events/event_test.go b/endpoints/events/event_test.go
index 09ad6d57..f700c31e 100644
--- a/endpoints/events/event_test.go
+++ b/endpoints/events/event_test.go
@@ -14,6 +14,7 @@ import (
 
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/stretchr/testify/assert"
 )
@@ -645,6 +646,16 @@ func TestShouldParseEventCorrectly(t *testing.T) {
 				Analytics: analytics.Enabled,
 			},
 		},
+		"three - vtype = start": {
+			req: httptest.NewRequest("GET", "/event?t=vast&vtype=start&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			expected: &analytics.EventRequest{
+				Type:      analytics.Vast,
+				VType:     analytics.Start,
+				BidID:     "bidId",
+				Timestamp: 0,
+				Analytics: analytics.Enabled,
+			},
+		},
 	}
 
 	for name, test := range tests {
@@ -750,3 +761,147 @@ func TestReadIntegrationType(t *testing.T) {
 		}
 	}
 }
+
+func TestShouldReturnBadRequestWhenVTypeIsInvalid(t *testing.T) {
+
+	reqData := ""
+
+	tests := []struct {
+		description        string
+		req                *http.Request
+		expectedStatusCode int
+		expectedStatus     string
+	}{
+		{
+			description:        "vtype parameter is missing",
+			req:                httptest.NewRequest("GET", "/event?t=vast&b=bidID", strings.NewReader(reqData)),
+			expectedStatusCode: 400,
+			expectedStatus:     "invalid request: parameter 'vtype' is required\n",
+		},
+		{
+			description:        "invalid vtype parameter",
+			req:                httptest.NewRequest("GET", "/event?t=vast&vtype=abc&b=bidID", strings.NewReader(reqData)),
+			expectedStatusCode: 400,
+			expectedStatus:     "invalid request: unknown vtype: 'abc'\n",
+		},
+		{
+			description:        "vtype is passed when event != vast",
+			req:                httptest.NewRequest("GET", "/event?t=win&vtype=startc&b=bidID", strings.NewReader(reqData)),
+			expectedStatusCode: 400,
+			expectedStatus:     "invalid request: parameter 'vtype' is only required for t=vast\n",
+		},
+	}
+
+	for _, test := range tests {
+		mockAccountsFetcher := &mockAccountsFetcher{}
+
+		mockAnalyticsModule := &eventsMockAnalyticsModule{
+			Fail: false,
+		}
+
+		cfg := &config.Configuration{
+			AccountDefaults: config.Account{},
+		}
+
+		recorder := httptest.NewRecorder()
+
+		e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+		e(recorder, test.req, nil)
+
+		d, err := ioutil.ReadAll(recorder.Result().Body)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assert.Equal(t, test.expectedStatusCode, recorder.Result().StatusCode, test.description)
+		assert.Equal(t, test.expectedStatus, string(d), test.description)
+
+	}
+}
+
+func TestReadVType(t *testing.T) {
+	type args struct {
+		er  *analytics.EventRequest
+		req *http.Request
+	}
+	tests := []struct {
+		name          string
+		args          args
+		expectedError error
+		expectedVType analytics.VastType
+	}{
+		{
+			name: "vtype = start",
+			args: args{
+				er: &analytics.EventRequest{
+					Type: analytics.Vast,
+				},
+				req: httptest.NewRequest("GET", "/event?t=vast&vtype=start&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			},
+			expectedError: nil,
+			expectedVType: analytics.Start,
+		},
+		{
+			name: "vtype = firstQuartile",
+			args: args{
+				er: &analytics.EventRequest{
+					Type: analytics.Vast,
+				},
+				req: httptest.NewRequest("GET", "/event?t=vast&vtype=firstQuartile&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			},
+			expectedError: nil,
+			expectedVType: analytics.FirstQuartile,
+		},
+		{
+			name: "vtype = midPoint",
+			args: args{
+				er: &analytics.EventRequest{
+					Type: analytics.Vast,
+				},
+				req: httptest.NewRequest("GET", "/event?t=vast&vtype=midPoint&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			},
+			expectedError: nil,
+			expectedVType: analytics.MidPoint,
+		},
+		{
+			name: "vtype = thirdQuartile",
+			args: args{
+				er: &analytics.EventRequest{
+					Type: analytics.Vast,
+				},
+				req: httptest.NewRequest("GET", "/event?t=vast&vtype=thirdQuartile&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			},
+			expectedError: nil,
+			expectedVType: analytics.ThirdQuartile,
+		},
+		{
+			name: "vtype = complete",
+			args: args{
+				er: &analytics.EventRequest{
+					Type: analytics.Vast,
+				},
+				req: httptest.NewRequest("GET", "/event?t=vast&vtype=complete&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			},
+			expectedError: nil,
+			expectedVType: analytics.Complete,
+		},
+		{
+			name: "unknown vtype",
+			args: args{
+				er: &analytics.EventRequest{
+					Type: analytics.Vast,
+				},
+				req: httptest.NewRequest("GET", "/event?t=vast&vtype=test&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			},
+			expectedError: &errortypes.BadInput{Message: "unknown vtype: 'test'"},
+			expectedVType: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := readVType(tt.args.er, tt.args.req)
+			assert.Equal(t, tt.expectedError, err, tt.name)
+			assert.Equal(t, tt.expectedVType, tt.args.er.VType, tt.name)
+		})
+	}
+}
diff --git a/firstpartydata/first_party_data_test.go b/firstpartydata/first_party_data_test.go
index 1e09bab1..1e0dc015 100644
--- a/firstpartydata/first_party_data_test.go
+++ b/firstpartydata/first_party_data_test.go
@@ -762,7 +762,7 @@ func TestResolveUser(t *testing.T) {
 	fpdConfigUser[yobKey] = []byte(`1980`)
 	fpdConfigUser[genderKey] = []byte(`"M"`)
 	fpdConfigUser[keywordsKey] = []byte(`"fpdConfigUserKeywords"`)
-	fpdConfigUser["data"] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
+	fpdConfigUser[dataKey] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
 	fpdConfigUser["ext"] = []byte(`{"data":{"fpdConfigUserExt": 123}}`)
 
 	bidRequestUser := &openrtb2.User{
@@ -789,6 +789,8 @@ func TestResolveUser(t *testing.T) {
 		Data: []openrtb2.Data{
 			{ID: "openRtbGlobalFPDUserDataId1", Name: "openRtbGlobalFPDUserDataName1"},
 			{ID: "openRtbGlobalFPDUserDataId2", Name: "openRtbGlobalFPDUserDataName2"},
+			{ID: "UserDataId1", Name: "UserDataName1"},
+			{ID: "UserDataId2", Name: "UserDataName2"},
 		},
 	}
 
@@ -801,10 +803,6 @@ func TestResolveUser(t *testing.T) {
 			description:       "bid request user.ext is nil",
 			bidRequestUserExt: nil,
 			expectedUserExt: `{"data":{
-									"data":[
-										{"id":"UserDataId1","name":"UserDataName1"},
-										{"id":"UserDataId2","name":"UserDataName2"}
-									],
 									"fpdConfigUserExt":123,
 									"globalFPDUserData":"globalFPDUserDataValue",
 									"id":"fpdConfigUserId"
@@ -815,10 +813,6 @@ func TestResolveUser(t *testing.T) {
 			description:       "bid request user.ext is not nil",
 			bidRequestUserExt: []byte(`{"bidRequestUserExt": 1234}`),
 			expectedUserExt: `{"data":{
-									"data":[
-										{"id":"UserDataId1","name":"UserDataName1"},
-										{"id":"UserDataId2","name":"UserDataName2"}
-									],
 									"fpdConfigUserExt":123,
 									"globalFPDUserData":"globalFPDUserDataValue",
 									"id":"fpdConfigUserId"
@@ -836,7 +830,7 @@ func TestResolveUser(t *testing.T) {
 		fpdConfigUser[yobKey] = []byte(`1980`)
 		fpdConfigUser[genderKey] = []byte(`"M"`)
 		fpdConfigUser[keywordsKey] = []byte(`"fpdConfigUserKeywords"`)
-		fpdConfigUser["data"] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
+		fpdConfigUser[dataKey] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
 		fpdConfigUser["ext"] = []byte(`{"data":{"fpdConfigUserExt": 123}}`)
 		fpdConfig := &openrtb_ext.ORTB2{User: fpdConfigUser}
 
@@ -885,7 +879,7 @@ func TestMergeUsers(t *testing.T) {
 	fpdConfigUser[yobKey] = []byte(`1980`)
 	fpdConfigUser[genderKey] = []byte(`"M"`)
 	fpdConfigUser[keywordsKey] = []byte(`"fpdConfigUserKeywords"`)
-	fpdConfigUser["data"] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
+	fpdConfigUser[dataKey] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
 	fpdConfigUser["ext"] = []byte(`{"data":{"fpdConfigUserExt": 123}}`)
 
 	resultUser, err := mergeUsers(originalUser, fpdConfigUser)
@@ -893,9 +887,6 @@ func TestMergeUsers(t *testing.T) {
 
 	expectedUserExt := `{"bidRequestUserExt":1234,
 						 "data":{
-							"data":[
-								{"id":"UserDataId1","name":"UserDataName1"},
-								{"id":"UserDataId2","name":"UserDataName2"}],
 							"fpdConfigUserExt":123,
 							"id":"fpdConfigUserId"}
 						 }`
@@ -910,6 +901,8 @@ func TestMergeUsers(t *testing.T) {
 		Data: []openrtb2.Data{
 			{ID: "openRtbGlobalFPDUserDataId1", Name: "openRtbGlobalFPDUserDataName1"},
 			{ID: "openRtbGlobalFPDUserDataId2", Name: "openRtbGlobalFPDUserDataName2"},
+			{ID: "UserDataId1", Name: "UserDataName1"},
+			{ID: "UserDataId2", Name: "UserDataName2"},
 		},
 	}
 	assert.Equal(t, expectedUser, resultUser, "Result user is incorrect")
@@ -959,7 +952,7 @@ func TestResolveSite(t *testing.T) {
 	fpdConfigSite[keywordsKey] = []byte(`"fpdConfigSiteKeywords"`)
 	fpdConfigSite[nameKey] = []byte(`"fpdConfigSiteName"`)
 	fpdConfigSite[pageKey] = []byte(`"fpdConfigSitePage"`)
-	fpdConfigSite["data"] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
+	fpdConfigSite[dataKey] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
 	fpdConfigSite["ext"] = []byte(`{"data":{"fpdConfigSiteExt": 123}}`)
 
 	bidRequestSite := &openrtb2.Site{
@@ -1070,7 +1063,7 @@ func TestResolveSite(t *testing.T) {
 		fpdConfigSite := make(map[string]json.RawMessage, 0)
 		fpdConfigSite["id"] = []byte(`"fpdConfigSiteId"`)
 		fpdConfigSite[keywordsKey] = []byte(`"fpdConfigSiteKeywords"`)
-		fpdConfigSite["data"] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
+		fpdConfigSite[dataKey] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
 		fpdConfigSite["ext"] = []byte(`{"data":{"fpdConfigSiteExt": 123}}`)
 		fpdConfig := &openrtb_ext.ORTB2{Site: fpdConfigSite}
 
@@ -1134,7 +1127,7 @@ func TestMergeSites(t *testing.T) {
 	fpdConfigSite[pageCatKey] = []byte(`["cars3", "auto3"]`)
 	fpdConfigSite[searchKey] = []byte(`"fpdConfigSiteSearch"`)
 	fpdConfigSite[refKey] = []byte(`"fpdConfigSiteRef"`)
-	fpdConfigSite["data"] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
+	fpdConfigSite[dataKey] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
 	fpdConfigSite["ext"] = []byte(`{"data":{"fpdConfigSiteExt": 123}}`)
 
 	resultSite, err := mergeSites(originalSite, fpdConfigSite, "appnexus")
@@ -1181,7 +1174,7 @@ func TestResolveApp(t *testing.T) {
 	fpdConfigApp[keywordsKey] = []byte(`"fpdConfigAppKeywords"`)
 	fpdConfigApp[nameKey] = []byte(`"fpdConfigAppName"`)
 	fpdConfigApp[bundleKey] = []byte(`"fpdConfigAppBundle"`)
-	fpdConfigApp["data"] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
+	fpdConfigApp[dataKey] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
 	fpdConfigApp["ext"] = []byte(`{"data":{"fpdConfigAppExt": 123}}`)
 
 	bidRequestApp := &openrtb2.App{
@@ -1292,7 +1285,7 @@ func TestResolveApp(t *testing.T) {
 		fpdConfigApp := make(map[string]json.RawMessage, 0)
 		fpdConfigApp["id"] = []byte(`"fpdConfigAppId"`)
 		fpdConfigApp[keywordsKey] = []byte(`"fpdConfigAppKeywords"`)
-		fpdConfigApp["data"] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
+		fpdConfigApp[dataKey] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
 		fpdConfigApp["ext"] = []byte(`{"data":{"fpdConfigAppExt": 123}}`)
 		fpdConfig := &openrtb_ext.ORTB2{App: fpdConfigApp}
 
@@ -1356,7 +1349,7 @@ func TestMergeApps(t *testing.T) {
 	fpdConfigApp[catKey] = []byte(`["cars1", "auto1"]`)
 	fpdConfigApp[sectionCatKey] = []byte(`["cars2", "auto2"]`)
 	fpdConfigApp[pageCatKey] = []byte(`["cars3", "auto3"]`)
-	fpdConfigApp["data"] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
+	fpdConfigApp[dataKey] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
 	fpdConfigApp["ext"] = []byte(`{"data":{"fpdConfigAppExt": 123}}`)
 
 	resultApp, err := mergeApps(originalApp, fpdConfigApp)
diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json
index c04838c7..ed81b415 100644
--- a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json
+++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json
@@ -200,19 +200,19 @@
         "gender": "F",
         "yob": 2000,
         "keywords": "apnFpdUserKeyword",
+        "data": [
+          {
+            "id": "apnFpdUserContentDataId1",
+            "name": "apnFpdUserContentDataName1"
+          },
+          {
+            "id": "apnFpdUserContentDataId2",
+            "name": "apnFpdUserContentDataName2"
+          }
+        ],
         "ext": {
           "data": {
-            "id": "apnUserId",
-            "data": [
-              {
-                "id": "apnFpdUserContentDataId1",
-                "name": "apnFpdUserContentDataName1"
-              },
-              {
-                "id": "apnFpdUserContentDataId2",
-                "name": "apnFpdUserContentDataName2"
-              }
-            ]
+            "id": "apnUserId"
           }
         }
       }