diff --git a/adapters/revantage/params_test.go b/adapters/revantage/params_test.go new file mode 100644 index 00000000000..8a470823f5e --- /dev/null +++ b/adapters/revantage/params_test.go @@ -0,0 +1,55 @@ +package revantage + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v4/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas: %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderRevantage, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected revantage params that should be valid: %s\nError: %s", validParam, err) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas: %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderRevantage, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema accepted revantage params that should be invalid: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"feedId":"feed-abc"}`, + `{"feedId":"feed-abc","placementId":"plc-1"}`, + `{"feedId":"feed-abc","placementId":"plc-1","publisherId":"pub-1"}`, + `{"feedId":"x"}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `[]`, + `{}`, + `{"placementId":"plc-1"}`, + `{"feedId":""}`, + `{"feedId":123}`, + `{"feedId":null}`, + `{"feedId":"feed-abc","placementId":42}`, +} diff --git a/adapters/revantage/revantage.go b/adapters/revantage/revantage.go new file mode 100644 index 00000000000..4596032e9f5 --- /dev/null +++ b/adapters/revantage/revantage.go @@ -0,0 +1,251 @@ +package revantage + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "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" +) + +type adapter struct { + endpoint string +} + +// Builder builds a new instance of the Revantage adapter for the given bidder with the given config. +func Builder(_ openrtb_ext.BidderName, cfg config.Adapter, _ config.Server) (adapters.Bidder, error) { + return &adapter{endpoint: cfg.Endpoint}, nil +} + +// rewrittenImpExt is the shape the Revantage endpoint expects on imp.ext. +// It mirrors the public Prebid.js client adapter (revantageBidAdapter.js) so the +// upstream endpoint can be a single code path for both client- and server-side +// integrations. +type rewrittenImpExt struct { + FeedID string `json:"feedId"` + Bidder rewrittenImpBidder `json:"bidder"` +} + +type rewrittenImpBidder struct { + PlacementID string `json:"placementId,omitempty"` + PublisherID string `json:"publisherId,omitempty"` +} + +// MakeRequests converts an OpenRTB bid request into one or more HTTP calls to the Revantage endpoint. +// +// Impressions are grouped by feedId. Each group becomes a separate HTTP call so that a single +// auction can serve multiple feeds without conflict — the endpoint's `?feed=` query param must +// match the feedId used in every imp it carries. +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, _ *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + if request == nil || len(request.Imp) == 0 { + return nil, []error{&errortypes.BadInput{Message: "no impressions in bid request"}} + } + + // Preserve insertion order of feed groups for deterministic test output. + type group struct { + imps []openrtb2.Imp + } + groups := make(map[string]*group) + feedOrder := make([]string, 0) + var errs []error + + for i := range request.Imp { + imp := request.Imp[i] + feedID, err := rewriteImpExt(&imp) + if err != nil { + errs = append(errs, err) + continue + } + g, ok := groups[feedID] + if !ok { + g = &group{} + groups[feedID] = g + feedOrder = append(feedOrder, feedID) + } + g.imps = append(g.imps, imp) + } + + if len(groups) == 0 { + return nil, errs + } + + requests := make([]*adapters.RequestData, 0, len(groups)) + for _, feedID := range feedOrder { + imps := groups[feedID].imps + + // Shallow copy the request and replace imps with this feed's slice. + reqCopy := *request + reqCopy.Imp = imps + + body, err := json.Marshal(reqCopy) + if err != nil { + errs = append(errs, fmt.Errorf("failed to marshal request for feed %s: %w", feedID, err)) + continue + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + requests = append(requests, &adapters.RequestData{ + Method: http.MethodPost, + Uri: a.endpoint + "?feed=" + url.QueryEscape(feedID), + Body: body, + Headers: headers, + ImpIDs: collectImpIDs(imps), + }) + } + + return requests, errs +} + +// rewriteImpExt validates the bidder params on a single imp and rewrites imp.ext into the +// shape the Revantage endpoint expects. Returns the resolved feedId. +func rewriteImpExt(imp *openrtb2.Imp) (string, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("imp %s: invalid imp.ext: %s", imp.ID, err.Error()), + } + } + + var revantageExt openrtb_ext.ImpExtRevantage + if err := json.Unmarshal(bidderExt.Bidder, &revantageExt); err != nil { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("imp %s: invalid imp.ext.bidder: %s", imp.ID, err.Error()), + } + } + + feedID := strings.TrimSpace(revantageExt.FeedID) + if feedID == "" { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("imp %s: missing required param feedId", imp.ID), + } + } + + rewritten := rewrittenImpExt{ + FeedID: feedID, + Bidder: rewrittenImpBidder{ + PlacementID: revantageExt.PlacementID, + PublisherID: revantageExt.PublisherID, + }, + } + extBytes, err := json.Marshal(rewritten) + if err != nil { + return "", fmt.Errorf("imp %s: failed to marshal rewritten ext: %w", imp.ID, err) + } + imp.Ext = extBytes + + return feedID, nil +} + +func collectImpIDs(imps []openrtb2.Imp) []string { + ids := make([]string, len(imps)) + for i, imp := range imps { + ids[i] = imp.ID + } + return ids +} + +// MakeBids parses the upstream Revantage response into typed Prebid bids. +func (a *adapter) MakeBids(request *openrtb2.BidRequest, _ *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: "invalid bid response: " + err.Error(), + }} + } + + if len(bidResp.SeatBid) == 0 { + return nil, nil + } + + response := adapters.NewBidderResponse() + if bidResp.Cur != "" { + response.Currency = bidResp.Cur + } else { + response.Currency = "USD" + } + + var errs []error + for _, seat := range bidResp.SeatBid { + for i := range seat.Bid { + bid := &seat.Bid[i] + mt, err := resolveMediaType(bid, request.Imp) + if err != nil { + errs = append(errs, err) + continue + } + response.Bids = append(response.Bids, &adapters.TypedBid{ + Bid: bid, + BidType: mt, + Seat: openrtb_ext.BidderName(seat.Seat), + }) + } + } + return response, errs +} + +// resolveMediaType determines the bid's media type. Resolution order: +// 1. bid.mtype (oRTB 2.6) — REQUIRED on bids for multi-format impressions. +// 2. bid.ext.mediaType — legacy signal, accepted as a fallback. +// 3. The single, unambiguous media type on the originating imp (only when the +// imp declares exactly one of banner/video). +// +// Bids on multi-format imps that arrive without bid.mtype or bid.ext.mediaType +// are rejected with a BadServerResponse error. Per Prebid Server guidance, the +// adapter server MUST set MType on every bid; we do not guess. +func resolveMediaType(bid *openrtb2.Bid, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + } + + if len(bid.Ext) > 0 { + var ext struct { + MediaType string `json:"mediaType"` + } + if err := json.Unmarshal(bid.Ext, &ext); err == nil { + switch strings.ToLower(ext.MediaType) { + case "banner": + return openrtb_ext.BidTypeBanner, nil + case "video": + return openrtb_ext.BidTypeVideo, nil + } + } + } + + for _, imp := range imps { + if imp.ID != bid.ImpID { + continue + } + hasBanner := imp.Banner != nil + hasVideo := imp.Video != nil + switch { + case hasVideo && !hasBanner: + return openrtb_ext.BidTypeVideo, nil + case hasBanner && !hasVideo: + return openrtb_ext.BidTypeBanner, nil + } + break + } + + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("could not determine media type for bid %s on imp %s: response missing bid.mtype and bid.ext.mediaType", bid.ID, bid.ImpID), + } +} diff --git a/adapters/revantage/revantage_test.go b/adapters/revantage/revantage_test.go new file mode 100644 index 00000000000..99be3d388ab --- /dev/null +++ b/adapters/revantage/revantage_test.go @@ -0,0 +1,25 @@ +package revantage + +import ( + "testing" + + "github.com/prebid/prebid-server/v4/adapters/adapterstest" + "github.com/prebid/prebid-server/v4/config" + "github.com/prebid/prebid-server/v4/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderRevantage, config.Adapter{ + Endpoint: "https://bid.revantage.io/bid", + }, config.Server{ + ExternalUrl: "http://hosturl.com", + GvlID: 1, + DataCenter: "2", + }) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error: %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "revantagetest", bidder) +} diff --git a/adapters/revantage/revantagetest/exemplary/multi-feed-split.json b/adapters/revantage/revantagetest/exemplary/multi-feed-split.json new file mode 100644 index 00000000000..778098dc589 --- /dev/null +++ b/adapters/revantage/revantagetest/exemplary/multi-feed-split.json @@ -0,0 +1,112 @@ +{ + "mockBidRequest": { + "id": "multi-feed-request", + "imp": [ + { + "id": "imp-a", + "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]}, + "ext": {"bidder": {"feedId": "feed-one"}} + }, + { + "id": "imp-b", + "banner": {"w": 728, "h": 90, "format": [{"w": 728, "h": 90}]}, + "ext": {"bidder": {"feedId": "feed-two"}} + } + ], + "site": {"domain": "example.com"}, + "tmax": 1000, + "cur": ["USD"] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.revantage.io/bid?feed=feed-one", + "body": { + "id": "multi-feed-request", + "imp": [ + { + "id": "imp-a", + "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]}, + "ext": {"feedId": "feed-one", "bidder": {}} + } + ], + "site": {"domain": "example.com"}, + "tmax": 1000, + "cur": ["USD"] + }, + "impIDs": ["imp-a"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "multi-feed-request", + "cur": "USD", + "seatbid": [ + { + "seat": "dsp-1", + "bid": [ + {"id": "b1", "impid": "imp-a", "price": 1.0, "adm": "
1
", "crid": "c1", "w": 300, "h": 250, "mtype": 1} + ] + } + ] + } + } + }, + { + "expectedRequest": { + "uri": "https://bid.revantage.io/bid?feed=feed-two", + "body": { + "id": "multi-feed-request", + "imp": [ + { + "id": "imp-b", + "banner": {"w": 728, "h": 90, "format": [{"w": 728, "h": 90}]}, + "ext": {"feedId": "feed-two", "bidder": {}} + } + ], + "site": {"domain": "example.com"}, + "tmax": 1000, + "cur": ["USD"] + }, + "impIDs": ["imp-b"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "multi-feed-request", + "cur": "USD", + "seatbid": [ + { + "seat": "dsp-2", + "bid": [ + {"id": "b2", "impid": "imp-b", "price": 2.0, "adm": "
2
", "crid": "c2", "w": 728, "h": 90, "mtype": 1} + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": {"id": "b1", "impid": "imp-a", "price": 1.0, "adm": "
1
", "crid": "c1", "w": 300, "h": 250, "mtype": 1}, + "type": "banner", + "seat": "dsp-1" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": {"id": "b2", "impid": "imp-b", "price": 2.0, "adm": "
2
", "crid": "c2", "w": 728, "h": 90, "mtype": 1}, + "type": "banner", + "seat": "dsp-2" + } + ] + } + ] +} diff --git a/adapters/revantage/revantagetest/exemplary/multi-imp-same-feed.json b/adapters/revantage/revantagetest/exemplary/multi-imp-same-feed.json new file mode 100644 index 00000000000..ad7db608a82 --- /dev/null +++ b/adapters/revantage/revantagetest/exemplary/multi-imp-same-feed.json @@ -0,0 +1,95 @@ +{ + "mockBidRequest": { + "id": "multi-imp-request", + "imp": [ + { + "id": "imp-1", + "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]}, + "ext": {"bidder": {"feedId": "feed-shared"}} + }, + { + "id": "imp-2", + "banner": {"w": 728, "h": 90, "format": [{"w": 728, "h": 90}]}, + "ext": {"bidder": {"feedId": "feed-shared"}} + } + ], + "site": {"domain": "example.com"}, + "tmax": 1000, + "cur": ["USD"] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.revantage.io/bid?feed=feed-shared", + "body": { + "id": "multi-imp-request", + "imp": [ + { + "id": "imp-1", + "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]}, + "ext": {"feedId": "feed-shared", "bidder": {}} + }, + { + "id": "imp-2", + "banner": {"w": 728, "h": 90, "format": [{"w": 728, "h": 90}]}, + "ext": {"feedId": "feed-shared", "bidder": {}} + } + ], + "site": {"domain": "example.com"}, + "tmax": 1000, + "cur": ["USD"] + }, + "impIDs": ["imp-1", "imp-2"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "multi-imp-request", + "cur": "USD", + "seatbid": [ + { + "seat": "dsp-a", + "bid": [ + { + "id": "b1", "impid": "imp-1", "price": 1.10, + "adm": "
A
", "crid": "c1", "w": 300, "h": 250, + "adomain": ["a.com"], "mtype": 1 + }, + { + "id": "b2", "impid": "imp-2", "price": 0.80, + "adm": "
B
", "crid": "c2", "w": 728, "h": 90, + "adomain": ["b.com"], "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "b1", "impid": "imp-1", "price": 1.10, + "adm": "
A
", "crid": "c1", "w": 300, "h": 250, + "adomain": ["a.com"], "mtype": 1 + }, + "type": "banner", + "seat": "dsp-a" + }, + { + "bid": { + "id": "b2", "impid": "imp-2", "price": 0.80, + "adm": "
B
", "crid": "c2", "w": 728, "h": 90, + "adomain": ["b.com"], "mtype": 1 + }, + "type": "banner", + "seat": "dsp-a" + } + ] + } + ] +} diff --git a/adapters/revantage/revantagetest/exemplary/simple-banner.json b/adapters/revantage/revantagetest/exemplary/simple-banner.json new file mode 100644 index 00000000000..4afc3a4891d --- /dev/null +++ b/adapters/revantage/revantagetest/exemplary/simple-banner.json @@ -0,0 +1,149 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "imp-1", + "tagid": "ad-unit-1", + "banner": { + "w": 300, + "h": 250, + "format": [ + {"w": 300, "h": 250}, + {"w": 300, "h": 600} + ] + }, + "bidfloor": 0.5, + "bidfloorcur": "USD", + "ext": { + "bidder": { + "feedId": "feed-abc", + "placementId": "plc-1", + "publisherId": "pub-1" + } + } + } + ], + "site": { + "domain": "example.com", + "page": "https://example.com/article" + }, + "device": { + "ua": "Mozilla/5.0", + "language": "en" + }, + "user": { + "ext": { + "consent": "BOJ/P2HOJ/P2HABABMAAAAAZ+A==" + } + }, + "regs": { + "ext": { + "gdpr": 1 + } + }, + "tmax": 1000, + "cur": ["USD"] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.revantage.io/bid?feed=feed-abc", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "imp-1", + "tagid": "ad-unit-1", + "banner": { + "w": 300, + "h": 250, + "format": [ + {"w": 300, "h": 250}, + {"w": 300, "h": 600} + ] + }, + "bidfloor": 0.5, + "bidfloorcur": "USD", + "ext": { + "feedId": "feed-abc", + "bidder": { + "placementId": "plc-1", + "publisherId": "pub-1" + } + } + } + ], + "site": { + "domain": "example.com", + "page": "https://example.com/article" + }, + "device": { + "ua": "Mozilla/5.0", + "language": "en" + }, + "user": { + "ext": { + "consent": "BOJ/P2HOJ/P2HABABMAAAAAZ+A==" + } + }, + "regs": { + "ext": { + "gdpr": 1 + } + }, + "tmax": 1000, + "cur": ["USD"] + }, + "impIDs": ["imp-1"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "test-dsp", + "bid": [ + { + "id": "bid-1", + "impid": "imp-1", + "price": 1.25, + "adm": "
Banner Ad
", + "crid": "creative-123", + "w": 300, + "h": 250, + "adomain": ["advertiser.com"], + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "bid-1", + "impid": "imp-1", + "price": 1.25, + "adm": "
Banner Ad
", + "crid": "creative-123", + "w": 300, + "h": 250, + "adomain": ["advertiser.com"], + "mtype": 1 + }, + "type": "banner", + "seat": "test-dsp" + } + ] + } + ] +} diff --git a/adapters/revantage/revantagetest/exemplary/simple-video.json b/adapters/revantage/revantagetest/exemplary/simple-video.json new file mode 100644 index 00000000000..c209fa11075 --- /dev/null +++ b/adapters/revantage/revantagetest/exemplary/simple-video.json @@ -0,0 +1,118 @@ +{ + "mockBidRequest": { + "id": "test-request-video", + "imp": [ + { + "id": "video-imp-1", + "tagid": "video-unit", + "video": { + "mimes": ["video/mp4", "video/webm"], + "w": 640, + "h": 480, + "minduration": 5, + "maxduration": 30, + "protocols": [2, 3, 5, 6], + "api": [1, 2], + "placement": 1, + "linearity": 1 + }, + "ext": { + "bidder": { + "feedId": "feed-video" + } + } + } + ], + "site": { + "domain": "example.com", + "page": "https://example.com/watch" + }, + "tmax": 1500, + "cur": ["USD"] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.revantage.io/bid?feed=feed-video", + "body": { + "id": "test-request-video", + "imp": [ + { + "id": "video-imp-1", + "tagid": "video-unit", + "video": { + "mimes": ["video/mp4", "video/webm"], + "w": 640, + "h": 480, + "minduration": 5, + "maxduration": 30, + "protocols": [2, 3, 5, 6], + "api": [1, 2], + "placement": 1, + "linearity": 1 + }, + "ext": { + "feedId": "feed-video", + "bidder": {} + } + } + ], + "site": { + "domain": "example.com", + "page": "https://example.com/watch" + }, + "tmax": 1500, + "cur": ["USD"] + }, + "impIDs": ["video-imp-1"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-video", + "cur": "USD", + "seatbid": [ + { + "seat": "video-dsp", + "bid": [ + { + "id": "video-bid-1", + "impid": "video-imp-1", + "price": 4.50, + "adm": "", + "crid": "video-creative-1", + "w": 640, + "h": 480, + "adomain": ["video-advertiser.com"], + "mtype": 2 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "video-bid-1", + "impid": "video-imp-1", + "price": 4.50, + "adm": "", + "crid": "video-creative-1", + "w": 640, + "h": 480, + "adomain": ["video-advertiser.com"], + "mtype": 2 + }, + "type": "video", + "seat": "video-dsp" + } + ] + } + ] +} diff --git a/adapters/revantage/revantagetest/supplemental/204-response.json b/adapters/revantage/revantagetest/supplemental/204-response.json new file mode 100644 index 00000000000..a603a4ca484 --- /dev/null +++ b/adapters/revantage/revantagetest/supplemental/204-response.json @@ -0,0 +1,41 @@ +{ + "mockBidRequest": { + "id": "no-content-request", + "imp": [ + { + "id": "imp-1", + "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]}, + "ext": {"bidder": {"feedId": "feed-abc"}} + } + ], + "site": {"domain": "example.com"}, + "tmax": 1000, + "cur": ["USD"] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.revantage.io/bid?feed=feed-abc", + "body": { + "id": "no-content-request", + "imp": [ + { + "id": "imp-1", + "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]}, + "ext": {"feedId": "feed-abc", "bidder": {}} + } + ], + "site": {"domain": "example.com"}, + "tmax": 1000, + "cur": ["USD"] + }, + "impIDs": ["imp-1"] + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/revantage/revantagetest/supplemental/400-response.json b/adapters/revantage/revantagetest/supplemental/400-response.json new file mode 100644 index 00000000000..17846b71972 --- /dev/null +++ b/adapters/revantage/revantagetest/supplemental/400-response.json @@ -0,0 +1,46 @@ +{ + "mockBidRequest": { + "id": "bad-request", + "imp": [ + { + "id": "imp-1", + "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]}, + "ext": {"bidder": {"feedId": "feed-abc"}} + } + ], + "site": {"domain": "example.com"}, + "tmax": 1000, + "cur": ["USD"] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.revantage.io/bid?feed=feed-abc", + "body": { + "id": "bad-request", + "imp": [ + { + "id": "imp-1", + "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]}, + "ext": {"feedId": "feed-abc", "bidder": {}} + } + ], + "site": {"domain": "example.com"}, + "tmax": 1000, + "cur": ["USD"] + }, + "impIDs": ["imp-1"] + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/revantage/revantagetest/supplemental/missing-feedid.json b/adapters/revantage/revantagetest/supplemental/missing-feedid.json new file mode 100644 index 00000000000..9a4693c899b --- /dev/null +++ b/adapters/revantage/revantagetest/supplemental/missing-feedid.json @@ -0,0 +1,22 @@ +{ + "mockBidRequest": { + "id": "missing-feed", + "imp": [ + { + "id": "imp-x", + "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]}, + "ext": {"bidder": {}} + } + ], + "site": {"domain": "example.com"}, + "tmax": 1000, + "cur": ["USD"] + }, + "httpCalls": [], + "expectedMakeRequestsErrors": [ + { + "value": "imp imp-x: missing required param feedId", + "comparison": "literal" + } + ] +} diff --git a/adapters/revantage/revantagetest/supplemental/multi-format-missing-mtype.json b/adapters/revantage/revantagetest/supplemental/multi-format-missing-mtype.json new file mode 100644 index 00000000000..2f3e699fcf3 --- /dev/null +++ b/adapters/revantage/revantagetest/supplemental/multi-format-missing-mtype.json @@ -0,0 +1,73 @@ +{ + "mockBidRequest": { + "id": "multi-format-request", + "imp": [ + { + "id": "imp-mf", + "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]}, + "video": {"mimes": ["video/mp4"], "w": 640, "h": 480}, + "ext": {"bidder": {"feedId": "feed-abc"}} + } + ], + "site": {"domain": "example.com"}, + "tmax": 1000, + "cur": ["USD"] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.revantage.io/bid?feed=feed-abc", + "body": { + "id": "multi-format-request", + "imp": [ + { + "id": "imp-mf", + "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]}, + "video": {"mimes": ["video/mp4"], "w": 640, "h": 480}, + "ext": {"feedId": "feed-abc", "bidder": {}} + } + ], + "site": {"domain": "example.com"}, + "tmax": 1000, + "cur": ["USD"] + }, + "impIDs": ["imp-mf"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "multi-format-request", + "cur": "USD", + "seatbid": [ + { + "seat": "dsp", + "bid": [ + { + "id": "b1", + "impid": "imp-mf", + "price": 1.0, + "adm": "
ambiguous
", + "crid": "c1", + "w": 300, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [] + } + ], + "expectedMakeBidsErrors": [ + { + "value": "could not determine media type for bid b1 on imp imp-mf: response missing bid.mtype and bid.ext.mediaType", + "comparison": "literal" + } + ] +} diff --git a/analytics/build/testFiles/test-20260428 b/analytics/build/testFiles/test-20260428 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 630fbe348b9..7fd4add7731 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -194,6 +194,7 @@ import ( "github.com/prebid/prebid-server/v4/adapters/rediads" "github.com/prebid/prebid-server/v4/adapters/relevantdigital" "github.com/prebid/prebid-server/v4/adapters/resetdigital" + "github.com/prebid/prebid-server/v4/adapters/revantage" "github.com/prebid/prebid-server/v4/adapters/revcontent" "github.com/prebid/prebid-server/v4/adapters/richaudience" "github.com/prebid/prebid-server/v4/adapters/rise" @@ -464,6 +465,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderRediads: rediads.Builder, openrtb_ext.BidderRelevantDigital: relevantdigital.Builder, openrtb_ext.BidderResetDigital: resetdigital.Builder, + openrtb_ext.BidderRevantage: revantage.Builder, openrtb_ext.BidderRevcontent: revcontent.Builder, openrtb_ext.BidderRichaudience: richaudience.Builder, openrtb_ext.BidderRise: rise.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 1b61ff724e2..abbdccc4846 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -212,6 +212,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderRediads, BidderRelevantDigital, BidderResetDigital, + BidderRevantage, BidderRevcontent, BidderRichaudience, BidderRise, @@ -586,6 +587,7 @@ const ( BidderRediads BidderName = "rediads" BidderRelevantDigital BidderName = "relevantdigital" BidderResetDigital BidderName = "resetdigital" + BidderRevantage BidderName = "revantage" BidderRevcontent BidderName = "revcontent" BidderRichaudience BidderName = "richaudience" BidderRise BidderName = "rise" diff --git a/openrtb_ext/imp_revantage.go b/openrtb_ext/imp_revantage.go new file mode 100644 index 00000000000..dd4fee2ef4f --- /dev/null +++ b/openrtb_ext/imp_revantage.go @@ -0,0 +1,12 @@ +package openrtb_ext + +// ImpExtRevantage defines the contract for the bidder-specific portion of +// imp.ext when targeting the Revantage adapter. +// +// feedId is required and identifies the publisher feed the impression +// belongs to. placementId and publisherId are optional pass-through values. +type ImpExtRevantage struct { + FeedID string `json:"feedId"` + PlacementID string `json:"placementId,omitempty"` + PublisherID string `json:"publisherId,omitempty"` +} diff --git a/static/bidder-info/revantage.yaml b/static/bidder-info/revantage.yaml new file mode 100644 index 00000000000..f10fcb323e1 --- /dev/null +++ b/static/bidder-info/revantage.yaml @@ -0,0 +1,17 @@ +endpoint: "https://bid.revantage.io/bid" +maintainer: + email: "prebid@revantage.io" +gvlVendorID: 0 +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video +userSync: + redirect: + url: "https://sync.revantage.io/pbs/usersync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&r={{.RedirectURL}}" + userMacro: "$UID" \ No newline at end of file diff --git a/static/bidder-params/revantage.json b/static/bidder-params/revantage.json new file mode 100644 index 00000000000..d693afa5714 --- /dev/null +++ b/static/bidder-params/revantage.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Revantage Adapter Params", + "description": "A schema which validates params accepted by the Revantage adapter", + "type": "object", + "properties": { + "feedId": { + "type": "string", + "description": "Revantage feed identifier (required)", + "minLength": 1 + }, + "placementId": { + "type": "string", + "description": "Optional placement identifier" + }, + "publisherId": { + "type": "string", + "description": "Optional publisher identifier" + } + }, + "required": ["feedId"] +}