Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support bidder-specific device data #4197

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions exchange/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,10 @@ func applyFPD(fpd map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyD
reqWrapper.App = fpdToApply.App
}

if fpdToApply.Device != nil {
reqWrapper.Device = fpdToApply.Device
}

if fpdToApply.User != nil {
if reqWrapper.User != nil {
if len(reqWrapper.User.BuyerUID) > 0 {
Expand Down
11 changes: 11 additions & 0 deletions exchange/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3794,6 +3794,17 @@ func TestApplyFPD(t *testing.T) {
expectedRequest: openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId", EIDs: []openrtb2.EID{{Source: "source3"}, {Source: "source4"}}}},
fpdUserEIDsExisted: false,
},
{
description: "req.Device defined; bidderFPD.Device defined; expect request.Device to be overriden by bidderFPD.Device",
inputFpd: map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData{
"bidderNormalized": {Device: &openrtb2.Device{Make: "DeviceMake"}},
},
inputBidderName: "bidderFromRequest",
inputBidderCoreName: "bidderNormalized",
inputBidderIsRequestAlias: false,
inputRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Make: "TestDeviceMake"}},
expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Make: "DeviceMake"}},
},
}

for _, testCase := range testCases {
Expand Down
44 changes: 41 additions & 3 deletions firstpartydata/first_party_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ const (
)

type ResolvedFirstPartyData struct {
Site *openrtb2.Site
App *openrtb2.App
User *openrtb2.User
Site *openrtb2.Site
App *openrtb2.App
User *openrtb2.User
Device *openrtb2.Device
}

// ExtractGlobalFPD extracts request level FPD from the request and removes req.{site,app,user}.ext.data if exists
Expand Down Expand Up @@ -159,13 +160,49 @@ func ResolveFPD(bidRequest *openrtb2.BidRequest, fpdBidderConfigData map[openrtb
}
resolvedFpdConfig.Site = newSite

newDevice, err := resolveDevice(fpdConfig, bidRequest.Device)
if err != nil {
errL = append(errL, err)
}
resolvedFpdConfig.Device = newDevice

if len(errL) == 0 {
resolvedFpd[openrtb_ext.BidderName(bidderName)] = resolvedFpdConfig
}
}
return resolvedFpd, errL
}

// resolveDevice merges the device information from the FPD (First Party Data) configuration
// with the device information provided in the bid request. It returns a new Device object
// that contains the merged data.
func resolveDevice(fpdConfig *openrtb_ext.ORTB2, bidRequestDevice *openrtb2.Device) (*openrtb2.Device, error) {
var fpdConfigDevice json.RawMessage

if fpdConfig != nil && fpdConfig.Device != nil {
fpdConfigDevice = fpdConfig.Device
}

if bidRequestDevice == nil && fpdConfigDevice == nil {
return nil, nil
}

var newDevice *openrtb2.Device
if bidRequestDevice != nil {
newDevice = ptrutil.Clone(bidRequestDevice)
} else {
newDevice = &openrtb2.Device{}
}

if fpdConfigDevice != nil {
if err := jsonutil.MergeClone(newDevice, fpdConfigDevice); err != nil {
return nil, formatMergeCloneError(err)
}
}

return newDevice, nil
}

func resolveUser(fpdConfig *openrtb_ext.ORTB2, bidRequestUser *openrtb2.User, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.User, error) {
var fpdConfigUser json.RawMessage

Expand Down Expand Up @@ -377,6 +414,7 @@ func ExtractBidderConfigFPD(reqExt *openrtb_ext.RequestExt) (map[openrtb_ext.Bid
fpdBidderData.Site = bidderConfig.Config.ORTB2.Site
fpdBidderData.App = bidderConfig.Config.ORTB2.App
fpdBidderData.User = bidderConfig.Config.ORTB2.User
fpdBidderData.Device = bidderConfig.Config.ORTB2.Device
}

fpd[bidderName] = fpdBidderData
Expand Down
86 changes: 86 additions & 0 deletions firstpartydata/first_party_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,12 @@ func TestExtractBidderConfigFPD(t *testing.T) {
} else {
assert.Nil(t, results[bidderName].User, "user expected to be nil")
}

if expectedFPD.Device != nil {
assert.JSONEq(t, string(expectedFPD.Device), string(results[bidderName].Device), "device is incorrect")
} else {
assert.Nil(t, results[bidderName].Device, "device expected to be nil")
}
}
})
}
Expand Down Expand Up @@ -625,6 +631,14 @@ func TestResolveFPD(t *testing.T) {
assert.JSONEq(t, string(expectedUserExt), string(resUserExt), "user.ext is incorrect")
assert.Equal(t, outputReq.User, bidderFPD.User, "User is incorrect")
}
if outputReq.Device != nil && len(outputReq.Device.Ext) > 0 {
resDeviceExt := bidderFPD.Device.Ext
expectedDeviceExt := outputReq.Device.Ext
bidderFPD.Device.Ext = nil
outputReq.Device.Ext = nil
assert.JSONEq(t, string(expectedDeviceExt), string(resDeviceExt), "device.ext is incorrect")
assert.Equal(t, outputReq.Device, bidderFPD.Device, "Device is incorrect")
}
}
} else {
assert.ElementsMatch(t, errL, testFile.ValidationErrors, "Incorrect first party data warning message")
Expand Down Expand Up @@ -1212,6 +1226,78 @@ func TestResolveApp(t *testing.T) {
}
}

func TestResolveDevice(t *testing.T) {
testCases := []struct {
description string
fpdConfig *openrtb_ext.ORTB2
bidRequestDevice *openrtb2.Device
expectedDevice *openrtb2.Device
expectError string
}{
{
description: "FPD config and bid request device are not specified",
expectedDevice: nil,
},
{
description: "FPD config device only is specified",
fpdConfig: &openrtb_ext.ORTB2{Device: json.RawMessage(`{"ua":"test-user-agent"}`)},
expectedDevice: &openrtb2.Device{
UA: "test-user-agent",
},
},
{
description: "FPD config and bid request device are specified",
fpdConfig: &openrtb_ext.ORTB2{Device: json.RawMessage(`{"ua":"test-user-agent-1"}`)},
bidRequestDevice: &openrtb2.Device{UA: "test-user-agent-2"},
expectedDevice: &openrtb2.Device{UA: "test-user-agent-1"},
},
{
description: "Bid request device only is specified",
bidRequestDevice: &openrtb2.Device{UA: "test-user-agent"},
expectedDevice: &openrtb2.Device{UA: "test-user-agent"},
},
{
description: "FPD config device with ext, bid request device with ext",
fpdConfig: &openrtb_ext.ORTB2{Device: json.RawMessage(`{"ua":"test-user-agent-1","ext":{"test":1}}`)},
bidRequestDevice: &openrtb2.Device{
UA: "test-user-agent-2",
Ext: json.RawMessage(`{"test":2,"key":"value"}`),
},
expectedDevice: &openrtb2.Device{
UA: "test-user-agent-1",
Ext: json.RawMessage(`{"key":"value","test":1}`),
},
},
{
description: "Bid request device with ext only is specified",
bidRequestDevice: &openrtb2.Device{UA: "test-user-agent", Ext: json.RawMessage(`{"customData":true}`)},
expectedDevice: &openrtb2.Device{UA: "test-user-agent", Ext: json.RawMessage(`{"customData":true}`)},
},
{
description: "FPD config device with malformed ext",
fpdConfig: &openrtb_ext.ORTB2{Device: json.RawMessage(`{"ua":"test-user-agent-1","ext":{malformed}}`)},
bidRequestDevice: &openrtb2.Device{
UA: "test-user-agent-2",
Ext: json.RawMessage(`{"test":2,"key":"value"}`),
},
expectError: "invalid first party data ext",
},
}

for _, test := range testCases {
t.Run(test.description, func(t *testing.T) {
resultDevice, err := resolveDevice(test.fpdConfig, test.bidRequestDevice)

if len(test.expectError) > 0 {
assert.EqualError(t, err, test.expectError)
} else {
assert.NoError(t, err)
assert.Equal(t, test.expectedDevice, resultDevice, "Result device is incorrect")
}
})
}
}

func TestBuildExtData(t *testing.T) {
testCases := []struct {
description string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"description": "Extracts bidder configs for a bidder, handling device data",
"inputRequestData": {
"data": {},
"bidderconfig": [
{
"bidders": [
"appnexus"
],
"config": {
"ortb2": {
"device": {
"ext": {
"wurfl": {
"is_mobile": true,
"complete_device_name": "Google Nexus 5",
"form_factor": "Feature Phone",
"model_name": "Nexus 5",
"brand_name": "Google"
}
},
"make": "Google",
"model": "Nexus 5"
}
}
}
}
]
},
"outputRequestData": {},
"bidderConfigFPD": {
"appnexus": {
"device": {
"ext": {
"wurfl": {
"is_mobile": true,
"complete_device_name": "Google Nexus 5",
"form_factor": "Feature Phone",
"model_name": "Nexus 5",
"brand_name": "Google"
}
},
"make": "Google",
"model": "Nexus 5"
}
}
}
}
46 changes: 46 additions & 0 deletions firstpartydata/tests/resolvefpd/bidder-fpd-only-device.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"description": "Bidder FPD defined only for device",
"inputRequestData": {
"device": {
"make": "reqMake",
"model": "reqModel"
}
},
"biddersWithGlobalFPD": [
"appnexus"
],
"bidderConfigFPD": {
"appnexus": {
"device": {
"ext": {
"wurfl": {
"is_mobile": true,
"complete_device_name": "Google Nexus 5",
"form_factor": "Feature Phone",
"model_name": "Nexus 5",
"brand_name": "Google"
}
},
"make": "Google",
"model": "Nexus 5"
}
}
},
"outputRequestData": {
"appnexus": {
"device": {
"ext": {
"wurfl": {
"is_mobile": true,
"complete_device_name": "Google Nexus 5",
"form_factor": "Feature Phone",
"model_name": "Nexus 5",
"brand_name": "Google"
}
},
"make": "Google",
"model": "Nexus 5"
}
}
}
}
9 changes: 5 additions & 4 deletions openrtb_ext/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,11 @@ type Config struct {
ORTB2 *ORTB2 `json:"ortb2,omitempty"`
}

type ORTB2 struct { //First party data
Site json.RawMessage `json:"site,omitempty"`
App json.RawMessage `json:"app,omitempty"`
User json.RawMessage `json:"user,omitempty"`
type ORTB2 struct { // First party data
Site json.RawMessage `json:"site,omitempty"`
App json.RawMessage `json:"app,omitempty"`
User json.RawMessage `json:"user,omitempty"`
Device json.RawMessage `json:"device,omitempty"`
}

type ExtRequestCurrency struct {
Expand Down
9 changes: 9 additions & 0 deletions openrtb_ext/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ func TestCloneExtRequestPrebid(t *testing.T) {
Bidders: []string{"foo"},
Config: &Config{&ORTB2{User: json.RawMessage(`{"value":"config3"}`)}},
},
{
Bidders: []string{"Bidder9"},
Config: &Config{&ORTB2{Device: json.RawMessage(`{"value":"config4"}`)}},
},
},
},
prebidCopy: &ExtRequestPrebid{
Expand All @@ -295,6 +299,10 @@ func TestCloneExtRequestPrebid(t *testing.T) {
Bidders: []string{"foo"},
Config: &Config{&ORTB2{User: json.RawMessage(`{"value":"config3"}`)}},
},
{
Bidders: []string{"Bidder9"},
Config: &Config{&ORTB2{Device: json.RawMessage(`{"value":"config4"}`)}},
},
},
},
mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
Expand All @@ -304,6 +312,7 @@ func TestCloneExtRequestPrebid(t *testing.T) {
Config: &Config{nil},
}
prebid.BidderConfigs[2].Config.ORTB2.User = json.RawMessage(`{"id": 345}`)
prebid.BidderConfigs[3].Config.ORTB2.Device = json.RawMessage(`{"id": 999}`)
prebid.BidderConfigs = append(prebid.BidderConfigs, BidderConfig{
Bidders: []string{"bidder2"},
Config: &Config{&ORTB2{}},
Expand Down