diff --git a/adapters/bidder.go b/adapters/bidder.go index fb0ec07e032..f7a588ea025 100644 --- a/adapters/bidder.go +++ b/adapters/bidder.go @@ -149,6 +149,7 @@ type ExtraRequestInfo struct { GlobalPrivacyControlHeader string CurrencyConversions currency.Conversions PreferredMediaType openrtb_ext.BidType + PageViewId string } func NewExtraRequestInfo(c currency.Conversions) ExtraRequestInfo { diff --git a/adapters/kobler/kobler.go b/adapters/kobler/kobler.go index cb1a1acaa71..5f03ddaf1e8 100644 --- a/adapters/kobler/kobler.go +++ b/adapters/kobler/kobler.go @@ -1,6 +1,7 @@ package kobler import ( + "encoding/json" "fmt" "net/http" "slices" @@ -33,6 +34,14 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server co return bidder, nil } +type RequestExt struct { + Kobler *KoblerRequestExt `json:"kobler,omitempty"` +} + +type KoblerRequestExt struct { + PageViewId string `json:"page_view_id,omitempty"` +} + func (a adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { var requestData []*adapters.RequestData var errors []error @@ -45,6 +54,20 @@ func (a adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.Ex sanitizedRequest.Cur = append(sanitizedRequest.Cur, supportedCurrency) } + if reqInfo.PageViewId != "" { + var ext RequestExt + ext.Kobler = &KoblerRequestExt{ + reqInfo.PageViewId, + } + jsonExt, err := json.Marshal(ext) + if err != nil { + errors = append(errors, err) + return nil, errors + } + + sanitizedRequest.Ext = jsonExt + } + for i := range sanitizedRequest.Imp { if err := convertImpCurrency(&sanitizedRequest.Imp[i], reqInfo); err != nil { errors = append(errors, err) diff --git a/exchange/exchange.go b/exchange/exchange.go index 1915e7b2731..a37e963cac1 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -220,7 +220,7 @@ type AuctionRequest struct { GDPREnforced bool } -// BidderRequest holds the bidder specific request and all other +// BidderRequest holds the bidder-specific request and all other // information needed to process that bidder request. type BidderRequest struct { BidRequest *openrtb2.BidRequest @@ -230,6 +230,7 @@ type BidderRequest struct { BidderStoredResponses map[string]json.RawMessage IsRequestAlias bool ImpReplaceImpId map[string]bool + PageViewId string } func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog *DebugLog) (*AuctionResponse, error) { @@ -758,6 +759,7 @@ func (e *exchange) getAllBids( reqInfo := adapters.NewExtraRequestInfo(conversions) reqInfo.PbsEntryPoint = bidderRequest.BidderLabels.RType reqInfo.GlobalPrivacyControlHeader = globalPrivacyControlHeader + reqInfo.PageViewId = bidderRequest.PageViewId if len(liveAdaptersPreferredMediaType) > 0 { if mtype, found := liveAdaptersPreferredMediaType[bidder.BidderName]; found { diff --git a/exchange/utils.go b/exchange/utils.go index 9659f19570b..3689bf6a483 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -56,6 +56,7 @@ type requestSplitter struct { // 1. BidRequest.Imp[].Ext will only contain the "prebid" field and a "bidder" field which has the params for the intended Bidder. // 2. Every BidRequest.Imp[] requested Bids from the Bidder who keys it. // 3. BidRequest.User.BuyerUID will be set to that Bidder's ID. +// 4. BidRequest.Ext.PageViewId will be set to that Bidder's ID. func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, auctionReq AuctionRequest, requestExt *openrtb_ext.ExtRequest, @@ -92,6 +93,18 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, lowerCaseExplicitBuyerUIDs[lowerKey] = uid } + pageViewIds, err := extractAndCleanPageViewIds(req) + if err != nil { + errs = []error{err} + return + } + + lowerCasePageViewIds := make(map[string]string) + for bidder, pageViewId := range pageViewIds { + lowerKey := strings.ToLower(bidder) + lowerCasePageViewIds[lowerKey] = pageViewId + } + bidderParamsInReqExt, err := ExtractReqExtBidderParamsMap(req.BidRequest) if err != nil { errs = []error{err} @@ -246,6 +259,8 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, bidderLabels.AdapterBids = metrics.AdapterBidPresent } + pageViewId := pageViewIds[strings.ToLower(bidder)] + bidderRequest := BidderRequest{ BidderName: openrtb_ext.BidderName(bidder), BidderCoreName: coreBidder, @@ -254,6 +269,7 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, BidderStoredResponses: bidderImpWithBidResp[openrtb_ext.BidderName(bidder)], ImpReplaceImpId: auctionReq.BidderImpReplaceImpID[bidder], BidderLabels: bidderLabels, + PageViewId: pageViewId, } bidderRequests = append(bidderRequests, bidderRequest) } @@ -638,6 +654,31 @@ func extractAndCleanBuyerUIDs(req *openrtb_ext.RequestWrapper) (map[string]strin return buyerUIDs, nil } +// extractAndCleanPageViewIds parses the values from ext.prebid.page_view_ids, and then deletes those values from the ext. +// This prevents a Bidder from getting access to page view IDs of other Bidders. +func extractAndCleanPageViewIds(req *openrtb_ext.RequestWrapper) (map[string]string, error) { + if req.Ext == nil { + return nil, nil + } + + requestExt, err := req.GetRequestExt() + if err != nil { + return nil, err + } + + prebid := requestExt.GetPrebid() + if prebid == nil { + return nil, nil + } + + pageViewIds := prebid.PageViewIds + + prebid.PageViewIds = nil + requestExt.SetPrebid(prebid) + + return pageViewIds, nil +} + // splitImps takes a list of Imps and returns a map of imps which have been sanitized for each bidder. // // For example, suppose imps has two elements. One goes to rubicon, while the other goes to appnexus and index. diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 317d7a640cd..a3ade1b99aa 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -3976,7 +3976,125 @@ func TestBuildExtData(t *testing.T) { } } -func TestCleanOpenRTBRequestsFilterBidderRequestExt(t *testing.T) { +func TestCleanOpenRTBRequestsFilterBidderRequestExtForPageViewIds(t *testing.T) { + pageViewId1 := "a147eab5-9b1f-4fdc-b1f8-900a58c87a39" + pageViewId2 := "468dd24e-de62-4d2b-bdef-1daaea079e4c" + testCases := []struct { + desc string + inExt json.RawMessage + wantExt []json.RawMessage + wantPageViewIds []string + }{ + { + desc: "Nil request ext, empty page view IDs", + inExt: nil, + wantExt: nil, + wantPageViewIds: []string{"", ""}, + }, + { + desc: "Missing prebid in request ext, empty page view IDs", + inExt: json.RawMessage(`{}`), + wantExt: nil, + wantPageViewIds: []string{"", ""}, + }, + { + desc: "Missing page_view_ids in prebid in request ext, empty page view IDs", + inExt: json.RawMessage(`{"prebid": {}}`), + wantExt: nil, + wantPageViewIds: []string{"", ""}, + }, + { + desc: "Empty page_view_ids in prebid in request ext, empty page view IDs", + inExt: json.RawMessage(`{"prebid": {"page_view_ids":{}, "channel":{"name":"b","version":"c"}}}`), + wantExt: []json.RawMessage{ + json.RawMessage(`{"prebid":{"channel":{"name":"b","version":"c"}}}`), + json.RawMessage(`{"prebid":{"channel":{"name":"b","version":"c"}}}`), + }, + wantPageViewIds: []string{"", ""}, + }, + { + desc: "Non-matching page_view_ids in prebid in request ext, empty page view IDs", + inExt: json.RawMessage(`{"prebid":{"page_view_ids":{ + "some-bidder":"page-view-id-for-some-bidder", + "second-bidder":"page-view-id-for-second-bidder" + }}}`), + wantExt: nil, + wantPageViewIds: []string{"", ""}, + }, + { + desc: "Matching page_view_ids in prebid in request ext, non-empty page view IDs", + inExt: json.RawMessage(`{"prebid": { "channel": { "name":"b", "version":"c" }, "page_view_ids": { + "some-bidder":"page-view-id-for-some-bidder", + "appnexus": "a147eab5-9b1f-4fdc-b1f8-900a58c87a39", + "pubmatic": "468dd24e-de62-4d2b-bdef-1daaea079e4c" + }}}`), + wantExt: []json.RawMessage{ + json.RawMessage(`{"prebid":{"channel":{"name":"b","version":"c"}}}`), + json.RawMessage(`{"prebid":{"channel":{"name":"b","version":"c"}}}`), + }, + wantPageViewIds: []string{pageViewId1, pageViewId2}, + }, + { + desc: "One matching page_view_id in prebid in request ext, one non-empty page view ID", + inExt: json.RawMessage(`{ "prebid": { "channel": { "name":"b", "version":"c" }, "page_view_ids": { + "some-bidder":"page-view-id-for-some-bidder", + "appnexus": "a147eab5-9b1f-4fdc-b1f8-900a58c87a39" + }}}`), + wantExt: []json.RawMessage{ + json.RawMessage(`{"prebid":{"channel":{"name":"b","version":"c"}}}`), + json.RawMessage(`{"prebid":{"channel":{"name":"b","version":"c"}}}`), + }, + wantPageViewIds: []string{pageViewId1, ""}, + }, + } + + for _, test := range testCases { + req := newBidRequestWithBidderParams() + req.Ext = nil + var extRequest *openrtb_ext.ExtRequest + if test.inExt != nil { + req.Ext = test.inExt + extRequest = &openrtb_ext.ExtRequest{} + err := jsonutil.UnmarshalValid(req.Ext, extRequest) + assert.NoErrorf(t, err, test.desc+":Error unmarshaling inExt") + } + + auctionReq := AuctionRequest{ + BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, + UserSyncs: &emptyUsersync{}, + Account: config.Account{}, + TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}), + } + gdprPermissionsBuilder := fakePermissionsBuilder{ + permissions: &permissionsMock{ + allowAllBidders: true, + }, + }.Builder + + reqSplitter := &requestSplitter{ + bidderToSyncerKey: map[string]string{}, + me: &metrics.MetricsEngineMock{}, + privacyConfig: config.Privacy{}, + gdprPermsBuilder: gdprPermissionsBuilder, + hostSChainNode: nil, + bidderInfo: config.BidderInfos{}, + } + + bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, map[string]float64{}) + assert.Equal(t, 0, len(errs), test.desc) + sort.Slice(bidderRequests, func(i, j int) bool { + return bidderRequests[i].BidderCoreName < bidderRequests[j].BidderCoreName + }) + for i, wantBidderRequest := range test.wantExt { + assert.Equal(t, wantBidderRequest, bidderRequests[i].BidRequest.Ext, test.desc+" : "+string(bidderRequests[i].BidderCoreName)+"\n\t\tGotRequestExt : "+string(bidderRequests[i].BidRequest.Ext)) + } + for i, wantPageViewId := range test.wantPageViewIds { + assert.Equal(t, wantPageViewId, bidderRequests[i].PageViewId, test.desc+" : "+string(bidderRequests[i].BidderCoreName)+"\n\t\tGotPageViewId : "+bidderRequests[i].PageViewId) + } + } +} + +func TestCleanOpenRTBRequestsFilterBidderRequestExtForAlternateBidderCodes(t *testing.T) { testCases := []struct { desc string inExt json.RawMessage diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index 7f8978e6eb7..a28187bcd5c 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -44,6 +44,7 @@ type ExtRequest struct { type ExtRequestPrebid struct { AdServerTargeting []AdServerTarget `json:"adservertargeting,omitempty"` Aliases map[string]string `json:"aliases,omitempty"` + PageViewIds map[string]string `json:"page_view_ids,omitempty"` AliasGVLIDs map[string]uint16 `json:"aliasgvlids,omitempty"` Analytics map[string]json.RawMessage `json:"analytics,omitempty"` BidAdjustmentFactors map[string]float64 `json:"bidadjustmentfactors,omitempty"`