-
Notifications
You must be signed in to change notification settings - Fork 930
Agenticx New Bidder Adapter #4864
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
base: master
Are you sure you want to change the base?
Changes from all commits
805e7ab
5cfb1d2
cfad8a6
80c5088
403fe1c
cc36c00
de04df7
17def18
9085157
93fd1af
d3477f9
94a65d2
001ca1f
034f41a
cf1c599
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 { | ||
| setTestMode = true | ||
| } | ||
|
|
||
| validImps = append(validImps, imp) | ||
| } | ||
|
|
||
| if len(validImps) == 0 { | ||
| return nil, append(errs, fmt.Errorf("no valid impressions")) | ||
| } | ||
|
|
||
| request.Imp = validImps | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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), | ||
| } | ||
| } | ||
| } | ||
| 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") | ||
| } |
| 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": [] | ||
| } |
There was a problem hiding this comment.
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).