-
Notifications
You must be signed in to change notification settings - Fork 937
New Adapter: Aniview #4852
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?
New Adapter: Aniview #4852
Changes from 2 commits
2a3a4c9
068e5f0
6674c18
4b88248
2e86eaf
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,224 @@ | ||
| package aniview | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "fmt" | ||
| "net/http" | ||
| "strings" | ||
|
|
||
| "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 | ||
| } | ||
|
|
||
| type aniviewExt struct { | ||
| PBS int `json:"pbs"` | ||
| } | ||
|
|
||
| // Builder builds a new instance of the Aniview adapter for the given bidder with the given config. | ||
| func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { | ||
| bidder := &adapter{ | ||
| endpoint: config.Endpoint, | ||
| } | ||
| return bidder, nil | ||
| } | ||
|
|
||
| func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { | ||
| var requests []*adapters.RequestData | ||
| var errors []error | ||
|
|
||
| headers := http.Header{} | ||
| headers.Add("Content-Type", "application/json;charset=utf-8") | ||
| headers.Add("Accept", "application/json") | ||
|
|
||
| requestExt, err := buildRequestExt(request.Ext) | ||
| if err != nil { | ||
| return nil, []error{err} | ||
| } | ||
|
|
||
| // One outgoing request per imp and per media type, mirroring the Prebid.js adapter. | ||
| for _, imp := range request.Imp { | ||
| impExt, err := extractImpExt(&imp) | ||
| if err != nil { | ||
| errors = append(errors, err) | ||
| continue | ||
| } | ||
|
|
||
| imp.TagID = strings.TrimSpace(impExt.ChannelId) | ||
|
|
||
| for _, singleTypeImp := range splitImpByMediaType(imp) { | ||
| requestCopy := *request | ||
| requestCopy.Imp = []openrtb2.Imp{singleTypeImp} | ||
| requestCopy.Ext = requestExt | ||
|
|
||
| requestJSON, err := jsonutil.Marshal(&requestCopy) | ||
| if err != nil { | ||
| errors = append(errors, fmt.Errorf("marshal bidRequest: %w", err)) | ||
| continue | ||
| } | ||
|
|
||
| requests = append(requests, &adapters.RequestData{ | ||
| Method: "POST", | ||
| Uri: a.endpoint, | ||
| Body: requestJSON, | ||
| Headers: headers, | ||
| ImpIDs: []string{singleTypeImp.ID}, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| return requests, errors | ||
| } | ||
|
|
||
| // buildRequestExt merges ext.aniview into the existing request.ext, preserving | ||
| // whatever PBS core has put there. | ||
| func buildRequestExt(requestExt []byte) ([]byte, error) { | ||
| extMap := map[string]interface{}{} | ||
| if len(requestExt) > 0 { | ||
| if err := jsonutil.Unmarshal(requestExt, &extMap); err != nil { | ||
| return nil, fmt.Errorf("unmarshal request.ext: %w", err) | ||
| } | ||
| } | ||
|
|
||
| extMap["aniview"] = aniviewExt{PBS: 1} | ||
|
|
||
| ext, err := jsonutil.Marshal(extMap) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("marshal request.ext: %w", err) | ||
| } | ||
| return ext, nil | ||
| } | ||
|
|
||
| // splitImpByMediaType returns one imp per media type, so each outgoing request | ||
| // carries a single media type, as the Aniview endpoint expects. | ||
| func splitImpByMediaType(imp openrtb2.Imp) []openrtb2.Imp { | ||
| if imp.Video == nil || imp.Banner == nil { | ||
| return []openrtb2.Imp{imp} | ||
| } | ||
|
|
||
| videoImp := imp | ||
| videoImp.Banner = nil | ||
| bannerImp := imp | ||
| bannerImp.Video = nil | ||
| return []openrtb2.Imp{videoImp, bannerImp} | ||
| } | ||
|
|
||
| func extractImpExt(imp *openrtb2.Imp) (*openrtb_ext.ImpExtAniview, error) { | ||
| var bidderExt adapters.ExtImpBidder | ||
| if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil { | ||
| return nil, fmt.Errorf("unmarshal bidderExt: %w", err) | ||
| } | ||
|
|
||
| var impExt openrtb_ext.ImpExtAniview | ||
| if err := jsonutil.Unmarshal(bidderExt.Bidder, &impExt); err != nil { | ||
| return nil, fmt.Errorf("unmarshal ImpExtAniview: %w", err) | ||
| } | ||
|
|
||
| if strings.TrimSpace(impExt.ChannelId) == "" { | ||
| return nil, &errortypes.BadInput{ | ||
| Message: fmt.Sprintf("Missing AV_CHANNELID for imp: %s", imp.ID), | ||
| } | ||
| } | ||
|
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. adapters.CheckResponseStatusCodeForErrors already returns the right typed error — BadInput for 400 and BadServerResponse for everything else (500, 503, etc.). The current code discards that typed error and always wraps a new errortypes.BadInput, so a 500 response from the exchange is misclassified as a buyer error. Please just propagate the helper's return value:
Author
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. Done |
||
| return &impExt, nil | ||
| } | ||
|
|
||
| func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { | ||
| var errs []error | ||
|
|
||
| if adapters.IsResponseStatusCodeNoContent(responseData) { | ||
| return nil, nil | ||
| } | ||
|
|
||
| if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { | ||
| return nil, []error{&errortypes.BadInput{ | ||
| Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", responseData.StatusCode), | ||
| }} | ||
| } | ||
|
|
||
| // The exchange may answer a no-bid as 200 with an empty/whitespace body. | ||
| if len(bytes.TrimSpace(responseData.Body)) == 0 { | ||
| return nil, nil | ||
| } | ||
|
|
||
| var response openrtb2.BidResponse | ||
| if err := jsonutil.Unmarshal(responseData.Body, &response); err != nil { | ||
| return nil, []error{&errortypes.BadServerResponse{ | ||
| Message: fmt.Sprintf("bad server response: %s", err), | ||
| }} | ||
| } | ||
|
|
||
| // The outgoing request is single media type per imp — use it for media type | ||
| // inference, not the original request (a multi-format imp carries both types there). | ||
| var sentRequest openrtb2.BidRequest | ||
| if err := jsonutil.Unmarshal(requestData.Body, &sentRequest); err != nil { | ||
| return nil, []error{fmt.Errorf("unmarshal outgoing request: %w", err)} | ||
| } | ||
|
|
||
| bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(response.SeatBid)) | ||
|
|
||
| if response.Cur != "" { | ||
| bidResponse.Currency = response.Cur | ||
| } | ||
|
|
||
| for _, seatBid := range response.SeatBid { | ||
| for i, bid := range seatBid.Bid { | ||
| // Mirror the Prebid.js adapter: a bid without markup or a VAST url is unusable. | ||
| if bid.AdM == "" && bid.NURL == "" { | ||
| continue | ||
| } | ||
|
|
||
| bidType, err := getMediaTypeForBid(&bid, &sentRequest) | ||
| if err != nil { | ||
| errs = append(errs, err) | ||
| continue | ||
| } | ||
|
|
||
| bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ | ||
| Bid: &seatBid.Bid[i], | ||
| BidType: bidType, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| return bidResponse, errs | ||
| } | ||
|
|
||
| // getMediaTypeForBid resolves the bid media type: bid.mtype when provided, | ||
| // otherwise the media type of the matching imp (requests are single media type), | ||
| // otherwise VAST markup detection. | ||
| func getMediaTypeForBid(bid *openrtb2.Bid, request *openrtb2.BidRequest) (openrtb_ext.BidType, error) { | ||
| switch bid.MType { | ||
| case openrtb2.MarkupBanner: | ||
| return openrtb_ext.BidTypeBanner, nil | ||
| case openrtb2.MarkupVideo: | ||
| return openrtb_ext.BidTypeVideo, nil | ||
| } | ||
|
|
||
| for _, imp := range request.Imp { | ||
| if imp.ID == bid.ImpID { | ||
| if imp.Video != nil { | ||
|
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, nil. 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. |
||
| return openrtb_ext.BidTypeVideo, nil | ||
| } | ||
| if imp.Banner != nil { | ||
|
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, nil. 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. |
||
| return openrtb_ext.BidTypeBanner, nil | ||
| } | ||
| break | ||
| } | ||
| } | ||
|
|
||
| adm := strings.TrimSpace(strings.ToLower(bid.AdM)) | ||
| if strings.HasPrefix(adm, "<vast") || strings.HasPrefix(adm, "<?xml") { | ||
| return openrtb_ext.BidTypeVideo, nil | ||
| } | ||
|
|
||
| return "", &errortypes.BadServerResponse{ | ||
| Message: fmt.Sprintf("Could not define bid type for imp: %s", bid.ImpID), | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package aniview | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "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" | ||
| ) | ||
|
|
||
| func TestBuildRequestExtInvalid(t *testing.T) { | ||
| if _, err := buildRequestExt([]byte(`"not-an-object"`)); err == nil { | ||
| t.Error("expected error for non-object request.ext") | ||
| } | ||
| ext, err := buildRequestExt([]byte(`{"prebid":{"debug":true}}`)) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if string(ext) == "" { | ||
| t.Error("expected merged ext") | ||
| } | ||
| } | ||
|
|
||
| func TestMakeBidsEmptyBody(t *testing.T) { | ||
| bidder, _ := Builder(openrtb_ext.BidderAniview, config.Adapter{Endpoint: "https://rtb.aniview.com/sspRTB2"}, config.Server{}) | ||
| for _, body := range []string{"", "\n", " \n"} { | ||
| resp, errs := bidder.(*adapter).MakeBids(nil, &adapters.RequestData{Body: []byte("{}")}, &adapters.ResponseData{StatusCode: 200, Body: []byte(body)}) | ||
| if resp != nil || errs != nil { | ||
| t.Errorf("empty body %q should be a silent no-bid, got resp=%v errs=%v", body, resp, errs) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestJsonSamples(t *testing.T) { | ||
| bidder, buildErr := Builder(openrtb_ext.BidderAniview, config.Adapter{ | ||
| Endpoint: "https://rtb.aniview.com/sspRTB2", | ||
| }, | ||
| config.Server{ | ||
| ExternalUrl: "http://hosturl.com", GvlID: 780, DataCenter: "2", | ||
| }) | ||
|
|
||
| if buildErr != nil { | ||
| t.Fatalf("Builder returned unexpected error %v", buildErr) | ||
| } | ||
|
|
||
| adapterstest.RunJSONBidderTest(t, "aniviewtest", bidder) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| { | ||
| "mockBidRequest": { | ||
| "id": "banner-request-id", | ||
| "site": { | ||
| "page": "https://publisher.com/page" | ||
| }, | ||
| "imp": [ | ||
| { | ||
| "id": "banner-imp-id", | ||
| "banner": { | ||
| "w": 300, | ||
| "h": 250, | ||
| "format": [{"w": 300, "h": 250}] | ||
| }, | ||
| "ext": { | ||
| "bidder": { | ||
| "AV_PUBLISHERID": "1234567890abcdef12345678", | ||
| "AV_CHANNELID": "abcdef1234567890abcdef12" | ||
| } | ||
| } | ||
| } | ||
| ] | ||
| }, | ||
| "httpCalls": [ | ||
| { | ||
| "expectedRequest": { | ||
| "uri": "https://rtb.aniview.com/sspRTB2", | ||
| "body": { | ||
| "id": "banner-request-id", | ||
| "site": { | ||
| "page": "https://publisher.com/page" | ||
| }, | ||
| "imp": [ | ||
| { | ||
| "id": "banner-imp-id", | ||
| "tagid": "abcdef1234567890abcdef12", | ||
| "banner": { | ||
| "w": 300, | ||
| "h": 250, | ||
| "format": [{"w": 300, "h": 250}] | ||
| }, | ||
| "ext": { | ||
| "bidder": { | ||
| "AV_PUBLISHERID": "1234567890abcdef12345678", | ||
| "AV_CHANNELID": "abcdef1234567890abcdef12" | ||
| } | ||
| } | ||
| } | ||
| ], | ||
| "ext": { | ||
| "aniview": { | ||
| "pbs": 1 | ||
| } | ||
| } | ||
| }, | ||
| "impIDs": ["banner-imp-id"] | ||
| }, | ||
| "mockResponse": { | ||
| "status": 200, | ||
| "body": { | ||
| "id": "banner-request-id", | ||
| "seatbid": [ | ||
| { | ||
| "seat": "1234567890abcdef12345678", | ||
| "bid": [ | ||
| { | ||
| "id": "banner-bid-id", | ||
| "impid": "banner-imp-id", | ||
| "price": 1.2, | ||
| "adm": "<div id=\"aniview-ad\"><script src=\"https://cdn.aniview.com/tag.js\"></script></div>", | ||
| "crid": "banner-creative-id", | ||
| "adomain": ["advertiser.com"], | ||
| "w": 300, | ||
| "h": 250 | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| ], | ||
| "expectedBidResponses": [ | ||
| { | ||
| "currency": "USD", | ||
| "bids": [ | ||
| { | ||
| "bid": { | ||
| "id": "banner-bid-id", | ||
| "impid": "banner-imp-id", | ||
| "price": 1.2, | ||
| "adm": "<div id=\"aniview-ad\"><script src=\"https://cdn.aniview.com/tag.js\"></script></div>", | ||
| "crid": "banner-creative-id", | ||
| "adomain": ["advertiser.com"], | ||
| "w": 300, | ||
| "h": 250 | ||
| }, | ||
| "type": "banner" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "expectedMakeBidsErrors": [] | ||
| } |
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.
http.Headeris amap[string][]string— a reference type. Creating it once outside the loop and assigning it to everyRequestData.Headersmeans all outgoing requests share the same map. If PBS or any middleware mutates one request's headers (e.g. adds a correlation ID), it silently modifies every other request in the batch.Please create a fresh
http.Header{}perRequestData, inside the inner loop: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.
Done