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

New Adapter: Connatix #3916

Merged
merged 36 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
395401d
add models for adapter
patrickszeleczki Sep 5, 2024
714bfe7
add adapter
patrickszeleczki Sep 5, 2024
51f4e61
add bidder info
patrickszeleczki Sep 5, 2024
215430b
add bidder params along with tests
patrickszeleczki Sep 5, 2024
185aeaa
add bid request tests
patrickszeleczki Sep 5, 2024
b377051
Merge branch 'prebid:master' into add-cnx-adapter
patrickszeleczki Sep 5, 2024
5ed6570
fix endpoint
patrickszeleczki Sep 6, 2024
8c03223
add validation for device ip
patrickszeleczki Sep 6, 2024
b61d5ff
add the maintainer email
patrickszeleczki Sep 9, 2024
1c5bcac
add user sync support
patrickszeleczki Sep 9, 2024
2162440
fix user sync config
patrickszeleczki Sep 10, 2024
5144c53
Merge pull request #1 from Connatix/add-cnx-adapter
patrickszeleczki Sep 12, 2024
08242be
Merge branch 'prebid:master' into master
patrickszeleczki Sep 12, 2024
55ec262
Merge branch 'prebid:master' into master
patrickszeleczki Sep 26, 2024
b57a08a
fix content type header
patrickszeleczki Sep 26, 2024
28ed966
Merge pull request #2 from Connatix/add-cnx-adapter
patrickszeleczki Sep 26, 2024
b5f955d
include ipv6 in ip validation and add headers
patrickszeleczki Oct 2, 2024
4c7a620
Merge pull request #3 from Connatix/add-cnx-adapter
patrickszeleczki Oct 2, 2024
6b50d55
improve test coverage
patrickszeleczki Oct 3, 2024
15bdaa2
Merge branch 'prebid:master' into master
patrickszeleczki Oct 3, 2024
c29ca38
Merge branch 'master' into add-cnx-adapter
patrickszeleczki Oct 3, 2024
9d79042
Merge pull request #4 from Connatix/add-cnx-adapter
patrickszeleczki Oct 3, 2024
a69ba3c
Merge branch 'master' into master
patrickszeleczki Nov 4, 2024
ce01457
use v3 package version
patrickszeleczki Nov 4, 2024
4be32f5
Merge pull request #5 from Connatix/add-cnx-adapter
patrickszeleczki Nov 4, 2024
66d781c
add nil check before accessing Device fields
patrickszeleczki Nov 11, 2024
8d50d87
remove validations already handled by PBS core
patrickszeleczki Nov 11, 2024
967e769
small improvements
patrickszeleczki Nov 11, 2024
586d31c
delete tests for removed validations
patrickszeleczki Nov 11, 2024
181c4f9
add more supplemental tests
patrickszeleczki Nov 11, 2024
5e74e5c
Merge branch 'prebid:master' into master
patrickszeleczki Nov 11, 2024
b04d466
Merge branch 'master' into add-cnx-adapter
patrickszeleczki Nov 11, 2024
38d26e9
Merge pull request #6 from Connatix/add-cnx-adapter
patrickszeleczki Nov 11, 2024
b4b1be2
Merge branch 'prebid:master' into master
patrickszeleczki Dec 3, 2024
8fcb657
small fixes
patrickszeleczki Dec 3, 2024
a8891b9
Merge pull request #7 from Connatix/add-cnx-adapter
patrickszeleczki Dec 3, 2024
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
269 changes: 269 additions & 0 deletions adapters/connatix/connatix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
package connatix

import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"

"github.com/buger/jsonparser"
"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v2/adapters"
"github.com/prebid/prebid-server/v2/config"
"github.com/prebid/prebid-server/v2/errortypes"
"github.com/prebid/prebid-server/v2/openrtb_ext"
)

const (
maxImpsPerReq = 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do you have a value of 1 here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

because right now we only support a single impression per request and it would be much simpler and clearer (for new devs too) to update this constraint when we will support multiple impressions per request

)

// Builder builds a new instance of the Connatix adapter for the given bidder with the given config.
func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
uri, err := url.Parse(config.Endpoint)
if err != nil {
return nil, err
}

bidder := &adapter{
uri: *uri,
}
return bidder, nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't see any endpoint params. You can simplify this to:

bidder := &adapter{
	endpoint: config.Endpoint,
}
return bidder, nil

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

}

func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
if request.Site == nil && request.App == nil {
return nil, []error{&errortypes.BadInput{
Message: "Either site or app object is required",
}}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can delete this check. Core will only call your adapter if site or app is present in the request because you have declared support for both site and app in your YAML file and have not declared support for the third option, DOOH.

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 8d50d87


if request.Device != nil && request.Device.IP == "" && request.Device.IPv6 == "" {
return nil, []error{&errortypes.BadInput{
Message: "Device IP is required",
}}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you also want to report this error if request.Device == nil?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

indeed. thank you! fixed

}

// connatix adapter expects imp.displaymanagerver to be populated in openrtb2 request
// but some SDKs will put it in imp.ext.prebid instead
displayManagerVer := buildDisplayManageVer(request)

var errs []error

validImps := []openrtb2.Imp{}
for i := 0; i < len(request.Imp); i++ {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nitpick: I suggest using a range here: for i, _ := range request.Imp {

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 967e769

Copy link
Collaborator

Choose a reason for hiding this comment

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

I suggest adding a multi imp test, perhaps where one is of type banner and another is of type video. You could also include a third imp with an error so you can be sure two imp requests are generated and one error is returned.

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 181c4f9

impExtIncoming, err := validateAndBuildImpExt(&request.Imp[i])
if err != nil {
errs = append(errs, err)
continue
}

if err := buildRequestImp(&request.Imp[i], impExtIncoming, displayManagerVer, reqInfo); err != nil {
errs = append(errs, err)
continue
}

validImps = append(validImps, request.Imp[i])
}
request.Imp = validImps
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm curious if this assignment is necessary. You could simplify a bit by just passing validImps directly into splitRequests.
The only reason I see this potentially being of value is if you wanted MakeBids to have access to this curated list of imps but you aren't using the internalRequest parameter in MakeBids so it doesn't seem worth it.

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 967e769


// If all the requests were malformed, don't bother making a server call with no impressions.
if len(request.Imp) == 0 {
return nil, errs
}
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 be able to delete this. I think your splitRequests function will gracefully handle this scenario.

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 967e769


// Divide imps to several requests
requests, errors := splitRequests(request.Imp, request, a.uri.String())
return requests, append(errs, errors...)
}

func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
if adapters.IsResponseStatusCodeNoContent(response) {
return nil, nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a supplemental JSON test for a 204 response.

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 181c4f9

}

if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil {
return nil, []error{err}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a supplemental JSON test for a 400 response

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 181c4f9

}

var connatixResponse openrtb2.BidResponse
if err := json.Unmarshal(response.Body, &connatixResponse); err != nil {
return nil, []error{err}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a supplemental JSON test for an invalid response. You should be able to do this easily by having your response include a known field that maps to the struct that is of the wrong type.

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 181c4f9

}

var errs []error
bidderResponse := adapters.NewBidderResponseWithBidsCapacity(1)
for _, sb := range connatixResponse.SeatBid {
for i := range sb.Bid {
bid := sb.Bid[i]
var bidExt bidExt
var bidType openrtb_ext.BidType

if err := json.Unmarshal(bid.Ext, &bidExt); err != nil {
bidType = openrtb_ext.BidTypeBanner
} else {
bidType = getBidType(bidExt)
}

bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{
Bid: &bid,
BidType: bidType,
})
}
}

bidderResponse.Currency = "USD"

return bidderResponse, errs
}

func validateAndBuildImpExt(imp *openrtb2.Imp) (impExtIncoming, error) {
var ext impExtIncoming
if err := json.Unmarshal(imp.Ext, &ext); err != nil {
return impExtIncoming{}, err
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a supplemental JSON test where the imp.ext cannot be unmarshaled. You should be able to do this by providing an imp.ext.bidder.placementId value that is some type other than a string.

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 181c4f9

}

if err := validateConnatixExt(&ext.Bidder); err != nil {
return impExtIncoming{}, err
}

return ext, nil
}

func validateConnatixExt(cnxExt *openrtb_ext.ExtImpConnatix) error {
if cnxExt.PlacementId == "" {
return &errortypes.BadInput{
Message: "Placement id is required",
}
}
return nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can delete the validation logic here. There's no need to ensure your placement ID is not empty because you have added a rule to your bidder-params JSON file that indicates it has a minimum length of 1. If a request came in with a placement ID of empty string, PBS Core would report this error to the publisher and skip calling your adapter.

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 8d50d87


func splitRequests(imps []openrtb2.Imp, request *openrtb2.BidRequest, uri string) ([]*adapters.RequestData, []error) {
var errs []error
// Initial capacity for future array of requests, memory optimization.
// Let's say there are 35 impressions and limit impressions per request equals to 10.
// In this case we need to create 4 requests with 10, 10, 10 and 5 impressions.
// With this formula initial capacity=(35+10-1)/10 = 4
initialCapacity := (len(imps) + maxImpsPerReq - 1) / maxImpsPerReq
resArr := make([]*adapters.RequestData, 0, initialCapacity)
startInd := 0
impsLeft := len(imps) > 0

headers := http.Header{}
headers.Add("Content-Type", "application/json")
headers.Add("Accept", "application/json")

if len(request.Device.UA) > 0 {
headers.Add("User-Agent", request.Device.UA)
}

if len(request.Device.IPv6) > 0 {
headers.Add("X-Forwarded-For", request.Device.IPv6)
}

if len(request.Device.IP) > 0 {
headers.Add("X-Forwarded-For", request.Device.IP)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

request.Device is a pointer so you'll need a nil check before accessing these fields on Device.

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 66d781c


for impsLeft {
endInd := startInd + maxImpsPerReq
if endInd >= len(imps) {
endInd = len(imps)
impsLeft = false
}
impsForReq := imps[startInd:endInd]
request.Imp = impsForReq

reqJSON, err := json.Marshal(request)
if err != nil {
errs = append(errs, err)
return nil, errs
}

resArr = append(resArr, &adapters.RequestData{
Method: "POST",
Uri: uri,
Body: reqJSON,
Headers: headers,
ImpIDs: openrtb_ext.GetImpIDs(request.Imp),
})
startInd = endInd
}
return resArr, errs
}

func buildRequestImp(imp *openrtb2.Imp, ext impExtIncoming, displayManagerVer string, reqInfo *adapters.ExtraRequestInfo) error {
if imp.Video == nil && imp.Banner == nil {
return &errortypes.BadInput{
Message: "Either video or banner object on impression is required",
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can delete this check. Given that you've declared support for banner and video in your yaml file for both site and app, PBS core will skip calling your adapter if it contains impressions that are not one of those formats.

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 8d50d87


if imp.Banner != nil {
bannerCopy := *imp.Banner

if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 {
firstFormat := bannerCopy.Format[0]
bannerCopy.W = &(firstFormat.W)
bannerCopy.H = &(firstFormat.H)
}
imp.Banner = &bannerCopy
}

// Populate imp.displaymanagerver if the SDK failed to do it.
if len(imp.DisplayManagerVer) == 0 && len(displayManagerVer) > 0 {
imp.DisplayManagerVer = displayManagerVer
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a supplemental test where the computed display manager version is copied to the imp. Perhaps this is the same test that adds the additional coverage I'm asking for in buildDisplayManageVer.

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 181c4f9

}

// Check if imp comes with bid floor amount defined in a foreign currency
if imp.BidFloor > 0 && imp.BidFloorCur != "" && !strings.EqualFold(imp.BidFloorCur, "USD") {
// Convert to US dollars
convertedValue, err := reqInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, "USD")
if err != nil {
return err
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a supplemental JSON test where the currency conversion fails. You should be able to force this by including an invalid bid floor currency in the impression.

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 181c4f9

}
// Update after conversion. All imp elements inside request.Imp are shallow copies
// therefore, their non-pointer values are not shared memory and are safe to modify.
imp.BidFloorCur = "USD"
imp.BidFloor = convertedValue
}

impExt := impExt{
Connatix: impExtConnatix{
PlacementId: ext.Bidder.PlacementId,
},
}

var err error
imp.Ext, err = json.Marshal(impExt)

return err
}

func buildDisplayManageVer(req *openrtb2.BidRequest) string {
if req.App == nil {
return ""
}

source, err := jsonparser.GetString(req.App.Ext, openrtb_ext.PrebidExtKey, "source")
if err != nil {
return ""
}

version, err := jsonparser.GetString(req.App.Ext, openrtb_ext.PrebidExtKey, "version")
if err != nil {
return ""
}
Comment on lines +223 to +225
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a supplemental JSON test where reading the version fails.

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 181c4f9


return fmt.Sprintf("%s-%s", source, version)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add test coverage for this line where both source and version exist

Copy link
Contributor Author

@patrickszeleczki patrickszeleczki Nov 11, 2024

Choose a reason for hiding this comment

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

handled in 181c4f9

}

func getBidType(ext bidExt) openrtb_ext.BidType {
if ext.Cnx.MediaType == "video" {
return openrtb_ext.BidTypeVideo
}

return openrtb_ext.BidTypeBanner
}
20 changes: 20 additions & 0 deletions adapters/connatix/connatix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package connatix

import (
"testing"

"github.com/prebid/prebid-server/v2/adapters/adapterstest"
"github.com/prebid/prebid-server/v2/config"
"github.com/prebid/prebid-server/v2/openrtb_ext"
)

func TestJsonSamples(t *testing.T) {
bidder, buildErr := Builder(openrtb_ext.BidderConnatix, config.Adapter{
Endpoint: "http://example.com"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})

if buildErr != nil {
t.Fatalf("Builder returned unexpected error %v", buildErr)
}

adapterstest.RunJSONBidderTest(t, "connatixtest", bidder)
}
Loading
Loading