diff --git a/adapters/scalibur/params_test.go b/adapters/scalibur/params_test.go new file mode 100644 index 00000000000..e03d549f366 --- /dev/null +++ b/adapters/scalibur/params_test.go @@ -0,0 +1,51 @@ +package scalibur + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v3/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.BidderScalibur, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected scalibur params: %s", validParam) + } + } +} + +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.BidderScalibur, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"placementId":"p123"}`, + `{"placementId":"p123", "bidfloor": 1.5}`, + `{"placementId":"p123", "bidfloor": 1.5, "bidfloorcur": "USD"}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `[]`, + `{}`, + `{"placementId": 123}`, + `{"bidfloor": 1.5}`, +} diff --git a/adapters/scalibur/scalibur.go b/adapters/scalibur/scalibur.go new file mode 100644 index 00000000000..291807462ee --- /dev/null +++ b/adapters/scalibur/scalibur.go @@ -0,0 +1,330 @@ +package scalibur + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v20/adcom1" + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v3/adapters" + "github.com/prebid/prebid-server/v3/config" + "github.com/prebid/prebid-server/v3/errortypes" + "github.com/prebid/prebid-server/v3/openrtb_ext" + "github.com/prebid/prebid-server/v3/util/jsonutil" +) + +type adapter struct { + endpoint *template.Template +} + +// Builder builds a new instance of the Scalibur adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + temp, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + return &adapter{ + endpoint: temp, + }, nil +} + +// MakeRequests creates the HTTP requests which should be made to fetch bids from Scalibur. +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + var validImps []openrtb2.Imp + + // Process each impression + for _, imp := range request.Imp { + scaliburExt, err := parseScaliburExt(imp.Ext) + if err != nil { + errs = append(errs, err) + continue + } + + impCopy := imp + + // Apply bid floor and currency + if scaliburExt.BidFloor != nil && *scaliburExt.BidFloor > 0 { + impCopy.BidFloor = *scaliburExt.BidFloor + if scaliburExt.BidFloorCur != "" { + impCopy.BidFloorCur = scaliburExt.BidFloorCur + } + } + + if impCopy.BidFloor > 0 && impCopy.BidFloorCur != "" && impCopy.BidFloorCur != "USD" { + convertedValue, err := reqInfo.ConvertCurrency(impCopy.BidFloor, impCopy.BidFloorCur, "USD") + if err != nil { + errs = append(errs, err) + continue + } + impCopy.BidFloor = convertedValue + impCopy.BidFloorCur = "USD" + } + + if impCopy.BidFloorCur == "" { + impCopy.BidFloorCur = "USD" + } + + // Prepare imp.ext with placementId and params + impExtData := make(map[string]interface{}) + impExtData["placementId"] = scaliburExt.PlacementID + + if impCopy.BidFloor > 0 { + impExtData["bidfloor"] = impCopy.BidFloor + } + impExtData["bidfloorcur"] = impCopy.BidFloorCur + + // Preserve GPID if present + var rawExt map[string]json.RawMessage + if err := jsonutil.Unmarshal(imp.Ext, &rawExt); err == nil { + if gpid, ok := rawExt["gpid"]; ok { + impExtData["gpid"] = json.RawMessage(gpid) + } + } + + impExt, err := jsonutil.Marshal(impExtData) + if err != nil { + errs = append(errs, err) + continue + } + impCopy.Ext = impExt + + // Apply video defaults (matching JS defaults) + if impCopy.Video != nil { + videoCopy := *impCopy.Video + + // Note: In openrtb v20, field names are capitalized (MIMEs not Mimes) + if len(videoCopy.MIMEs) == 0 { + videoCopy.MIMEs = []string{"video/mp4"} + } + if videoCopy.MinDuration == 0 { + videoCopy.MinDuration = 1 + } + if videoCopy.MaxDuration == 0 { + videoCopy.MaxDuration = 180 + } + if videoCopy.MaxBitRate == 0 { + videoCopy.MaxBitRate = 30000 + } + if len(videoCopy.Protocols) == 0 { + // Use adcom1.MediaCreativeSubtype for protocols in v20 + videoCopy.Protocols = []adcom1.MediaCreativeSubtype{2, 3, 5, 6} + } + // Note: In openrtb v20, W and H are pointers + if videoCopy.W == nil || *videoCopy.W == 0 { + w := int64(640) + videoCopy.W = &w + } + if videoCopy.H == nil || *videoCopy.H == 0 { + h := int64(480) + videoCopy.H = &h + } + if videoCopy.Placement == 0 { + videoCopy.Placement = 1 + } + if videoCopy.Linearity == 0 { + videoCopy.Linearity = 1 + } + + impCopy.Video = &videoCopy + } + + validImps = append(validImps, impCopy) + } + + // If no valid impressions, return errors + if len(validImps) == 0 { + return nil, errs + } + + // Create the outgoing request + requestCopy := *request + requestCopy.Imp = validImps + requestCopy.Cur = nil + + isDebug := request.Test == 1 + if !isDebug && len(request.Ext) > 0 { + var extRequest openrtb_ext.ExtRequest + if err := jsonutil.Unmarshal(request.Ext, &extRequest); err == nil { + isDebug = extRequest.Prebid.Debug + } + } + + if isDebug { + reqExt := openrtb_ext.ExtRequestScalibur{IsDebug: 1} + if reqExtJSON, err := jsonutil.Marshal(reqExt); err == nil { + requestCopy.Ext = reqExtJSON + } + } else { + requestCopy.Ext = nil + } + + reqJSON, err := jsonutil.Marshal(requestCopy) + if err != nil { + return nil, append(errs, err) + } + + var endpointBuffer bytes.Buffer + if err := a.endpoint.Execute(&endpointBuffer, nil); err != nil { + return nil, []error{err} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: endpointBuffer.String(), + Body: reqJSON, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(requestCopy.Imp), + } + + return []*adapters.RequestData{requestData}, errs +} + +// MakeBids unpacks the server's response into bids. +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + + if response.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), + }} + } + + var bidResp openrtb2.BidResponse + if err := jsonutil.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + // Parse the external request to get impression details + var bidReq openrtb2.BidRequest + if err := jsonutil.Unmarshal(externalRequest.Body, &bidReq); err != nil { + return nil, []error{err} + } + + // Build impression map for lookup + impMap := make(map[string]*openrtb2.Imp, len(bidReq.Imp)) + for i := range bidReq.Imp { + impMap[bidReq.Imp[i].ID] = &bidReq.Imp[i] + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidReq.Imp)) + + // Set currency + if bidResp.Cur != "" { + bidResponse.Currency = bidResp.Cur + } else { + bidResponse.Currency = "USD" + } + + // Process each seat bid + for _, seatBid := range bidResp.SeatBid { + for _, bid := range seatBid.Bid { + // Find the corresponding imp + imp, found := impMap[bid.ImpID] + if !found { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Invalid bid imp ID %s", bid.ImpID), + }} + } + + // Determine bid type based on imp + bidType, err := getBidMediaType(bid, imp) + if err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: err.Error(), + }} + } + + bidCopy := bid + + // Handle video VAST + if bidType == openrtb_ext.BidTypeVideo { + // Try to extract custom fields vastXml and vastUrl from bid.ext + var bidExtData struct { + VastXML string `json:"vastXml"` + VastURL string `json:"vastUrl"` + } + if bid.Ext != nil { + if err := jsonutil.Unmarshal(bid.Ext, &bidExtData); err == nil { + if bidExtData.VastXML != "" { + bidCopy.AdM = bidExtData.VastXML + } else if bidExtData.VastURL != "" && bidCopy.AdM == "" { + // Wrap VAST URL in VAST wrapper + bidCopy.AdM = fmt.Sprintf(``, bidExtData.VastURL) + } + } + } + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bidCopy, + BidType: bidType, + }) + } + } + + if len(bidResponse.Bids) == 0 { + return nil, nil + } + + return bidResponse, nil +} + +// parseScaliburExt extracts Scalibur-specific parameters from the impression extension. +func parseScaliburExt(impExt json.RawMessage) (*openrtb_ext.ExtImpScalibur, error) { + var bidderExt adapters.ExtImpBidder + if err := jsonutil.Unmarshal(impExt, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to parse imp.ext: %s", err.Error()), + } + } + + var scaliburExt openrtb_ext.ExtImpScalibur + if err := jsonutil.Unmarshal(bidderExt.Bidder, &scaliburExt); err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to parse Scalibur params: %s", err.Error()), + } + } + + if scaliburExt.PlacementID == "" { + return nil, &errortypes.BadInput{ + Message: "placementId is required", + } + } + + return &scaliburExt, nil +} + +// getBidMediaType determines the media type based on the impression +func getBidMediaType(bid openrtb2.Bid, imp *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 + case openrtb2.MarkupAudio: + return openrtb_ext.BidTypeAudio, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + } + + // Fallback for bidders not supporting mtype (non-multi-format requests) + if imp.Banner != nil && imp.Video == nil { + return openrtb_ext.BidTypeBanner, nil + } + if imp.Video != nil && imp.Banner == nil { + return openrtb_ext.BidTypeVideo, nil + } + + return "", fmt.Errorf("unsupported or ambiguous media type for bid id=%s", bid.ID) +} diff --git a/adapters/scalibur/scalibur_test.go b/adapters/scalibur/scalibur_test.go new file mode 100644 index 00000000000..2979cba817c --- /dev/null +++ b/adapters/scalibur/scalibur_test.go @@ -0,0 +1,339 @@ +package scalibur + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v3/adapters" + "github.com/prebid/prebid-server/v3/adapters/adapterstest" + "github.com/prebid/prebid-server/v3/config" + "github.com/prebid/prebid-server/v3/openrtb_ext" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestAdapter() adapters.Bidder { + adapter, _ := Builder( + openrtb_ext.BidderScalibur, + config.Adapter{Endpoint: "https://srv.scalibur.io/adserver/ortb?type=prebid-server"}, + config.Server{}, + ) + return adapter +} + +// +// ------------------------------------------------------------------------------------------ +// MAKE REQUESTS TESTS +// ------------------------------------------------------------------------------------------ +// + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "scaliburtest", newTestAdapter()) +} + +func TestMakeRequests_SuccessBanner(t *testing.T) { + bidder := newTestAdapter() + + ext, _ := json.Marshal(adapters.ExtImpBidder{ + Bidder: json.RawMessage(`{ + "placementId": "p123", + "bidfloor": 1.25, + "bidfloorcur": "EUR" + }`), + }) + + req := &openrtb2.BidRequest{ + ID: "req1", + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Ext: ext, + Banner: &openrtb2.Banner{ + W: ptrInt64(300), H: ptrInt64(250), + }, + }, + }, + Site: &openrtb2.Site{Page: "https://test.com"}, + } + + requests, errs := bidder.MakeRequests(req, &adapters.ExtraRequestInfo{ + CurrencyConversions: &mockConversions{}, + }) + + require.Len(t, errs, 0) + require.Len(t, requests, 1) + + r := requests[0] + assert.Equal(t, "https://srv.scalibur.io/adserver/ortb?type=prebid-server", r.Uri) + assert.Equal(t, "POST", r.Method) + assert.Contains(t, r.Headers.Get("Content-Type"), "application/json") + + // Ensure body contains rewritten ext fields + var out openrtb2.BidRequest + require.NoError(t, json.Unmarshal(r.Body, &out)) + require.Len(t, out.Imp, 1) + + imp := out.Imp[0] + + assert.Equal(t, float64(1.25), imp.BidFloor) + assert.Equal(t, "USD", imp.BidFloorCur) + + var outExt map[string]interface{} + require.NoError(t, json.Unmarshal(imp.Ext, &outExt)) + assert.Equal(t, "p123", outExt["placementId"]) +} + +func TestMakeRequests_InvalidExt(t *testing.T) { + bidder := newTestAdapter() + + // Missing placementId + badExt, _ := json.Marshal(adapters.ExtImpBidder{ + Bidder: json.RawMessage(`{"bidfloor": 1.5}`), + }) + + req := &openrtb2.BidRequest{ + ID: "req2", + Imp: []openrtb2.Imp{ + { + ID: "imp1", + Ext: badExt, + Banner: &openrtb2.Banner{ + W: ptrInt64(300), H: ptrInt64(250), + }, + }, + }, + } + + requests, errs := bidder.MakeRequests(req, &adapters.ExtraRequestInfo{}) + + require.Len(t, requests, 0) + require.Len(t, errs, 1) + assert.Contains(t, errs[0].Error(), "placementId is required") +} + +func TestMakeRequests_VideoDefaultsApplied(t *testing.T) { + bidder := newTestAdapter() + + ext, _ := json.Marshal(adapters.ExtImpBidder{ + Bidder: json.RawMessage(`{"placementId": "p123"}`), + }) + + req := &openrtb2.BidRequest{ + ID: "req-video", + Imp: []openrtb2.Imp{ + { + ID: "v1", + Ext: ext, + Video: &openrtb2.Video{ + // Intentionally empty → should fill defaults + }, + }, + }, + } + + requests, errs := bidder.MakeRequests(req, &adapters.ExtraRequestInfo{ + CurrencyConversions: &mockConversions{}, + }) + + require.Len(t, errs, 0) + require.Len(t, requests, 1) + + var out openrtb2.BidRequest + require.NoError(t, json.Unmarshal(requests[0].Body, &out)) + + v := out.Imp[0].Video + require.NotNil(t, v) + + assert.NotEmpty(t, v.MIMEs) + assert.NotZero(t, v.MinDuration) + assert.NotZero(t, v.MaxDuration) + assert.NotZero(t, v.MaxBitRate) + assert.NotEmpty(t, v.Protocols) + assert.NotNil(t, v.W) + assert.NotNil(t, v.H) + assert.NotZero(t, v.Placement) + assert.NotZero(t, v.Linearity) +} + +type mockConversions struct{} + +func (m *mockConversions) GetRate(from string, to string) (float64, error) { + return 1.0, nil +} + +func (m *mockConversions) GetRates() *map[string]map[string]float64 { + return nil +} + +// +// ------------------------------------------------------------------------------------------ +// MAKE BIDS TESTS +// ------------------------------------------------------------------------------------------ +// + +func TestMakeBids_SuccessBanner(t *testing.T) { + bidder := newTestAdapter() + + mockReq := &openrtb2.BidRequest{ + ID: "req1", + Imp: []openrtb2.Imp{ + { + ID: "1", + Banner: &openrtb2.Banner{W: ptrInt64(300), H: ptrInt64(250)}, + }, + }, + } + + mockReqData := &adapters.RequestData{ + Body: func() []byte { + b, _ := json.Marshal(mockReq) + return b + }(), + } + + mockResp := &openrtb2.BidResponse{ + ID: "resp1", + Cur: "USD", + SeatBid: []openrtb2.SeatBid{ + { + Bid: []openrtb2.Bid{ + { + ID: "b1", + ImpID: "1", + Price: 2.5, + AdM: "
ad markup
", + W: 300, + H: 250, + }, + }, + }, + }, + } + + respData := &adapters.ResponseData{ + StatusCode: http.StatusOK, + Body: func() []byte { + b, _ := json.Marshal(mockResp) + return b + }(), + } + + bidderResp, errs := bidder.MakeBids(mockReq, mockReqData, respData) + + require.Len(t, errs, 0) + require.NotNil(t, bidderResp) + require.Len(t, bidderResp.Bids, 1) + + b := bidderResp.Bids[0] + assert.Equal(t, openrtb_ext.BidTypeBanner, b.BidType) + assert.Equal(t, float64(2.5), b.Bid.Price) + assert.Equal(t, "
ad markup
", b.Bid.AdM) +} + +func TestMakeBids_EmptySeatBid(t *testing.T) { + bidder := newTestAdapter() + + mockReq := &openrtb2.BidRequest{ + ID: "req2", + Imp: []openrtb2.Imp{}, + } + + mockReqData := &adapters.RequestData{ + Body: []byte(`{"id":"req2","imp":[]}`), + } + + respData := &adapters.ResponseData{ + StatusCode: http.StatusOK, + Body: []byte(`{"id":"resp","seatbid":[]}`), + } + + bidderResp, errs := bidder.MakeBids(mockReq, mockReqData, respData) + + require.Len(t, errs, 0) + assert.Nil(t, bidderResp) +} + +func TestMakeBids_Status204(t *testing.T) { + bidder := newTestAdapter() + + respData := &adapters.ResponseData{ + StatusCode: http.StatusNoContent, + } + + resp, errs := bidder.MakeBids(&openrtb2.BidRequest{}, &adapters.RequestData{}, respData) + + assert.Nil(t, resp) + assert.Nil(t, errs) +} + +func TestMakeBids_InvalidJSON(t *testing.T) { + bidder := newTestAdapter() + + respData := &adapters.ResponseData{ + StatusCode: http.StatusOK, + Body: []byte(`not-json`), + } + + _, errs := bidder.MakeBids(&openrtb2.BidRequest{}, &adapters.RequestData{}, respData) + require.Len(t, errs, 1) +} + +func TestMakeBids_ImpNotFound(t *testing.T) { + bidder := newTestAdapter() + + mockReq := &openrtb2.BidRequest{ + ID: "req1", + Imp: []openrtb2.Imp{ + {ID: "1"}, + }, + } + + mockReqData := &adapters.RequestData{ + Body: []byte(`{"id":"req1","imp":[{"id":"1"}]}`), + } + + mockResp := &openrtb2.BidResponse{ + ID: "resp1", + SeatBid: []openrtb2.SeatBid{ + { + Bid: []openrtb2.Bid{ + { + ID: "b1", + ImpID: "non-existent", + }, + }, + }, + }, + } + + respData := &adapters.ResponseData{ + StatusCode: http.StatusOK, + Body: func() []byte { + b, _ := json.Marshal(mockResp) + return b + }(), + } + + _, errs := bidder.MakeBids(mockReq, mockReqData, respData) + require.Len(t, errs, 1) + assert.Contains(t, errs[0].Error(), "Invalid bid imp ID non-existent") +} + +func TestGetBidMediaType_Error(t *testing.T) { + bid := openrtb2.Bid{ + ID: "bid1", + ImpID: "imp1", + } + imp := &openrtb2.Imp{ + ID: "imp1", + // No banner or video + } + + _, err := getBidMediaType(bid, imp) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsupported or ambiguous media type") +} + +func ptrInt64(x int64) *int64 { return &x } diff --git a/adapters/scalibur/scaliburtest/exemplary/debug.json b/adapters/scalibur/scaliburtest/exemplary/debug.json new file mode 100644 index 00000000000..4422ffb07ee --- /dev/null +++ b/adapters/scalibur/scaliburtest/exemplary/debug.json @@ -0,0 +1,59 @@ +{ + "mockBidRequest": { + "id": "test-debug-request", + "test": 1, + "imp": [{ + "id": "test-imp-id", + "banner": { "format": [{"w": 300, "h": 250}] }, + "ext": { "bidder": { "placementId": "test-placement" } } + }] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server", + "impIDs": ["test-imp-id"], + "body": { + "id": "test-debug-request", + "test": 1, + "imp": [{ + "id": "test-imp-id", + "banner": { "format": [{"w": 300, "h": 250}] }, + "bidfloorcur": "USD", + "ext": { + "placementId": "test-placement", + "bidfloorcur": "USD" + } + }], + "ext": { "isDebug": 1 } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-debug-request", + "seatbid": [{ + "bid": [{ + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "...", + "crid": "test-creative-id" + }] + }] + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "...", + "crid": "test-creative-id" + }, + "type": "banner" + }] + }] +} diff --git a/adapters/scalibur/scaliburtest/exemplary/debug_prebid.json b/adapters/scalibur/scaliburtest/exemplary/debug_prebid.json new file mode 100644 index 00000000000..5f780c6810f --- /dev/null +++ b/adapters/scalibur/scaliburtest/exemplary/debug_prebid.json @@ -0,0 +1,62 @@ +{ + "mockBidRequest": { + "id": "test-debug-prebid-request", + "imp": [{ + "id": "test-imp-id", + "banner": { "format": [{"w": 300, "h": 250}] }, + "ext": { "bidder": { "placementId": "test-placement" } } + }], + "ext": { + "prebid": { + "debug": true + } + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server", + "impIDs": ["test-imp-id"], + "body": { + "id": "test-debug-prebid-request", + "imp": [{ + "id": "test-imp-id", + "banner": { "format": [{"w": 300, "h": 250}] }, + "bidfloorcur": "USD", + "ext": { + "placementId": "test-placement", + "bidfloorcur": "USD" + } + }], + "ext": { "isDebug": 1 } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-debug-prebid-request", + "seatbid": [{ + "bid": [{ + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "...", + "crid": "test-creative-id" + }] + }] + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "...", + "crid": "test-creative-id" + }, + "type": "banner" + }] + }] +} diff --git a/adapters/scalibur/scaliburtest/exemplary/gpid.json b/adapters/scalibur/scaliburtest/exemplary/gpid.json new file mode 100644 index 00000000000..393c8a86ddb --- /dev/null +++ b/adapters/scalibur/scaliburtest/exemplary/gpid.json @@ -0,0 +1,65 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "banner": { "format": [{"w": 300, "h": 250}] }, + "ext": { + "bidder": { "placementId": "test-scl-placement" }, + "gpid": "test-gpid-value" + } + }] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server", + "impIDs": ["test-imp-id"], + "body": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "banner": { "format": [{"w": 300, "h": 250}] }, + "bidfloorcur": "USD", + "ext": { + "placementId": "test-scl-placement", + "bidfloorcur": "USD", + "gpid": "test-gpid-value" + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [{ + "bid": [{ + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "...", + "crid": "test-creative-id", + "w": 300, + "h": 250 + }] + }], + "cur": "USD" + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "...", + "crid": "test-creative-id", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }] +} diff --git a/adapters/scalibur/scaliburtest/exemplary/mtype.json b/adapters/scalibur/scaliburtest/exemplary/mtype.json new file mode 100644 index 00000000000..4256373d8f8 --- /dev/null +++ b/adapters/scalibur/scaliburtest/exemplary/mtype.json @@ -0,0 +1,58 @@ +{ + "mockBidRequest": { + "id": "test-mtype-request", + "imp": [{ + "id": "test-imp-id", + "banner": { "format": [{"w": 300, "h": 250}] }, + "ext": { "bidder": { "placementId": "test-placement" } } + }] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server", + "impIDs": ["test-imp-id"], + "body": { + "id": "test-mtype-request", + "imp": [{ + "id": "test-imp-id", + "banner": { "format": [{"w": 300, "h": 250}] }, + "bidfloorcur": "USD", + "ext": { + "placementId": "test-placement", + "bidfloorcur": "USD" + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-mtype-request", + "seatbid": [{ + "bid": [{ + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "...", + "crid": "test-creative-id", + "mtype": 1 + }] + }] + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "...", + "crid": "test-creative-id", + "mtype": 1 + }, + "type": "banner" + }] + }] +} diff --git a/adapters/scalibur/scaliburtest/exemplary/success_banner.json b/adapters/scalibur/scaliburtest/exemplary/success_banner.json new file mode 100644 index 00000000000..aabe2beb479 --- /dev/null +++ b/adapters/scalibur/scaliburtest/exemplary/success_banner.json @@ -0,0 +1,61 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "banner": { "format": [{"w": 300, "h": 250}] }, + "ext": { "bidder": { "placementId": "test-scl-placement" } } + }] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server", + "impIDs": ["test-imp-id"], + "body": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "banner": { "format": [{"w": 300, "h": 250}] }, + "bidfloorcur": "USD", + "ext": { + "placementId": "test-scl-placement", + "bidfloorcur": "USD" + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [{ + "bid": [{ + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "...", + "crid": "test-creative-id", + "w": 300, + "h": 250 + }] + }], + "cur": "USD" + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "...", + "crid": "test-creative-id", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }] +} \ No newline at end of file diff --git a/adapters/scalibur/scaliburtest/exemplary/video.json b/adapters/scalibur/scaliburtest/exemplary/video.json new file mode 100644 index 00000000000..7eaea7a2895 --- /dev/null +++ b/adapters/scalibur/scaliburtest/exemplary/video.json @@ -0,0 +1,78 @@ +{ + "mockBidRequest": { + "id": "test-video-req", + "imp": [{ + "id": "video-imp-1", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "placementId": "46" + } + } + }] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server", + "impIDs": ["video-imp-1"], + "body": { + "id": "test-video-req", + "imp": [{ + "id": "video-imp-1", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 180, + "protocols": [2, 3, 5, 6], + "w": 640, + "h": 480, + "placement": 1, + "linearity": 1, + "maxbitrate": 30000 + }, + "bidfloorcur": "USD", + "ext": { + "placementId": "46", + "bidfloorcur": "USD" + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-video-req", + "seatbid": [{ + "bid": [{ + "id": "bid-v1", + "impid": "video-imp-1", + "price": 1.5, + "crid": "cr-video", + "ext": { + "vastXml": "..." + } + }] + }], + "cur": "USD" + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "bid-v1", + "impid": "video-imp-1", + "price": 1.5, + "adm": "...", + "crid": "cr-video", + "ext": { "vastXml": "..." } + }, + "type": "video" + }] + }] +} \ No newline at end of file diff --git a/adapters/scalibur/scaliburtest/exemplary/video_vast_url.json b/adapters/scalibur/scaliburtest/exemplary/video_vast_url.json new file mode 100644 index 00000000000..f00833b95b9 --- /dev/null +++ b/adapters/scalibur/scaliburtest/exemplary/video_vast_url.json @@ -0,0 +1,67 @@ +{ + "mockBidRequest": { + "id": "test-video-url-req", + "imp": [{ + "id": "video-imp-2", + "video": { "mimes": ["video/mp4"] }, + "ext": { "bidder": { "placementId": "46" } } + }] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server", + "impIDs": ["video-imp-2"], + "body": { + "id": "test-video-url-req", + "imp": [{ + "id": "video-imp-2", + "video": { + "mimes": ["video/mp4"], + "minduration": 1, + "maxduration": 180, + "protocols": [2, 3, 5, 6], + "w": 640, + "h": 480, + "placement": 1, + "linearity": 1, + "maxbitrate": 30000 + }, + "bidfloorcur": "USD", + "ext": { + "placementId": "46", + "bidfloorcur": "USD" + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-video-url-req", + "seatbid": [{ + "bid": [{ + "id": "bid-v2", + "impid": "video-imp-2", + "price": 2.0, + "ext": { + "vastUrl": "https://cdn.scalibur.io/vast.xml" + } + }] + }] + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "bid-v2", + "impid": "video-imp-2", + "price": 2.0, + "adm": "", + "ext": { "vastUrl": "https://cdn.scalibur.io/vast.xml" } + }, + "type": "video" + }] + }] +} \ No newline at end of file diff --git a/docs/bidders/scalibur.md b/docs/bidders/scalibur.md new file mode 100644 index 00000000000..95f409b997d --- /dev/null +++ b/docs/bidders/scalibur.md @@ -0,0 +1,16 @@ +# Scalibur + +The Scalibur adapter supports Banner and Video media types via OpenRTB 2.6. + +## Registration +Contact [support@scalibur.io](mailto:support@scalibur.io) to obtain a valid `placementId`. + +## Bid Params +| Name | Type | Description | Notes | +| :--- | :--- | :--- |:----------------------| +| `placementId` | string | **Required**. Scalibur placement identifier. | Example: `"468acd11"` | +| `bidfloor` | number | Optional. Minimum bid floor price. | | +| `bidfloorcur` | string | Optional. Currency for the bid floor. | Default is `USD`. | + +## User Sync +Supports iframe synchronization. Prebid Server handles the mapping of the sync ID to the `user.buyeruid` field in outgoing OpenRTB requests. \ No newline at end of file diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 3cb651bcbdb..cccc1bdf288 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -201,6 +201,7 @@ import ( "github.com/prebid/prebid-server/v3/adapters/rtbhouse" "github.com/prebid/prebid-server/v3/adapters/rubicon" salunamedia "github.com/prebid/prebid-server/v3/adapters/sa_lunamedia" + "github.com/prebid/prebid-server/v3/adapters/scalibur" "github.com/prebid/prebid-server/v3/adapters/screencore" "github.com/prebid/prebid-server/v3/adapters/seedingAlliance" "github.com/prebid/prebid-server/v3/adapters/seedtag" @@ -473,6 +474,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderSeedingAlliance: seedingAlliance.Builder, openrtb_ext.BidderSeedtag: seedtag.Builder, openrtb_ext.BidderSaLunaMedia: salunamedia.Builder, + openrtb_ext.BidderScalibur: scalibur.Builder, openrtb_ext.BidderScreencore: screencore.Builder, openrtb_ext.BidderSharethrough: sharethrough.Builder, openrtb_ext.BidderShowheroes: showheroes.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index e94d0fe5513..e563260ca1f 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -221,6 +221,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderSeedingAlliance, BidderSeedtag, BidderSaLunaMedia, + BidderScalibur, BidderScreencore, BidderSharethrough, BidderShowheroes, @@ -595,6 +596,7 @@ const ( BidderSeedingAlliance BidderName = "seedingAlliance" BidderSeedtag BidderName = "seedtag" BidderSaLunaMedia BidderName = "sa_lunamedia" + BidderScalibur BidderName = "scalibur" BidderScreencore BidderName = "screencore" BidderSharethrough BidderName = "sharethrough" BidderShowheroes BidderName = "showheroes" diff --git a/openrtb_ext/imp_scalibur.go b/openrtb_ext/imp_scalibur.go new file mode 100644 index 00000000000..9a0ff7902f5 --- /dev/null +++ b/openrtb_ext/imp_scalibur.go @@ -0,0 +1,11 @@ +package openrtb_ext + +type ExtImpScalibur struct { + PlacementID string `json:"placementId"` // required + BidFloor *float64 `json:"bidfloor,omitempty"` // optional, used as fallback + BidFloorCur string `json:"bidfloorcur,omitempty"` // optional, defaults to USD if empty +} + +type ExtRequestScalibur struct { + IsDebug int `json:"isDebug,omitempty"` +} diff --git a/static/bidder-info/scalibur.yaml b/static/bidder-info/scalibur.yaml new file mode 100644 index 00000000000..4b6a081d81a --- /dev/null +++ b/static/bidder-info/scalibur.yaml @@ -0,0 +1,34 @@ +endpoint: "https://srv.scalibur.io/adserver/ortb?type=prebid-server" + +# Leave disabled: false → adapter enabled +compression: gzip +disabled: false + +maintainer: + email: "support@scalibur.io" +supported-media-types: + - banner + - video + +gvlVendorID: 1471 + +openrtb: + version: 2.6 + gpp-supported: true + +capabilities: + site: + mediaTypes: + - banner + - video + app: + mediaTypes: + - banner + - video + +userSync: + iframe: + url: "https://srv.scalibur.io/adserver/sync?type=iframe&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}" + userMacro: "[PBS_UID]" + supports: + - iframe \ No newline at end of file diff --git a/static/bidder-params/scalibur.json b/static/bidder-params/scalibur.json new file mode 100644 index 00000000000..05a12e99f24 --- /dev/null +++ b/static/bidder-params/scalibur.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Scalibur Adapter Params", + "description": "A valid BidderParams object for the Scalibur adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1 + }, + "bidfloor": { + "type": "number", + "minimum": 0 + }, + "bidfloorcur": { + "type": "string" + } + }, + "required": [ + "placementId" + ] +}