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 3 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
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
Next Next commit
Support bidder-specific device data
This PR introduces support for bidder-specific device data. It merges
bidderconfig.ortb2.device fields into request.device, with
bidderconfig.ortb2.device taking precedence.
Fixes #4147
lucor committed Feb 7, 2025
commit bed560d027241e703dc55ad0d1acf65160be0f82
4 changes: 4 additions & 0 deletions exchange/utils.go
Original file line number Diff line number Diff line change
@@ -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 {
11 changes: 11 additions & 0 deletions exchange/utils_test.go
Original file line number Diff line number Diff line change
@@ -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 {
44 changes: 41 additions & 3 deletions firstpartydata/first_party_data.go
Original file line number Diff line number Diff line change
@@ -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
@@ -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
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add device validation before returning it?
Logic should be the same as in func validateDevice(device *openrtb2.Device).
This function is not available here, so copying it is ok.
Also please add a unit test for this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 7abb2a8


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

@@ -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
86 changes: 86 additions & 0 deletions firstpartydata/first_party_data_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
})
}
@@ -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")
@@ -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
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
@@ -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 {
9 changes: 9 additions & 0 deletions openrtb_ext/request_test.go
Original file line number Diff line number Diff line change
@@ -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{
@@ -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) {
@@ -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{}},