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

Yieldlab: Add Digital Service Act (DSA) handling #3473

Merged
56 changes: 49 additions & 7 deletions adapters/yieldlab/types.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,60 @@
package yieldlab

import (
"github.com/prebid/prebid-server/v2/openrtb_ext"
"strconv"
"time"
)

type bidResponse struct {
ID uint64 `json:"id"`
Price uint `json:"price"`
Advertiser string `json:"advertiser"`
Adsize string `json:"adsize"`
Pid uint64 `json:"pid"`
Did uint64 `json:"did"`
Pvid string `json:"pvid"`
ID uint64 `json:"id"`
Price uint `json:"price"`
Advertiser string `json:"advertiser"`
Adsize string `json:"adsize"`
Pid uint64 `json:"pid"`
Did uint64 `json:"did"`
Pvid string `json:"pvid"`
DSA *dsaResponse `json:"dsa,omitempty"`
}

// dsaResponse defines Digital Service Act (DSA) parameters from Yieldlab yieldprobe response.
type dsaResponse struct {
Behalf string `json:"behalf,omitempty"`
Paid string `json:"paid,omitempty"`
Adrender *int `json:"adrender,omitempty"`
Transparency []dsaTransparency `json:"transparency,omitempty"`
}

// openRTBExtRegsWithDSA defines the contract for bidrequest.regs.ext with the missing DSA property.
//
// The openrtb_ext.ExtRegs needs to be extended on yieldlab adapter level until DSA has been implemented
// by the prebid server team (https://github.com/prebid/prebid-server/issues/3424).
type openRTBExtRegsWithDSA struct {
openrtb_ext.ExtRegs
DSA *dsaRequest `json:"dsa,omitempty"`
}

// responseExtWithDSA defines seatbid.bid.ext with the DSA object.
type responseExtWithDSA struct {
DSA dsaResponse `json:"dsa"`
}

// dsaRequest defines Digital Service Act (DSA) parameter
// as specified by the OpenRTB 2.X DSA Transparency community extension.
//
// Should rather come from openrtb_ext package but will be defined here until DSA has been
// implemented by the prebid server team (https://github.com/prebid/prebid-server/issues/3424).
type dsaRequest struct {
Required *int `json:"dsarequired"`
PubRender *int `json:"pubrender"`
DataToPub *int `json:"datatopub"`
Transparency []dsaTransparency `json:"transparency"`
}

// dsaTransparency Digital Service Act (DSA) transparency object
type dsaTransparency struct {
Domain string `json:"domain,omitempty"`
Params []int `json:"dsaparams,omitempty"`
}

type cacheBuster func() string
Expand Down
107 changes: 105 additions & 2 deletions adapters/yieldlab/yieldlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,94 @@ func (a *YieldlabAdapter) makeEndpointURL(req *openrtb2.BidRequest, params *open
}
}

dsa, err := getDSA(req)
if err != nil {
return "", err
}
if dsa != nil {
if dsa.Required != nil {
q.Set("dsarequired", strconv.Itoa(*dsa.Required))
}
if dsa.PubRender != nil {
q.Set("dsapubrender", strconv.Itoa(*dsa.PubRender))
}
if dsa.DataToPub != nil {
q.Set("dsadatatopub", strconv.Itoa(*dsa.DataToPub))
Copy link
Collaborator

Choose a reason for hiding this comment

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

It is unclear to me as to what this parameter is supposed to be. I'm guessing what you have here is correct but I'm trying to confirm. In Object Specification for OpenRTB 2.X it is datatopub and in URL-based support it is dsadatapubs.
I would have expected the two to be the same with maybe the URL-based parameter having a dsa prefix that the object field does not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is indeed correct. The have prefixed all our DSA URL parameters, but saw no reason why this parameter should be plural, even though this might be against the current spec.

}
if len(dsa.Transparency) != 0 {
transparencyParam := makeDSATransparencyURLParam(dsa.Transparency)
if len(transparencyParam) != 0 {
q.Set("dsatransparency", transparencyParam)
}
}
}

uri.RawQuery = q.Encode()

return uri.String(), nil
}

// getDSA extracts the Digital Service Act (DSA) properties from the request.
func getDSA(req *openrtb2.BidRequest) (*dsaRequest, error) {
if req.Regs == nil || req.Regs.Ext == nil {
return nil, nil
}

var extRegs openRTBExtRegsWithDSA
err := json.Unmarshal(req.Regs.Ext, &extRegs)
if err != nil {
return nil, fmt.Errorf("failed to parse Regs.Ext object from Yieldlab response: %v", err)
}

return extRegs.DSA, nil
}

// makeDSATransparencyURLParam creates the transparency url parameter
// as specified by the OpenRTB 2.X DSA Transparency community extension.
//
// Example result: platform1domain.com~1~~SSP2domain.com~1_2
func makeDSATransparencyURLParam(transparencyObjects []dsaTransparency) string {
valueSeparator, itemSeparator, objectSeparator := "_", "~", "~~"

var b strings.Builder

concatParams := func(params []int) {
b.WriteString(strconv.Itoa(params[0]))
for _, param := range params[1:] {
b.WriteString(valueSeparator)
b.WriteString(strconv.Itoa(param))
}
}

concatTransparency := func(object dsaTransparency) {
if len(object.Domain) == 0 {
return
}

b.WriteString(object.Domain)
if len(object.Params) != 0 {
b.WriteString(itemSeparator)
concatParams(object.Params)
}
}

concatTransparencies := func(objects []dsaTransparency) {
if len(objects) == 0 {
return
}

concatTransparency(objects[0])
for _, obj := range objects[1:] {
b.WriteString(objectSeparator)
concatTransparency(obj)
}
}

concatTransparencies(transparencyObjects)

return b.String()
}

func (a *YieldlabAdapter) makeFormats(req *openrtb2.BidRequest) (bool, string) {
var formats []string
const sizesSeparator, adslotSizesSeparator = "|", ","
Expand Down Expand Up @@ -253,6 +336,7 @@ func (a *YieldlabAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa
}
}

var bidErrors []error
for _, bid := range bids {
width, height, err := splitSize(bid.Adsize)
if err != nil {
Expand All @@ -269,7 +353,13 @@ func (a *YieldlabAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa
if imp, exists := adslotToImpMap[strconv.FormatUint(bid.ID, 10)]; !exists {
continue
} else {
var bidType openrtb_ext.BidType
extJson, err := makeResponseExt(bid)
if err != nil {
bidErrors = append(bidErrors, err)
// skip as bids with missing ext.dsa will be discarded anyway
continue
}

responseBid := &openrtb2.Bid{
ID: strconv.FormatUint(bid.ID, 10),
Price: float64(bid.Price) / 100,
Expand All @@ -278,8 +368,10 @@ func (a *YieldlabAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa
DealID: strconv.FormatUint(bid.Pid, 10),
W: int64(width),
H: int64(height),
Ext: extJson,
}

var bidType openrtb_ext.BidType
if imp.Video != nil {
bidType = openrtb_ext.BidTypeVideo
responseBid.NURL = a.makeAdSourceURL(internalRequest, req, bid)
Expand All @@ -299,7 +391,18 @@ func (a *YieldlabAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa
}
}

return bidderResponse, nil
return bidderResponse, bidErrors
}

func makeResponseExt(bid *bidResponse) (json.RawMessage, error) {
if bid.DSA != nil {
extJson, err := json.Marshal(responseExtWithDSA{*bid.DSA})
if err != nil {
return nil, fmt.Errorf("failed to make JSON for seatbid.bid.ext for adslotID %v. This is most likely a programming issue", bid.ID)
}
return extJson, nil
}
return nil, nil
}

func (a *YieldlabAdapter) findBidReq(adslotID uint64, params []*openrtb_ext.ExtImpYieldlab) *openrtb_ext.ExtImpYieldlab {
Expand Down
92 changes: 92 additions & 0 deletions adapters/yieldlab/yieldlab_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,98 @@ func Test_makeSupplyChain(t *testing.T) {
}
}

func Test_makeDSATransparencyUrlParam(t *testing.T) {
tests := []struct {
name string
transparencies []dsaTransparency
expected string
}{
{
Copy link
Collaborator

Choose a reason for hiding this comment

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

You might want to consider adding two more test cases for when transparencies is nil and when Params is nil.

name: "No transparency objects",
transparencies: []dsaTransparency{},
expected: "",
},
{
name: "Nil transparency",
transparencies: nil,
expected: "",
},
{
name: "Params without a domain",
transparencies: []dsaTransparency{
{
Params: []int{1, 2},
},
},
expected: "",
},
{
name: "Params without a params",
transparencies: []dsaTransparency{
{
Domain: "domain.com",
},
},
expected: "domain.com",
},
{
name: "One object; No Params",
transparencies: []dsaTransparency{
{
Domain: "domain.com",
Params: []int{},
},
},
expected: "domain.com",
},
{
name: "One object; One Param",
transparencies: []dsaTransparency{
{
Domain: "domain.com",
Params: []int{1},
},
},
expected: "domain.com~1",
},
{
name: "Three domain objects",
transparencies: []dsaTransparency{
{
Domain: "domain1.com",
Params: []int{1, 2},
},
{
Domain: "domain2.com",
Params: []int{3, 4},
},
{
Domain: "domain3.com",
Params: []int{5, 6},
},
},
expected: "domain1.com~1_2~~domain2.com~3_4~~domain3.com~5_6",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := makeDSATransparencyURLParam(test.transparencies)
assert.Equal(t, test.expected, actual)
})
}
}

func Test_getDSA_invalidRequestExt(t *testing.T) {
req := &openrtb2.BidRequest{
Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"DSA":"wrongValueType"}`)},
}

dsa, err := getDSA(req)

assert.NotNil(t, err)
assert.Nil(t, dsa)
}
Comment on lines +341 to +350
Copy link
Collaborator

Choose a reason for hiding this comment

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

Error cases like this should be covered using the JSON test framework wherever possible. To do so, you can create a new directory yieldlabtest/supplemental with a new JSON test in it (e.g. invalid-reg-ext) that has an invalid reg.ext in the mockBidRequest:

"regs": {
  "ext": {
    "dsa": ""
  }
}

In your JSON test you declare the expected MakeRequests error as such:

"expectedMakeRequestsErrors": [{
    "value": "failed to parse Regs.Ext object from Yieldlab response: json: cannot unmarshal string into Go struct field openRTBExtRegsWithDSA.dsa of type yieldlab.dsaRequest",
    "comparison": "literal"
  }]


func TestYieldlabAdapter_makeEndpointURL_invalidEndpoint(t *testing.T) {
bidder, buildErr := Builder(openrtb_ext.BidderYieldlab, config.Adapter{
Endpoint: "test$:/something§"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
Expand Down
Loading
Loading