Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
141 changes: 141 additions & 0 deletions adapters/agenticx/agenticx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package agenticx

import (
"fmt"
"net/http"

"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v4/adapters"
"github.com/prebid/prebid-server/v4/config"
"github.com/prebid/prebid-server/v4/errortypes"
"github.com/prebid/prebid-server/v4/openrtb_ext"
"github.com/prebid/prebid-server/v4/util/jsonutil"
)

type adapter struct {
endpoint string
}

func Builder(_ openrtb_ext.BidderName, cfg config.Adapter, _ config.Server) (adapters.Bidder, error) {
return &adapter{endpoint: cfg.Endpoint}, nil
}

func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
var errs []error
validImps := make([]openrtb2.Imp, 0, len(request.Imp))
var setTestMode bool

for _, imp := range request.Imp {
impExt, err := parseImpExt(imp.Ext)
if err != nil {
errs = append(errs, fmt.Errorf("impID %s: %w", imp.ID, err))
continue
}

if imp.Banner == nil && imp.Video == nil && imp.Audio == nil {
errs = append(errs, fmt.Errorf("impID %s: no banner, video, or audio object specified", imp.ID))
continue
}

if imp.BidFloor == 0 && impExt.BidFloor > 0 {
imp.BidFloor = impExt.BidFloor
}

if impExt.TestMode == 1 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

testMode is an imp-level parameter, but setting it causes request.Test = 1 — a request-level flag that affects every impression in the outgoing request. If only one imp out of several has testMode: 1, all imps are sent as a test request.

Is this intentional? If so, please add a comment explaining the design decision. If not, consider an alternative approach (e.g. reject the request entirely, or apply test mode only if all imps agree).

setTestMode = true
}

validImps = append(validImps, imp)
}

if len(validImps) == 0 {
return nil, append(errs, fmt.Errorf("no valid impressions"))
}

request.Imp = validImps

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

MakeRequests receives request as a pointer and modifies it directly (request.Imp = validImps, request.Test = 1). In multi-bidder auctions, PBS passes the same *openrtb2.BidRequest pointer to every adapter concurrently — mutating it here can corrupt what other adapters see.

Please work on a shallow copy:

reqCopy := *request
reqCopy.Imp = validImps
if setTestMode {
    reqCopy.Test = 1
}
reqJSON, err := jsonutil.Marshal(reqCopy)

if setTestMode {
request.Test = 1
}

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

headers := http.Header{}
headers.Set("Content-Type", "application/json;charset=utf-8")
headers.Set("Accept", "application/json")

return []*adapters.RequestData{
{
Method: "POST",
Uri: a.endpoint,
Body: reqJSON,
Headers: headers,
ImpIDs: openrtb_ext.GetImpIDs(validImps),
},
}, errs
}

func parseImpExt(ext jsonutil.RawMessage) (openrtb_ext.ImpExtAgenticx, error) {
var bidderExt adapters.ExtImpBidder
if err := jsonutil.Unmarshal(ext, &bidderExt); err != nil {
return openrtb_ext.ImpExtAgenticx{}, err
}
var agenticxExt openrtb_ext.ImpExtAgenticx
if err := jsonutil.Unmarshal(bidderExt.Bidder, &agenticxExt); err != nil {
return openrtb_ext.ImpExtAgenticx{}, err
}
return agenticxExt, nil
}

func (a *adapter) MakeBids(request *openrtb2.BidRequest, reqData *adapters.RequestData, respData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
if adapters.IsResponseStatusCodeNoContent(respData) {
return nil, nil
}
if err := adapters.CheckResponseStatusCodeForErrors(respData); err != nil {
return nil, []error{err}
}

var bidResp openrtb2.BidResponse
if err := jsonutil.Unmarshal(respData.Body, &bidResp); err != nil {
return nil, []error{err}
}

br := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid))
if bidResp.Cur != "" {
br.Currency = bidResp.Cur
}

var errs []error
for _, seatBid := range bidResp.SeatBid {
for i, bid := range seatBid.Bid {
bidType, err := getBidType(bid.MType)
if err != nil {
errs = append(errs, err)
continue
}

br.Bids = append(br.Bids, &adapters.TypedBid{
Bid: &seatBid.Bid[i],
BidType: bidType,
})
}
}
return br, errs
}

func getBidType(mtype openrtb2.MarkupType) (openrtb_ext.BidType, error) {
switch mtype {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consider this as a suggestion. The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to openrtb_ext.BidTypeAudio. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consider this as a suggestion. The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to openrtb_ext.BidTypeBanner. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consider this as a suggestion. The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to openrtb_ext.BidTypeVideo. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression.

case openrtb2.MarkupBanner:
return openrtb_ext.BidTypeBanner, nil
case openrtb2.MarkupVideo:
return openrtb_ext.BidTypeVideo, nil
case openrtb2.MarkupAudio:
return openrtb_ext.BidTypeAudio, nil
default:
return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("unknown bid type mtype=%d", mtype),
}
}
}
165 changes: 165 additions & 0 deletions adapters/agenticx/agenticx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package agenticx

import (
"strings"
"testing"

"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v4/adapters"
"github.com/prebid/prebid-server/v4/adapters/adapterstest"
"github.com/prebid/prebid-server/v4/config"
"github.com/prebid/prebid-server/v4/openrtb_ext"
"github.com/prebid/prebid-server/v4/util/jsonutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestJsonSamples(t *testing.T) {
bidder, buildErr := Builder(
openrtb_ext.BidderAgenticx,
config.Adapter{
Endpoint: "https://ads.theagenticx.ai/ads/rtb/prebid/server",
},
config.Server{
ExternalUrl: "http://hosturl.com",
GvlID: 0,
DataCenter: "2",
},
)

require.NoError(t, buildErr, "Builder returned unexpected error")
adapterstest.RunJSONBidderTest(t, "agenticxtest", bidder)
}

func TestParseImpExt(t *testing.T) {
tests := []struct {
name string
ext jsonutil.RawMessage
wantErr bool
}{
{"Valid ext", jsonutil.RawMessage(`{"bidder":{"bidfloor":0.5}}`), false},
{"Valid ext with sspId", jsonutil.RawMessage(`{"bidder":{"sspId":"ssp-123","siteId":"site-456"}}`), false},
{"Invalid JSON", jsonutil.RawMessage(`not-json`), true},
{"Not an object", jsonutil.RawMessage(`"string"`), true},
{"Bidder not object", jsonutil.RawMessage(`{"bidder":"not-an-object"}`), true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := parseImpExt(tt.ext)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}

func TestGetBidType(t *testing.T) {
tests := []struct {
name string
mtype openrtb2.MarkupType
wantErr bool
wantBidTy openrtb_ext.BidType
}{
{"Banner", openrtb2.MarkupBanner, false, openrtb_ext.BidTypeBanner},
{"Video", openrtb2.MarkupVideo, false, openrtb_ext.BidTypeVideo},
{"Audio", openrtb2.MarkupAudio, false, openrtb_ext.BidTypeAudio},
{"Unknown", 99, true, ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bidType, err := getBidType(tt.mtype)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantBidTy, bidType)
})
}
}

func TestMakeRequestsErrors(t *testing.T) {
a := &adapter{endpoint: "http://test-endpoint"}
tests := []struct {
name string
imps []openrtb2.Imp
wantErr string
}{
{"Invalid ext", []openrtb2.Imp{{ID: "1", Ext: jsonutil.RawMessage(`not-json`)}}, "impID 1:"},
{"No valid imps", []openrtb2.Imp{}, "no valid impressions"},
{"No banner or video or audio", []openrtb2.Imp{{ID: "1", Ext: jsonutil.RawMessage(`{"bidder":{"bidfloor": 0.5}}`)}}, "no banner, video, or audio object specified"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := &openrtb2.BidRequest{Imp: tt.imps}
_, errs := a.MakeRequests(req, nil)
require.NotEmpty(t, errs, "expected error, got none")
found := false
for _, err := range errs {
if err != nil && (tt.wantErr == "" || strings.Contains(err.Error(), tt.wantErr)) {
found = true
break
}
}
assert.True(t, found, "expected error containing %q, got %v", tt.wantErr, errs)
})
}
}

func TestMakeBidsErrors(t *testing.T) {
a := &adapter{endpoint: "http://test-endpoint"}
validReq := &openrtb2.BidRequest{ID: "1"}
validReqData := &adapters.RequestData{}
tests := []struct {
name string
respData *adapters.ResponseData
wantErr string
}{
{"Non-200/204 response", &adapters.ResponseData{StatusCode: 500, Body: []byte(`{}`)}, "Unexpected status code"},
{"Invalid JSON", &adapters.ResponseData{StatusCode: 200, Body: []byte(`not-json`)}, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, errs := a.MakeBids(validReq, validReqData, tt.respData)
require.NotEmpty(t, errs, "expected error, got none")
found := false
for _, err := range errs {
if err != nil && strings.Contains(err.Error(), tt.wantErr) {
found = true
break
}
}
assert.True(t, found, "expected error containing %q, got %v", tt.wantErr, errs)
})
}
}

func TestMakeBidsSkipsBadBidType(t *testing.T) {
a := &adapter{endpoint: "http://test-endpoint"}
validReq := &openrtb2.BidRequest{ID: "1"}
validReqData := &adapters.RequestData{}

respBody := `{
"id": "1",
"seatbid": [{
"bid": [
{"id": "good-bid", "impid": "1", "price": 1.0, "adm": "<div>ad</div>", "mtype": 1},
{"id": "bad-bid", "impid": "2", "price": 2.0, "adm": "<div>ad2</div>", "mtype": 99},
{"id": "good-bid-2", "impid": "3", "price": 3.0, "adm": "<div>ad3</div>", "mtype": 2}
]
}],
"cur": "USD"
}`

resp := &adapters.ResponseData{StatusCode: 200, Body: []byte(respBody)}
bidderResp, errs := a.MakeBids(validReq, validReqData, resp)

require.NotNil(t, bidderResp, "expected bid response, got nil")
assert.Len(t, bidderResp.Bids, 2, "expected 2 valid bids (bad bid should be skipped)")
assert.Len(t, errs, 1, "expected 1 error for the bad bid type")
assert.Contains(t, errs[0].Error(), "unknown bid type")
}
36 changes: 36 additions & 0 deletions adapters/agenticx/agenticxtest/exemplary/banner_bid_floor_set.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"mockBidRequest": {
"id": "test-bid-floor-set",
"imp": [
{
"id": "1",
"banner": { "w": 300, "h": 250 },
"ext": { "bidder": { "bidfloor": 1.23, "sspId": "ssp-123" } }
}
]
},
"httpCalls": [
{
"expectedRequest": {
"uri": "https://ads.theagenticx.ai/ads/rtb/prebid/server",
"body": {
"id": "test-bid-floor-set",
"imp": [
{
"id": "1",
"banner": { "w": 300, "h": 250 },
"bidfloor": 1.23,
"ext": { "bidder": { "bidfloor": 1.23, "sspId": "ssp-123" } }
}
]
},
"impIDs": ["1"]
},
"mockResponse": {
"status": 204,
"body": {}
}
}
],
"expectedBidResponses": []
}
Loading
Loading