Skip to content

Commit

Permalink
EEA countries account-level override (#4118)
Browse files Browse the repository at this point in the history
  • Loading branch information
przemkaczmarek authored Feb 14, 2025
1 parent fe354b5 commit b335bc5
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 10 deletions.
1 change: 1 addition & 0 deletions config/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ type AccountGDPR struct {
PurposeConfigs map[consentconstants.Purpose]*AccountGDPRPurpose
PurposeOneTreatment AccountGDPRPurposeOneTreatment `mapstructure:"purpose_one_treatment" json:"purpose_one_treatment"`
SpecialFeature1 AccountGDPRSpecialFeature `mapstructure:"special_feature1" json:"special_feature1"`
EEACountries []string `mapstructure:"eea_countries" json:"eea_countries"`
}

// EnabledForChannelType indicates whether GDPR is turned on at the account level for the specified channel type
Expand Down
28 changes: 22 additions & 6 deletions exchange/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,11 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog

recordImpMetrics(r.BidRequestWrapper, e.me)

// Retrieve EEA countries configuration from either host or account settings
eeaCountries := selectEEACountries(e.privacyConfig.GDPR.EEACountries, r.Account.GDPR.EEACountries)

// Make our best guess if GDPR applies
gdprDefaultValue := e.parseGDPRDefaultValue(r.BidRequestWrapper)
gdprDefaultValue := e.parseGDPRDefaultValue(r.BidRequestWrapper, eeaCountries)
gdprSignal, err := getGDPR(r.BidRequestWrapper)
if err != nil {
return nil, err
Expand Down Expand Up @@ -571,7 +574,7 @@ func buildMultiBidMap(prebid *openrtb_ext.ExtRequestPrebid) map[string]openrtb_e
return multiBidMap
}

func (e *exchange) parseGDPRDefaultValue(r *openrtb_ext.RequestWrapper) gdpr.Signal {
func (e *exchange) parseGDPRDefaultValue(r *openrtb_ext.RequestWrapper, eeaCountries []string) gdpr.Signal {
gdprDefaultValue := e.gdprDefaultValue

var geo *openrtb2.Geo
Expand All @@ -582,12 +585,11 @@ func (e *exchange) parseGDPRDefaultValue(r *openrtb_ext.RequestWrapper) gdpr.Sig
}

if geo != nil {
// If we have a country set, and it is on the list, we assume GDPR applies if not set on the request.
// Otherwise we assume it does not apply as long as it appears "valid" (is 3 characters long).
if _, found := e.privacyConfig.GDPR.EEACountriesMap[strings.ToUpper(geo.Country)]; found {
// If the country is in the EEA list, GDPR applies.
// Otherwise, if the country code is properly formatted (3 characters), GDPR does not apply.
if isEEACountry(geo.Country, eeaCountries) {
gdprDefaultValue = gdpr.SignalYes
} else if len(geo.Country) == 3 {
// The country field is formatted properly as a three character country code
gdprDefaultValue = gdpr.SignalNo
}
}
Expand Down Expand Up @@ -1622,3 +1624,17 @@ func setSeatNonBid(bidResponseExt *openrtb_ext.ExtBidResponse, seatNonBidBuilder
bidResponseExt.Prebid.SeatNonBid = seatNonBidBuilder.Slice()
return bidResponseExt
}

func isEEACountry(country string, eeaCountries []string) bool {
if len(eeaCountries) == 0 {
return false
}

country = strings.ToUpper(country)
for _, c := range eeaCountries {
if strings.ToUpper(c) == country {
return true
}
}
return false
}
67 changes: 63 additions & 4 deletions exchange/exchange_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2114,9 +2114,10 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
}

var s struct{}
eeac := make(map[string]struct{})
for _, c := range []string{"FIN", "FRA", "GUF"} {
eeac[c] = s
eeacMap := make(map[string]struct{})
eeac := []string{"FIN", "FRA", "GUF"}
for _, c := range eeac {
eeacMap[c] = s
}

var gdprDefaultValue string
Expand All @@ -2136,7 +2137,8 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
GDPR: config.GDPR{
Enabled: spec.GDPREnabled,
DefaultValue: gdprDefaultValue,
EEACountriesMap: eeac,
EEACountries: eeac,
EEACountriesMap: eeacMap,
TCF2: config.TCF2{
Enabled: spec.GDPREnabled,
},
Expand Down Expand Up @@ -2192,6 +2194,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
PriceFloors: config.AccountPriceFloors{Enabled: spec.AccountFloorsEnabled, EnforceDealFloors: spec.AccountEnforceDealFloors},
Privacy: spec.AccountPrivacy,
Validations: spec.AccountConfigBidValidation,
GDPR: config.AccountGDPR{EEACountries: spec.AccountEEACountries},
},
UserSyncs: mockIdFetcher(spec.IncomingRequest.Usersyncs),
ImpExtInfoMap: impExtInfoMap,
Expand Down Expand Up @@ -5518,6 +5521,7 @@ type exchangeSpec struct {
Server exchangeServer `json:"server,omitempty"`
AccountPrivacy config.AccountPrivacy `json:"accountPrivacy,omitempty"`
ORTBVersion map[string]string `json:"ortbversion"`
AccountEEACountries []string `json:"account_eea_countries"`
}

type multiBidSpec struct {
Expand Down Expand Up @@ -6351,6 +6355,61 @@ func TestBidsToUpdate(t *testing.T) {
}
}

func TestIsEEACountry(t *testing.T) {
eeaCountries := []string{"FRA", "DEU", "ITA", "ESP", "NLD"}

tests := []struct {
name string
country string
eeaList []string
expected bool
}{
{
name: "Country_in_EEA",
country: "FRA",
eeaList: eeaCountries,
expected: true,
},
{
name: "Country_in_EEA_lowercase",
country: "fra",
eeaList: eeaCountries,
expected: true,
},
{
name: "Country_not_in_EEA",
country: "USA",
eeaList: eeaCountries,
expected: false,
},
{
name: "Empty_country_string",
country: "",
eeaList: eeaCountries,
expected: false,
},
{
name: "EEA_list_is_empty",
country: "FRA",
eeaList: []string{},
expected: false,
},
{
name: "EEA_list_is_nil",
country: "FRA",
eeaList: nil,
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isEEACountry(tt.country, tt.eeaList)
assert.Equal(t, tt.expected, result)
})
}
}

type mockRequestValidator struct {
errors []error
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"assume_gdpr_applies": true,
"account_eea_countries": ["POL"],
"gdpr_enabled": true,
"incomingRequest": {
"ortbRequest": {
"id": "some-request-id",
"site": {
"page": "test.somepage.com"
},
"imp": [
{
"id": "my-imp-id",
"video": {
"mimes": [
"video/mp4"
]
},
"ext": {
"prebid": {
"bidder": {
"appnexus": {
"placementId": 1
}
}
}
}
}
],
"user": {
"buyeruid": "some-buyer-id",
"geo": {
"country": "POL"
}
}
}
},
"outgoingRequests": {
"appnexus": {
"expectRequest": {
"ortbRequest": {
"id": "some-request-id",
"site": {
"page": "test.somepage.com"
},
"imp": [
{
"id": "my-imp-id",
"video": {
"mimes": [
"video/mp4"
]
},
"ext": {
"bidder": {
"placementId": 1
}
}
}
],
"user": {
"geo": {
"country": "POL"
}
}
}
},
"mockResponse": {
"errors": [
"appnexus-error"
]
}
}
}
}
9 changes: 9 additions & 0 deletions exchange/gdpr.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,12 @@ func enforceGDPR(signal gdpr.Signal, defaultValue gdpr.Signal, channelEnabled bo
gdprApplies := signal == gdpr.SignalYes || (signal == gdpr.SignalAmbiguous && defaultValue == gdpr.SignalYes)
return gdprApplies && channelEnabled
}

// SelectEEACountries selects the EEA countries based on host and account configurations.
// Account-level configuration takes precedence over the host-level configuration.
func selectEEACountries(hostEEACountries []string, accountEEACountries []string) []string {
if accountEEACountries != nil {
return accountEEACountries
}
return hostEEACountries
}
59 changes: 59 additions & 0 deletions exchange/gdpr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,65 @@ func TestGetConsent(t *testing.T) {
}
}

func TestSelectEEACountries(t *testing.T) {
tests := []struct {
description string
hostEEACountries []string
accountEEACountries []string
expected []string
}{
{
description: "Account_EEA_countries_provided",
hostEEACountries: []string{"UK", "DE"},
accountEEACountries: []string{"FR", "IT"},
expected: []string{"FR", "IT"},
},
{
description: "Account_is_nil",
hostEEACountries: []string{"UK"},
accountEEACountries: nil,
expected: []string{"UK"},
},
{
description: "Both_nil",
hostEEACountries: nil,
accountEEACountries: nil,
expected: nil,
},
{
description: "Account_is_empty_slice",
hostEEACountries: []string{"UK"},
accountEEACountries: []string{},
expected: []string{},
},
{
description: "Host_is_nil",
hostEEACountries: nil,
accountEEACountries: []string{"DE"},
expected: []string{"DE"},
},
{
description: "Host_and_account_both_non-nil",
hostEEACountries: []string{"UK"},
accountEEACountries: []string{"FR"},
expected: []string{"FR"},
},
{
description: "Host_is_empty_slice,_account_is_nil",
hostEEACountries: []string{},
accountEEACountries: nil,
expected: []string{},
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
result := selectEEACountries(tt.hostEEACountries, tt.accountEEACountries)
assert.Equal(t, tt.expected, result)
})
}
}

var upsv1Section mockGPPSection = mockGPPSection{sectionID: 6, value: "1YNY"}
var tcf1Section mockGPPSection = mockGPPSection{sectionID: 2, value: "BOS2bx5OS2bx5ABABBAAABoAAAAAFA"}

Expand Down

0 comments on commit b335bc5

Please sign in to comment.