Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 55 additions & 0 deletions adapters/revantage/params_test.go
Original file line number Diff line number Diff line change
@@ -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}`,
}
251 changes: 251 additions & 0 deletions adapters/revantage/revantage.go
Original file line number Diff line number Diff line change
@@ -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) {
Comment thread
v0idxyz marked this conversation as resolved.
Comment thread
v0idxyz marked this conversation as resolved.
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),
}
}
25 changes: 25 additions & 0 deletions adapters/revantage/revantage_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading