Skip to content

Support bidder-specific device data #4197

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

Merged
merged 6 commits into from
Jun 4, 2025
Merged
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
67 changes: 45 additions & 22 deletions exchange/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ func (fpb fakePermissionsBuilder) Builder(gdpr.TCF2ConfigReader, gdpr.RequestInf
}

func assertReq(t *testing.T, bidderRequests []BidderRequest,
applyCOPPA bool, consentedVendors map[string]bool) {
applyCOPPA bool, consentedVendors map[string]bool,
) {
// assert individual bidder requests
assert.NotEqual(t, bidderRequests, 0, "cleanOpenRTBRequest should split request into individual bidder requests")

Expand Down Expand Up @@ -922,7 +923,8 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) {
BidRequest: &openrtb2.BidRequest{Imp: nil},
BidderName: "bidderA",
BidderStoredResponses: map[string]json.RawMessage{
"imp-id1": bidRespId1},
"imp-id1": bidRespId1,
},
},
},
},
Expand Down Expand Up @@ -952,7 +954,8 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) {
}},
BidderName: "bidderA",
BidderStoredResponses: map[string]json.RawMessage{
"imp-id1": bidRespId1},
"imp-id1": bidRespId1,
},
},
},
},
Expand Down Expand Up @@ -983,7 +986,8 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) {
BidRequest: &openrtb2.BidRequest{Imp: nil},
BidderName: "bidderB",
BidderStoredResponses: map[string]json.RawMessage{
"imp-id1": bidRespId2},
"imp-id1": bidRespId2,
},
},
},
},
Expand Down Expand Up @@ -1013,13 +1017,15 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) {
}},
BidderName: "bidderA",
BidderStoredResponses: map[string]json.RawMessage{
"imp-id1": bidRespId1},
"imp-id1": bidRespId1,
},
},
"bidderB": {
BidRequest: &openrtb2.BidRequest{Imp: nil},
BidderName: "bidderB",
BidderStoredResponses: map[string]json.RawMessage{
"imp-id1": bidRespId2},
"imp-id1": bidRespId2,
},
},
},
},
Expand Down Expand Up @@ -1053,13 +1059,15 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) {
}},
BidderName: "bidderA",
BidderStoredResponses: map[string]json.RawMessage{
"imp-id1": bidRespId1},
"imp-id1": bidRespId1,
},
},
"bidderB": {
BidRequest: &openrtb2.BidRequest{Imp: nil},
BidderName: "bidderB",
BidderStoredResponses: map[string]json.RawMessage{
"imp-id1": bidRespId2},
"imp-id1": bidRespId2,
},
},
"bidderC": {
BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{
Expand Down Expand Up @@ -1092,7 +1100,8 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) {
}},
BidderName: "bidderA",
BidderStoredResponses: map[string]json.RawMessage{
"imp-id2": bidRespId2},
"imp-id2": bidRespId2,
},
},
},
},
Expand Down Expand Up @@ -2106,7 +2115,6 @@ func TestParseRequestDebugValues(t *testing.T) {
}

func TestSetDebugLogValues(t *testing.T) {

type aTest struct {
desc string
inAccountDebugFlag bool
Expand All @@ -2118,7 +2126,6 @@ func TestSetDebugLogValues(t *testing.T) {
desc string
testCases []aTest
}{

{
"nil debug log",
[]aTest{
Expand Down Expand Up @@ -2573,7 +2580,6 @@ func TestCleanOpenRTBRequestsWithOpenRTBDowngrade(t *testing.T) {

for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {

gdprPermsBuilder := fakePermissionsBuilder{
permissions: &permissionsMock{
allowAllBidders: true,
Expand All @@ -2593,7 +2599,6 @@ func TestCleanOpenRTBRequestsWithOpenRTBDowngrade(t *testing.T) {
bidRequest := bidderRequests[0]
assert.Equal(t, test.expectRegs, bidRequest.BidRequest.Regs)
assert.Equal(t, test.expectUser, bidRequest.BidRequest.User)

})
}
}
Expand Down Expand Up @@ -3016,7 +3021,6 @@ func TestRemoveUnpermissionedEids(t *testing.T) {
eidPermissions []openrtb_ext.ExtRequestPrebidDataEidPermission
expectedUserEids []openrtb2.EID
}{

{
description: "Eids Empty",
userEids: []openrtb2.EID{},
Expand Down Expand Up @@ -3569,7 +3573,6 @@ func TestCleanOpenRTBRequestsBuyerUID(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {

req := &openrtb2.BidRequest{
Site: &openrtb2.Site{
Publisher: &openrtb2.Publisher{
Expand Down Expand Up @@ -3794,6 +3797,28 @@ 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"}},
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you please add the following test case:

        {
			description: "req.Device defined; bidderFPD.Device not defined; expect request.Device remains the same",
			inputFpd: map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData{
				"bidderNormalized": {Device: nil},
			},
			inputBidderName:           "bidderFromRequest",
			inputBidderCoreName:       "bidderNormalized",
			inputBidderIsRequestAlias: false,
			inputRequest:              openrtb2.BidRequest{Device: &openrtb2.Device{Make: "TestDeviceMake"}},
			expectedRequest:           openrtb2.BidRequest{Device: &openrtb2.Device{Make: "TestDeviceMake"}},
		},

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added in e65a0b2

{
description: "req.Device defined; bidderFPD.Device not defined; expect request.Device remains the same",
inputFpd: map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData{
"bidderNormalized": {Device: nil},
},
inputBidderName: "bidderFromRequest",
inputBidderCoreName: "bidderNormalized",
inputBidderIsRequestAlias: false,
inputRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Make: "TestDeviceMake"}},
expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Make: "TestDeviceMake"}},
},
}

for _, testCase := range testCases {
Expand Down Expand Up @@ -4921,7 +4946,6 @@ func TestGetPrebidMediaTypeForBid(t *testing.T) {
} else {
assert.Equal(t, tt.expectedError, err.Error())
}

})
}
}
Expand Down Expand Up @@ -4977,7 +5001,6 @@ func TestGetMediaTypeForBid(t *testing.T) {
} else {
assert.Equal(t, tt.expectedError, err.Error())
}

})
}
}
Expand Down Expand Up @@ -5057,8 +5080,8 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) {
expectedSource: expectedSourceDefault,
},
{
//remove user.eids, user.ext.data.*, user.data.*, user.{id, buyeruid, yob, gender}
//and device-specific IDs
// remove user.eids, user.ext.data.*, user.data.*, user.{id, buyeruid, yob, gender}
// and device-specific IDs
name: "transmit_ufpd_deny",
req: newBidRequest(),
privacyConfig: getTransmitUFPDActivityConfig("appnexus", false),
Expand Down Expand Up @@ -5099,8 +5122,8 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) {
expectedSource: expectedSourceDefault,
},
{
//round user's geographic location by rounding off IP address and lat/lng data.
//this applies to both device.geo and user.geo
// round user's geographic location by rounding off IP address and lat/lng data.
// this applies to both device.geo and user.geo
name: "transmit_precise_geo_deny",
req: newBidRequest(),
privacyConfig: getTransmitPreciseGeoActivityConfig("appnexus", false),
Expand Down Expand Up @@ -5144,7 +5167,7 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) {
expectedSource: expectedSourceDefault,
},
{
//remove source.tid and imp.ext.tid
// remove source.tid and imp.ext.tid
name: "transmit_tid_deny",
req: newBidRequest(),
privacyConfig: getTransmitTIDActivityConfig("appnexus", false),
Expand Down
70 changes: 67 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,75 @@ 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)
}
}

err := validateDevice(newDevice)
if err != nil {
return nil, 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
Contributor 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 validateDevice(device *openrtb2.Device) error {
if device == nil {
return nil
}

// The following fields were previously uints in the OpenRTB library we use, but have
// since been changed to ints. We decided to maintain the non-negative check.
if device.W < 0 {
return errors.New("request.device.w must be a positive number")
}
if device.H < 0 {
return errors.New("request.device.h must be a positive number")
}
if device.PPI < 0 {
return errors.New("request.device.ppi must be a positive number")
}
if device.Geo != nil && device.Geo.Accuracy < 0 {
return errors.New("request.device.geo.accuracy must be a positive number")
}
return 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 +440,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
Loading
Loading