From dfb4691dbc3988aa60c4138ea85b27f98b9d6197 Mon Sep 17 00:00:00 2001 From: Andrea Fassina Date: Mon, 25 Nov 2024 17:31:10 +0100 Subject: [PATCH] feat: add nativery prebid adapter --- adapters/nativery/models.go | 39 ++++ adapters/nativery/nativery.go | 283 +++++++++++++++++++++++++++++ adapters/nativery/nativery_test.go | 20 ++ adapters/nativery/params_test.go | 55 ++++++ exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_nativery.go | 8 + static/bidder-info/nativery.yaml | 13 ++ static/bidder-params/nativery.json | 15 ++ 9 files changed, 437 insertions(+) create mode 100644 adapters/nativery/models.go create mode 100644 adapters/nativery/nativery.go create mode 100644 adapters/nativery/nativery_test.go create mode 100644 adapters/nativery/params_test.go create mode 100644 openrtb_ext/imp_nativery.go create mode 100644 static/bidder-info/nativery.yaml create mode 100644 static/bidder-params/nativery.json diff --git a/adapters/nativery/models.go b/adapters/nativery/models.go new file mode 100644 index 00000000000..1984a913702 --- /dev/null +++ b/adapters/nativery/models.go @@ -0,0 +1,39 @@ +package nativery + +type adapter struct { + endpoint string +} + +type refRef struct { + Page string `json:"page"` + Ref string `json:"ref"` +} + +// request body to send to widget server in ext +type nativeryExtReqBody struct { + Id string `json:"id"` //the placement/widget id + Xhr int `json:"xhr"` + V int `json:"v"` + Ref string `json:"ref"` + RefRef refRef `json:"refref"` +} + +type impExt struct { + Nativery nativeryExtReqBody `json:"nativery"` +} + +type bidReqExtNativery struct { + IsAMP bool `json:"is_amp"` +} + +type bidExtNativery struct { + BidType string `json:"bid_ad_media_type"` + BidAdvDomains []string `json:"bid_adv_domains"` + + BrandId int `json:"brand_id,omitempty"` + BrandCategory int `json:"brand_category_id,omitempty"` +} + +type bidExt struct { + Nativery bidExtNativery `json:"nativery"` +} diff --git a/adapters/nativery/nativery.go b/adapters/nativery/nativery.go new file mode 100644 index 00000000000..7b8b34b1bbb --- /dev/null +++ b/adapters/nativery/nativery.go @@ -0,0 +1,283 @@ +package nativery + +import ( + "fmt" + "maps" + "net/http" + "encoding/json" + + "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/metrics" + "github.com/prebid/prebid-server/v3/openrtb_ext" +) + +// Function used to builds a new instance of the Nativery adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + // build bidder + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +// makeRequests creates HTTP requests for a given BidRequest and adapter configuration. +// It generates requests for each ad exchange targeted by the BidRequest, +// serializes the BidRequest into the request body, and sets the appropriate +// HTTP headers and other parameters. +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + reqCopy := *request + var errs []error + + // check if the request come from AMP + var isAMP int + if reqInfo.PbsEntryPoint == metrics.ReqTypeAMP { + isAMP = 1 + } + + // attach body request for all the impressions + validImps := []openrtb2.Imp{} + for _, imp := range request.Imp { + reqCopy.Imp = []openrtb2.Imp{imp} + + nativeryExt, err := buildNativeryExt(&reqCopy.Imp[0]) + if err != nil { + errs = append(errs, err) + continue + } + + if err := buildRequest(reqCopy, nativeryExt); err != nil { + errs = append(errs, err) + continue + } + + validImps = append(validImps, reqCopy.Imp...) + + } + + reqCopy.Imp = validImps + // If all the requests were malformed, don't bother making a server call with no impressions. + if len(reqCopy.Imp) == 0 { + return nil, errs + } + + reqExt, err := getRequestExt(reqCopy.Ext) + if err != nil { + return nil, append(errs, err) + } + + reqExtNativery, err := getNativeryExt(reqExt, isAMP) + if err != nil { + return nil, append(errs, err) + } + // TODO: optimize it, we reiterate imp there and before + adapterRequests, errors := splitRequests(reqCopy.Imp, &reqCopy, reqExt, reqExtNativery, a.endpoint) + + return adapterRequests, append(errs, errors...) +} + +func buildNativeryExt(imp *openrtb2.Imp) (openrtb_ext.ImpExtNativery, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return openrtb_ext.ImpExtNativery{}, err + } + + var nativeryExt openrtb_ext.ImpExtNativery + if err := json.Unmarshal(bidderExt.Bidder, &nativeryExt); err != nil { + return openrtb_ext.ImpExtNativery{}, err + } + + return nativeryExt, nil +} + +// utility function used to build the body for the http request for a single impression +func buildRequest(reqCopy openrtb2.BidRequest, reqExt openrtb_ext.ImpExtNativery) error { + + impExt := impExt{Nativery: nativeryExtReqBody{ + Id: reqExt.PlacementID, + Xhr: 2, + V: 3, + // TODO: Site is only for browser request, we have to handle if the req comes from app or dooh + Ref: reqCopy.Site.Page, + RefRef: refRef{Page: reqCopy.Site.Page, Ref: reqCopy.Site.Ref}, + }} + + var err error + reqCopy.Imp[0].Ext, err = json.Marshal(&impExt) + + return err +} + +// makebids handles the entire bidding process for a single BidRequest. +// It creates and sends bid requests to multiple ad exchanges, receives +// and parses responses, extracts bids and other relevant information, +// and populates a BidderResponse object with the aggregated information. +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + // check if the response has no content + if adapters.IsResponseStatusCodeNoContent(response) { + return nil, nil + } + + // check if the response has errors + if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil { + return nil, []error{err} + } + + // handle response + var nativeryResponse openrtb2.BidResponse + if err := json.Unmarshal(response.Body, &nativeryResponse); err != nil { + return nil, []error{err} + } + + var errs []error + // create bidder with impressions length capacity + bidderResponse := adapters.NewBidderResponseWithBidsCapacity(len(internalRequest.Imp)) + for _, sb := range nativeryResponse.SeatBid { + for i := range sb.Bid { + bid := sb.Bid[i] + + // should be data sended from nativery server to partecipate to the auction + var bidExt bidExt + if err := json.Unmarshal(bid.Ext, &bidExt); err != nil { + errs = append(errs, err) + continue + } + + bidType, err := getMediaTypeForBid(&bidExt) + if err != nil { + errs = append(errs, err) + continue + } + // get metadata + bidMeta := buildBidMeta(string(bidType), bidExt.Nativery.BidAdvDomains) + + bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: bidType, + // metadata is encouraged + BidMeta: bidMeta, + }) + } + + } + + // set bidder currency, EUR by default + if nativeryResponse.Cur != "" { + bidderResponse.Currency = nativeryResponse.Cur + } else { + bidderResponse.Currency = "EUR" + } + return bidderResponse, errs + +} + +// getMediaTypeForBid switch nativery type in bid type. +func getMediaTypeForBid(bid *bidExt) (openrtb_ext.BidType, error) { + switch bid.Nativery.BidType { + case "native": + return openrtb_ext.BidTypeNative, nil + case "display": + return openrtb_ext.BidTypeBanner, nil + case "video": + return openrtb_ext.BidTypeVideo, nil + default: + return "", fmt.Errorf("unrecognized bid_ad_media_type in response from nativery: %s", bid.Nativery.BidType) + } +} + +func convertIntToBoolean(num *int) bool { + var b bool + // Dereferenzia num usando * + if num != nil && *num == 1 { + b = true + } else { + b = false + } + return b +} + +func buildBidMeta(mediaType string, advDomain []string) *openrtb_ext.ExtBidPrebidMeta { + + //advertiserDomains and dchain are encouraged to implements + return &openrtb_ext.ExtBidPrebidMeta{ + MediaType: mediaType, + AdvertiserDomains: advDomain, + /* + DChain: json.RawMessage{} , + Cosa include Dchain: + nodes: Un array di oggetti che rappresentano i diversi partecipanti alla catena di domanda. + complete: Un flag che indica se la catena di domanda รจ completa (1) o incompleta (0). + ver: La versione del modulo Dchain utilizzato. + */ + } +} + +func splitRequests(imps []openrtb2.Imp, request *openrtb2.BidRequest, requestExt map[string]json.RawMessage, requestExtNativery bidReqExtNativery, uri string) ([]*adapters.RequestData, []error) { + var errs []error + + resArr := make([]*adapters.RequestData, 0, 1) + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + nativeryExtJson, err := json.Marshal(requestExtNativery) + if err != nil { + errs = append(errs, err) + } + + requestExtClone := maps.Clone(requestExt) + requestExtClone["nativery"] = nativeryExtJson + + request.Ext, err = json.Marshal(requestExtClone) + if err != nil { + errs = append(errs, err) + } + + for _, imp := range imps { + impsForReq := []openrtb2.Imp{imp} + request.Imp = impsForReq + + reqJSON, err := json.Marshal(request) + if err != nil { + errs = append(errs, err) + return nil, errs + } + + resArr = append(resArr, &adapters.RequestData{ + Method: "POST", + Uri: uri, + Body: reqJSON, + Headers: headers, + }) + } + return resArr, errs +} + +func getRequestExt(ext json.RawMessage) (map[string]json.RawMessage, error) { + extMap := make(map[string]json.RawMessage) + + if len(ext) > 0 { + if err := json.Unmarshal(ext, &extMap); err != nil { + return nil, err + } + } + + return extMap, nil +} + +func getNativeryExt(extMap map[string]json.RawMessage, isAMP int) (bidReqExtNativery, error) { + var nativeryExt bidReqExtNativery + + // if ext.nativery already exists return it + if nativeryExtJson, exists := extMap["nativery"]; exists && len(nativeryExtJson) > 0 { + if err := json.Unmarshal(nativeryExtJson, &nativeryExt); err != nil { + return nativeryExt, err + } + } + + nativeryExt.IsAMP = convertIntToBoolean(&isAMP) + + return nativeryExt, nil +} diff --git a/adapters/nativery/nativery_test.go b/adapters/nativery/nativery_test.go new file mode 100644 index 00000000000..3a309e20349 --- /dev/null +++ b/adapters/nativery/nativery_test.go @@ -0,0 +1,20 @@ +package nativery + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderNativery, config.Adapter{ + Endpoint: "http://example.com/hb"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "nativerytest", bidder) +} diff --git a/adapters/nativery/params_test.go b/adapters/nativery/params_test.go new file mode 100644 index 00000000000..ce476a6a498 --- /dev/null +++ b/adapters/nativery/params_test.go @@ -0,0 +1,55 @@ +package nativery + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v3/openrtb_ext" +) + +// This file actually intends to test static/bidder-params/appnexus.json +// +// These also validate the format of the external API: request.imp[i].ext.prebid.bidder.appnexus + +// TestValidParams makes sure that the appnexus schema accepts all imp.ext fields which we intend to support. +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.BidderNativery, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected nativery params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the appnexus schema rejects all the imp.ext fields we don't support. +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.BidderNativery, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"placement_id":"123"}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"placement_id":123}`, +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 97a03dde2a5..0a2524f1f74 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -142,6 +142,7 @@ import ( "github.com/prebid/prebid-server/v3/adapters/mobfoxpb" "github.com/prebid/prebid-server/v3/adapters/mobilefuse" "github.com/prebid/prebid-server/v3/adapters/motorik" + "github.com/prebid/prebid-server/v3/adapters/nativery" "github.com/prebid/prebid-server/v3/adapters/nativo" "github.com/prebid/prebid-server/v3/adapters/nextmillennium" "github.com/prebid/prebid-server/v3/adapters/nobid" @@ -375,6 +376,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderMobfoxpb: mobfoxpb.Builder, openrtb_ext.BidderMobileFuse: mobilefuse.Builder, openrtb_ext.BidderMotorik: motorik.Builder, + openrtb_ext.BidderNativery: nativery.Builder, openrtb_ext.BidderNativo: nativo.Builder, openrtb_ext.BidderNextMillennium: nextmillennium.Builder, openrtb_ext.BidderNoBid: nobid.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 9e86ad86ec1..46b852b2310 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -160,6 +160,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderMobfoxpb, BidderMobileFuse, BidderMotorik, + BidderNativery, BidderNativo, BidderNextMillennium, BidderNoBid, @@ -490,6 +491,7 @@ const ( BidderMobfoxpb BidderName = "mobfoxpb" BidderMobileFuse BidderName = "mobilefuse" BidderMotorik BidderName = "motorik" + BidderNativery BidderName = "nativery" BidderNativo BidderName = "nativo" BidderNextMillennium BidderName = "nextmillennium" BidderNoBid BidderName = "nobid" diff --git a/openrtb_ext/imp_nativery.go b/openrtb_ext/imp_nativery.go new file mode 100644 index 00000000000..38fb87ee144 --- /dev/null +++ b/openrtb_ext/imp_nativery.go @@ -0,0 +1,8 @@ +package openrtb_ext + +// ImpExtNativery defines the contract for bidrequest.imp[i].ext.prebid.bidder.nativery +// ref to json schema in static/bidder-params/nativery + +type ImpExtNativery struct { + PlacementID string `json:"placement_id"` +} \ No newline at end of file diff --git a/static/bidder-info/nativery.yaml b/static/bidder-info/nativery.yaml new file mode 100644 index 00000000000..9b0694490bb --- /dev/null +++ b/static/bidder-info/nativery.yaml @@ -0,0 +1,13 @@ +endpoint: "http://127.0.0.1:3000/v1/pbjs" +maintainer: + email: "andrea.fassina@nativery.com" +gvlVendorID: 0000 +disabled: false +geoscope: + - EEA +capabilities: + site: + mediaTypes: + - banner + - native +userSync: \ No newline at end of file diff --git a/static/bidder-params/nativery.json b/static/bidder-params/nativery.json new file mode 100644 index 00000000000..b5e6df0e520 --- /dev/null +++ b/static/bidder-params/nativery.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Nativery Adapter Params", + "description": "A schema which validates params accepted by the Natiery adapter", + + "type": "object", + "properties": { + "placement_id": { + "type": "string", + "description": "An ID which identifies this placement of the impression" + } + }, + + "required": ["placement_id"] + } \ No newline at end of file