Skip to content
This repository has been archived by the owner on Dec 22, 2022. It is now read-only.

Commit

Permalink
Add support for multiple root schain nodes (prebid#1374)
Browse files Browse the repository at this point in the history
  • Loading branch information
bsardo authored Jul 15, 2020
1 parent 9656caa commit 0c92177
Show file tree
Hide file tree
Showing 5 changed files with 366 additions and 7 deletions.
9 changes: 9 additions & 0 deletions endpoints/openrtb2/auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error {
if err := validateBidAdjustmentFactors(bidExt.Prebid.BidAdjustmentFactors, aliases); err != nil {
return []error{err}
}

if err := validateSChains(bidExt); err != nil {
return []error{err}
}
}

if (req.Site == nil && req.App == nil) || (req.Site != nil && req.App != nil) {
Expand Down Expand Up @@ -362,6 +366,11 @@ func validateBidAdjustmentFactors(adjustmentFactors map[string]float64, aliases
return nil
}

func validateSChains(req *openrtb_ext.ExtRequest) error {
_, err := exchange.BidderToPrebidSChains(req)
return err
}

func (deps *endpointDeps) validateImp(imp *openrtb.Imp, aliases map[string]string, index int) []error {
if imp.ID == "" {
return []error{fmt.Errorf("request.imp[%d] missing required field: \"id\"", index)}
Expand Down
47 changes: 47 additions & 0 deletions endpoints/openrtb2/auction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,53 @@ func TestCCPAInvalid(t *testing.T) {
assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request")
}

func TestSChainInvalid(t *testing.T) {
deps := &endpointDeps{
&nobidExchange{},
newParamsValidator(t),
&mockStoredReqFetcher{},
empty_fetcher.EmptyFetcher{},
empty_fetcher.EmptyFetcher{},
&config.Configuration{},
pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}),
analyticsConf.NewPBSAnalytics(&config.Analytics{}),
map[string]string{},
false,
[]byte{},
openrtb_ext.BidderMap,
nil,
nil,
hardcodedResponseIPValidator{response: true},
}

ui := uint64(1)
req := openrtb.BidRequest{
ID: "someID",
Imp: []openrtb.Imp{
{
ID: "imp-ID",
Banner: &openrtb.Banner{
W: &ui,
H: &ui,
},
Ext: json.RawMessage(`{"appnexus": {"placementId": 5667}}`),
},
},
Site: &openrtb.Site{
ID: "myID",
},
Regs: &openrtb.Regs{
Ext: json.RawMessage(`{"us_privacy":"abcd"}`),
},
Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`),
}

errL := deps.validateRequest(&req)

expectedError := fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder appnexus; it must contain no more than one per bidder.")
assert.ElementsMatch(t, errL, []error{expectedError})
}

func TestSanitizeRequest(t *testing.T) {
testCases := []struct {
description string
Expand Down
94 changes: 93 additions & 1 deletion exchange/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ type cleanMetrics struct {
gdprTcfVersion int
}

func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) {
bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain)

if len(req.Prebid.SChains) == 0 {
return bidderToSChains, nil
}

for _, schainWrapper := range req.Prebid.SChains {
if schainWrapper != nil && len(schainWrapper.Bidders) > 0 {
for _, bidder := range schainWrapper.Bidders {
if _, present := bidderToSChains[bidder]; present {
return nil, fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder %s; "+
"it must contain no more than one per bidder.", bidder)
} else {
bidderToSChains[bidder] = &schainWrapper.SChain
}
}
}
}

return bidderToSChains, nil
}

// cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is:
//
// 1. BidRequest.Imp[].Ext will only contain the "prebid" field and a "bidder" field which has the params for the intended Bidder.
Expand Down Expand Up @@ -103,12 +126,35 @@ func cleanOpenRTBRequests(ctx context.Context,
return
}

func splitBidRequest(req *openrtb.BidRequest, impsByBidder map[string][]openrtb.Imp, aliases map[string]string, usersyncs IdFetcher, blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels, labels pbsmetrics.Labels) (map[openrtb_ext.BidderName]*openrtb.BidRequest, []error) {
func splitBidRequest(req *openrtb.BidRequest,
impsByBidder map[string][]openrtb.Imp,
aliases map[string]string,
usersyncs IdFetcher,
blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels,
labels pbsmetrics.Labels) (map[openrtb_ext.BidderName]*openrtb.BidRequest, []error) {

requestsByBidder := make(map[openrtb_ext.BidderName]*openrtb.BidRequest, len(impsByBidder))
explicitBuyerUIDs, err := extractBuyerUIDs(req.User)
if err != nil {
return nil, []error{err}
}

var requestExt openrtb_ext.ExtRequest
var sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain
if len(req.Ext) > 0 {
err := json.Unmarshal(req.Ext, &requestExt)
if err != nil {
return nil, []error{err}
}

sChainsByBidder, err = BidderToPrebidSChains(&requestExt)
if err != nil {
return nil, []error{err}
}
} else {
sChainsByBidder = make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain)
}

for bidder, imps := range impsByBidder {
reqCopy := *req
coreBidder := resolveBidder(bidder, aliases)
Expand All @@ -128,11 +174,57 @@ func splitBidRequest(req *openrtb.BidRequest, impsByBidder map[string][]openrtb.
blabels[coreBidder].CookieFlag = pbsmetrics.CookieFlagYes
}
reqCopy.Imp = imps

prepareSource(&reqCopy, bidder, sChainsByBidder)
prepareExt(&reqCopy, &requestExt)

requestsByBidder[openrtb_ext.BidderName(bidder)] = &reqCopy
}
return requestsByBidder, nil
}

func prepareExt(req *openrtb.BidRequest, unpackedExt *openrtb_ext.ExtRequest) {
if len(req.Ext) == 0 {
return
}
extCopy := *unpackedExt
extCopy.Prebid.SChains = nil
reqExt, err := json.Marshal(extCopy)
if err == nil {
req.Ext = reqExt
}
}

func prepareSource(req *openrtb.BidRequest, bidder string, sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) {
const sChainWildCard = "*"
var selectedSChain *openrtb_ext.ExtRequestPrebidSChainSChain

wildCardSChain := sChainsByBidder[sChainWildCard]
bidderSChain := sChainsByBidder[bidder]

// source should not be modified
if bidderSChain == nil && wildCardSChain == nil {
return
}

if bidderSChain != nil {
selectedSChain = bidderSChain
} else {
selectedSChain = wildCardSChain
}

// set source
var source openrtb.Source
schain := openrtb_ext.ExtRequestPrebidSChain{
SChain: *selectedSChain,
}
sourceExt, err := json.Marshal(schain)
if err == nil {
source.Ext = sourceExt
req.Source = &source
}
}

// extractBuyerUIDs parses the values from user.ext.prebid.buyeruids, and then deletes those values from the ext.
// This prevents a Bidder from using these values to figure out who else is involved in the Auction.
func extractBuyerUIDs(user *openrtb.User) (map[string]string, error) {
Expand Down
180 changes: 180 additions & 0 deletions exchange/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,92 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
}
}

func TestCleanOpenRTBRequestsSChain(t *testing.T) {
testCases := []struct {
description string
inSourceExt json.RawMessage
inExt json.RawMessage
outSourceExt json.RawMessage
outExt json.RawMessage
hasError bool
}{
{
description: "Empty root ext and source ext",
inSourceExt: json.RawMessage(``),
inExt: json.RawMessage(``),
outSourceExt: json.RawMessage(``),
outExt: json.RawMessage(``),
hasError: false,
},
{
description: "No schains in root ext and empty source ext",
inSourceExt: json.RawMessage(``),
inExt: json.RawMessage(`{"prebid":{"schains":[]}}`),
outSourceExt: json.RawMessage(``),
outExt: json.RawMessage(`{"prebid":{}}`),
hasError: false,
},
{
description: "Use source schain -- no bidder schain or wildcard schain in ext.prebid.schains",
inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`),
inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["bidder1"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`),
outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`),
outExt: json.RawMessage(`{"prebid":{}}`),
hasError: false,
},
{
description: "Use schain for bidder in ext.prebid.schains",
inSourceExt: json.RawMessage(``),
inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`),
outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`),
outExt: json.RawMessage(`{"prebid":{}}`),
hasError: false,
},
{
description: "Use wildcard schain in ext.prebid.schains",
inSourceExt: json.RawMessage(``),
inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`),
outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`),
outExt: json.RawMessage(`{"prebid":{}}`),
hasError: false,
},
{
description: "Use schain for bidder in ext.prebid.schains instead of wildcard",
inSourceExt: json.RawMessage(``),
inExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"},"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"wildcard.com","sid":"wildcard1","rid":"WildcardReq1","hp":1}],"ver":"1.0"}} ]}}`),
outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`),
outExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"}}}`),
hasError: false,
},
{
description: "Use source schain -- multiple (two) bidder schains in ext.prebid.schains",
inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`),
inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`),
outSourceExt: nil,
outExt: nil,
hasError: true,
},
}

for _, test := range testCases {
req := newBidRequest(t)
req.Source.Ext = test.inSourceExt
req.Ext = test.inExt

results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, config.Privacy{})
result := results["appnexus"]

if test.hasError == true {
assert.NotNil(t, errs)
assert.Nil(t, result)
} else {
assert.Nil(t, errs)
assert.Equal(t, test.outSourceExt, result.Source.Ext, test.description+":Source.Ext")
assert.Equal(t, test.outExt, result.Ext, test.description+":Ext")
}
}
}

func TestCleanOpenRTBRequestsLMT(t *testing.T) {
var (
enabled int8 = 1
Expand Down Expand Up @@ -302,5 +388,99 @@ func TestRandomizeList(t *testing.T) {
if len(adapters) != 1 {
t.Errorf("RandomizeList, expected a list of 1, found %d", len(adapters))
}
}

func TestBidderToPrebidChains(t *testing.T) {
input := openrtb_ext.ExtRequest{
Prebid: openrtb_ext.ExtRequestPrebid{
SChains: []*openrtb_ext.ExtRequestPrebidSChain{
{
Bidders: []string{"Bidder1", "Bidder2"},
SChain: openrtb_ext.ExtRequestPrebidSChainSChain{
Complete: 1,
Nodes: []*openrtb_ext.ExtRequestPrebidSChainSChainNode{
{
ASI: "asi1",
SID: "sid1",
Name: "name1",
RID: "rid1",
Domain: "domain1",
HP: 1,
},
{
ASI: "asi2",
SID: "sid2",
Name: "name2",
RID: "rid2",
Domain: "domain2",
HP: 2,
},
},
Ver: "version1",
},
},
{
Bidders: []string{"Bidder3", "Bidder4"},
SChain: openrtb_ext.ExtRequestPrebidSChainSChain{},
},
},
},
}

output, err := BidderToPrebidSChains(&input)

assert.Nil(t, err)
assert.Equal(t, len(output), 4)
assert.Same(t, output["Bidder1"], &input.Prebid.SChains[0].SChain)
assert.Same(t, output["Bidder2"], &input.Prebid.SChains[0].SChain)
assert.Same(t, output["Bidder3"], &input.Prebid.SChains[1].SChain)
assert.Same(t, output["Bidder4"], &input.Prebid.SChains[1].SChain)
}

func TestBidderToPrebidChainsDiscardMultipleChainsForBidder(t *testing.T) {
input := openrtb_ext.ExtRequest{
Prebid: openrtb_ext.ExtRequestPrebid{
SChains: []*openrtb_ext.ExtRequestPrebidSChain{
{
Bidders: []string{"Bidder1"},
SChain: openrtb_ext.ExtRequestPrebidSChainSChain{},
},
{
Bidders: []string{"Bidder1", "Bidder2"},
SChain: openrtb_ext.ExtRequestPrebidSChainSChain{},
},
},
},
}

output, err := BidderToPrebidSChains(&input)

assert.NotNil(t, err)
assert.Nil(t, output)
}

func TestBidderToPrebidChainsNilSChains(t *testing.T) {
input := openrtb_ext.ExtRequest{
Prebid: openrtb_ext.ExtRequestPrebid{
SChains: nil,
},
}

output, err := BidderToPrebidSChains(&input)

assert.Nil(t, err)
assert.Equal(t, len(output), 0)
}

func TestBidderToPrebidChainsZeroLengthSChains(t *testing.T) {
input := openrtb_ext.ExtRequest{
Prebid: openrtb_ext.ExtRequestPrebid{
SChains: []*openrtb_ext.ExtRequestPrebidSChain{},
},
}

output, err := BidderToPrebidSChains(&input)

assert.Nil(t, err)
assert.Equal(t, len(output), 0)
}
Loading

0 comments on commit 0c92177

Please sign in to comment.