github.com/prebid/prebid-server/v2@v2.18.0/adapters/appnexus/appnexus.go (about)

     1  package appnexus
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/buger/jsonparser"
    13  	"github.com/prebid/openrtb/v20/adcom1"
    14  	"github.com/prebid/openrtb/v20/openrtb2"
    15  	"github.com/prebid/prebid-server/v2/config"
    16  	"github.com/prebid/prebid-server/v2/util/maputil"
    17  	"github.com/prebid/prebid-server/v2/util/ptrutil"
    18  	"github.com/prebid/prebid-server/v2/util/randomutil"
    19  
    20  	"github.com/prebid/prebid-server/v2/adapters"
    21  	"github.com/prebid/prebid-server/v2/errortypes"
    22  	"github.com/prebid/prebid-server/v2/metrics"
    23  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    24  )
    25  
    26  const (
    27  	defaultPlatformID = 5
    28  	maxImpsPerReq     = 10
    29  )
    30  
    31  type adapter struct {
    32  	uri             url.URL
    33  	hbSource        int
    34  	randomGenerator randomutil.RandomGenerator
    35  }
    36  
    37  // Builder builds a new instance of the AppNexus adapter for the given bidder with the given config.
    38  func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
    39  	uri, err := url.Parse(config.Endpoint)
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  
    44  	bidder := &adapter{
    45  		uri:             *uri,
    46  		hbSource:        resolvePlatformID(config.PlatformID),
    47  		randomGenerator: randomutil.RandomNumberGenerator{},
    48  	}
    49  	return bidder, nil
    50  }
    51  
    52  func resolvePlatformID(platformID string) int {
    53  	if len(platformID) > 0 {
    54  		if val, err := strconv.Atoi(platformID); err == nil {
    55  			return val
    56  		}
    57  	}
    58  
    59  	return defaultPlatformID
    60  }
    61  
    62  func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
    63  	// appnexus adapter expects imp.displaymanagerver to be populated in openrtb2 endpoint
    64  	// but some SDKs will put it in imp.ext.prebid instead
    65  	displayManagerVer := buildDisplayManageVer(request)
    66  
    67  	var (
    68  		shouldGenerateAdPodId *bool
    69  		uniqueMemberID        string
    70  		errs                  []error
    71  	)
    72  
    73  	validImps := []openrtb2.Imp{}
    74  	for i := 0; i < len(request.Imp); i++ {
    75  		appnexusExt, err := validateAndBuildAppNexusExt(&request.Imp[i])
    76  		if err != nil {
    77  			errs = append(errs, err)
    78  			continue
    79  		}
    80  
    81  		if err := buildRequestImp(&request.Imp[i], &appnexusExt, displayManagerVer); err != nil {
    82  			errs = append(errs, err)
    83  			continue
    84  		}
    85  
    86  		memberId := appnexusExt.Member
    87  		if memberId != "" {
    88  			// The Appnexus API requires a Member ID in the URL. This means the request may fail if
    89  			// different impressions have different member IDs.
    90  			// Check for this condition, and log an error if it's a problem.
    91  			if uniqueMemberID == "" {
    92  				uniqueMemberID = memberId
    93  			} else if uniqueMemberID != memberId {
    94  				errs = append(errs, fmt.Errorf("all request.imp[i].ext.prebid.bidder.appnexus.member params must match. Request contained member IDs %s and %s", uniqueMemberID, memberId))
    95  				return nil, errs
    96  			}
    97  		}
    98  
    99  		shouldGenerateAdPodIdForImp := appnexusExt.AdPodId
   100  		if shouldGenerateAdPodId == nil {
   101  			shouldGenerateAdPodId = &shouldGenerateAdPodIdForImp
   102  		} else if *shouldGenerateAdPodId != shouldGenerateAdPodIdForImp {
   103  			errs = append(errs, errors.New("generate ad pod option should be same for all pods in request"))
   104  			return nil, errs
   105  		}
   106  
   107  		validImps = append(validImps, request.Imp[i])
   108  	}
   109  	request.Imp = validImps
   110  
   111  	// If all the requests were malformed, don't bother making a server call with no impressions.
   112  	if len(request.Imp) == 0 {
   113  		return nil, errs
   114  	}
   115  
   116  	requestURI := a.uri
   117  	if uniqueMemberID != "" {
   118  		requestURI = appendMemberId(requestURI, uniqueMemberID)
   119  	}
   120  
   121  	// Add Appnexus request level extension
   122  	var isAMP, isVIDEO int
   123  	if reqInfo.PbsEntryPoint == metrics.ReqTypeAMP {
   124  		isAMP = 1
   125  	} else if reqInfo.PbsEntryPoint == metrics.ReqTypeVideo {
   126  		isVIDEO = 1
   127  	}
   128  
   129  	reqExt, err := getRequestExt(request.Ext)
   130  	if err != nil {
   131  		return nil, append(errs, err)
   132  	}
   133  
   134  	reqExtAppnexus, err := a.getAppnexusExt(reqExt, isAMP, isVIDEO)
   135  	if err != nil {
   136  		return nil, append(errs, err)
   137  	}
   138  
   139  	if err := moveSupplyChain(request, reqExt); err != nil {
   140  		return nil, append(errs, err)
   141  	}
   142  
   143  	// For long form requests if adpodId feature enabled, adpod_id must be sent downstream.
   144  	// Adpod id is a unique identifier for pod
   145  	// All impressions in the same pod must have the same pod id in request extension
   146  	// For this all impressions in  request should belong to the same pod
   147  	// If impressions number per pod is more than maxImpsPerReq - divide those imps to several requests but keep pod id the same
   148  	// If  adpodId feature disabled and impressions number per pod is more than maxImpsPerReq  - divide those imps to several requests but do not include ad pod id
   149  	if isVIDEO == 1 && *shouldGenerateAdPodId {
   150  		requests, errors := a.buildAdPodRequests(request.Imp, request, reqExt, reqExtAppnexus, requestURI.String())
   151  		return requests, append(errs, errors...)
   152  	}
   153  
   154  	requests, errors := splitRequests(request.Imp, request, reqExt, reqExtAppnexus, requestURI.String())
   155  	return requests, append(errs, errors...)
   156  }
   157  
   158  func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
   159  	if adapters.IsResponseStatusCodeNoContent(response) {
   160  		return nil, nil
   161  	}
   162  
   163  	if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil {
   164  		return nil, []error{err}
   165  	}
   166  
   167  	var appnexusResponse openrtb2.BidResponse
   168  	if err := json.Unmarshal(response.Body, &appnexusResponse); err != nil {
   169  		return nil, []error{err}
   170  	}
   171  
   172  	var errs []error
   173  	bidderResponse := adapters.NewBidderResponseWithBidsCapacity(5)
   174  	for _, sb := range appnexusResponse.SeatBid {
   175  		for i := range sb.Bid {
   176  			bid := sb.Bid[i]
   177  
   178  			var bidExt bidExt
   179  			if err := json.Unmarshal(bid.Ext, &bidExt); err != nil {
   180  				errs = append(errs, err)
   181  				continue
   182  			}
   183  
   184  			bidType, err := getMediaTypeForBid(&bidExt)
   185  			if err != nil {
   186  				errs = append(errs, err)
   187  				continue
   188  			}
   189  
   190  			iabCategory, found := a.findIabCategoryForBid(&bidExt)
   191  			if found {
   192  				bid.Cat = []string{iabCategory}
   193  			} else if len(bid.Cat) > 1 {
   194  				//create empty categories array to force bid to be rejected
   195  				bid.Cat = []string{}
   196  			}
   197  
   198  			bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{
   199  				Bid:          &bid,
   200  				BidType:      bidType,
   201  				BidVideo:     &openrtb_ext.ExtBidPrebidVideo{Duration: bidExt.Appnexus.CreativeInfo.Video.Duration},
   202  				DealPriority: bidExt.Appnexus.DealPriority,
   203  			})
   204  		}
   205  	}
   206  
   207  	if appnexusResponse.Cur != "" {
   208  		bidderResponse.Currency = appnexusResponse.Cur
   209  	}
   210  
   211  	return bidderResponse, errs
   212  }
   213  
   214  func getRequestExt(ext json.RawMessage) (map[string]json.RawMessage, error) {
   215  	extMap := make(map[string]json.RawMessage)
   216  
   217  	if len(ext) > 0 {
   218  		if err := json.Unmarshal(ext, &extMap); err != nil {
   219  			return nil, err
   220  		}
   221  	}
   222  
   223  	return extMap, nil
   224  }
   225  
   226  func (a *adapter) getAppnexusExt(extMap map[string]json.RawMessage, isAMP int, isVIDEO int) (bidReqExtAppnexus, error) {
   227  	var appnexusExt bidReqExtAppnexus
   228  
   229  	if appnexusExtJson, exists := extMap["appnexus"]; exists && len(appnexusExtJson) > 0 {
   230  		if err := json.Unmarshal(appnexusExtJson, &appnexusExt); err != nil {
   231  			return appnexusExt, err
   232  		}
   233  	}
   234  
   235  	if prebidJson, exists := extMap["prebid"]; exists {
   236  		_, valueType, _, err := jsonparser.Get(prebidJson, "targeting", "includebrandcategory")
   237  		if err != nil && !errors.Is(err, jsonparser.KeyPathNotFoundError) {
   238  			return appnexusExt, err
   239  		}
   240  
   241  		if valueType == jsonparser.Object {
   242  			appnexusExt.BrandCategoryUniqueness = ptrutil.ToPtr(true)
   243  			appnexusExt.IncludeBrandCategory = ptrutil.ToPtr(true)
   244  		}
   245  	}
   246  
   247  	appnexusExt.IsAMP = isAMP
   248  	appnexusExt.HeaderBiddingSource = a.hbSource + isVIDEO
   249  
   250  	return appnexusExt, nil
   251  }
   252  
   253  func validateAndBuildAppNexusExt(imp *openrtb2.Imp) (openrtb_ext.ExtImpAppnexus, error) {
   254  	var bidderExt adapters.ExtImpBidder
   255  	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
   256  		return openrtb_ext.ExtImpAppnexus{}, err
   257  	}
   258  
   259  	var appnexusExt openrtb_ext.ExtImpAppnexus
   260  	if err := json.Unmarshal(bidderExt.Bidder, &appnexusExt); err != nil {
   261  		return openrtb_ext.ExtImpAppnexus{}, err
   262  	}
   263  
   264  	handleLegacyParams(&appnexusExt)
   265  
   266  	if err := validateAppnexusExt(&appnexusExt); err != nil {
   267  		return openrtb_ext.ExtImpAppnexus{}, err
   268  	}
   269  
   270  	return appnexusExt, nil
   271  }
   272  
   273  func handleLegacyParams(appnexusExt *openrtb_ext.ExtImpAppnexus) {
   274  	if appnexusExt.PlacementId == 0 && appnexusExt.DeprecatedPlacementId != 0 {
   275  		appnexusExt.PlacementId = appnexusExt.DeprecatedPlacementId
   276  	}
   277  	if appnexusExt.InvCode == "" && appnexusExt.LegacyInvCode != "" {
   278  		appnexusExt.InvCode = appnexusExt.LegacyInvCode
   279  	}
   280  	if appnexusExt.TrafficSourceCode == "" && appnexusExt.LegacyTrafficSourceCode != "" {
   281  		appnexusExt.TrafficSourceCode = appnexusExt.LegacyTrafficSourceCode
   282  	}
   283  	if appnexusExt.UsePaymentRule == nil && appnexusExt.DeprecatedUsePaymentRule != nil {
   284  		appnexusExt.UsePaymentRule = appnexusExt.DeprecatedUsePaymentRule
   285  	}
   286  }
   287  
   288  func groupByPods(imps []openrtb2.Imp) map[string]([]openrtb2.Imp) {
   289  	// find number of pods in response
   290  	podImps := make(map[string][]openrtb2.Imp)
   291  	for _, imp := range imps {
   292  		pod := strings.Split(imp.ID, "_")[0]
   293  		podImps[pod] = append(podImps[pod], imp)
   294  	}
   295  	return podImps
   296  }
   297  
   298  func splitRequests(imps []openrtb2.Imp, request *openrtb2.BidRequest, requestExt map[string]json.RawMessage, requestExtAppnexus bidReqExtAppnexus, uri string) ([]*adapters.RequestData, []error) {
   299  	var errs []error
   300  	// Initial capacity for future array of requests, memory optimization.
   301  	// Let's say there are 35 impressions and limit impressions per request equals to 10.
   302  	// In this case we need to create 4 requests with 10, 10, 10 and 5 impressions.
   303  	// With this formula initial capacity=(35+10-1)/10 = 4
   304  	initialCapacity := (len(imps) + maxImpsPerReq - 1) / maxImpsPerReq
   305  	resArr := make([]*adapters.RequestData, 0, initialCapacity)
   306  	startInd := 0
   307  	impsLeft := len(imps) > 0
   308  
   309  	headers := http.Header{}
   310  	headers.Add("Content-Type", "application/json;charset=utf-8")
   311  	headers.Add("Accept", "application/json")
   312  
   313  	appnexusExtJson, err := json.Marshal(requestExtAppnexus)
   314  	if err != nil {
   315  		errs = append(errs, err)
   316  	}
   317  
   318  	requestExtClone := maputil.Clone(requestExt)
   319  	requestExtClone["appnexus"] = appnexusExtJson
   320  
   321  	request.Ext, err = json.Marshal(requestExtClone)
   322  	if err != nil {
   323  		errs = append(errs, err)
   324  	}
   325  
   326  	for impsLeft {
   327  		endInd := startInd + maxImpsPerReq
   328  		if endInd >= len(imps) {
   329  			endInd = len(imps)
   330  			impsLeft = false
   331  		}
   332  		impsForReq := imps[startInd:endInd]
   333  		request.Imp = impsForReq
   334  
   335  		reqJSON, err := json.Marshal(request)
   336  		if err != nil {
   337  			errs = append(errs, err)
   338  			return nil, errs
   339  		}
   340  
   341  		resArr = append(resArr, &adapters.RequestData{
   342  			Method:  "POST",
   343  			Uri:     uri,
   344  			Body:    reqJSON,
   345  			Headers: headers,
   346  			ImpIDs:  openrtb_ext.GetImpIDs(request.Imp),
   347  		})
   348  		startInd = endInd
   349  	}
   350  	return resArr, errs
   351  }
   352  
   353  func validateAppnexusExt(appnexusExt *openrtb_ext.ExtImpAppnexus) error {
   354  	if appnexusExt.PlacementId == 0 && (appnexusExt.InvCode == "" || appnexusExt.Member == "") {
   355  		return &errortypes.BadInput{
   356  			Message: "No placement or member+invcode provided",
   357  		}
   358  	}
   359  	return nil
   360  }
   361  
   362  func buildRequestImp(imp *openrtb2.Imp, appnexusExt *openrtb_ext.ExtImpAppnexus, displayManagerVer string) error {
   363  	if appnexusExt.InvCode != "" {
   364  		imp.TagID = appnexusExt.InvCode
   365  	}
   366  
   367  	if imp.BidFloor <= 0 && appnexusExt.Reserve > 0 {
   368  		imp.BidFloor = appnexusExt.Reserve // This will be broken for non-USD currency.
   369  	}
   370  
   371  	if imp.Banner != nil {
   372  		bannerCopy := *imp.Banner
   373  		if appnexusExt.Position == "above" {
   374  			bannerCopy.Pos = adcom1.PositionAboveFold.Ptr()
   375  		} else if appnexusExt.Position == "below" {
   376  			bannerCopy.Pos = adcom1.PositionBelowFold.Ptr()
   377  		}
   378  
   379  		if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 {
   380  			firstFormat := bannerCopy.Format[0]
   381  			bannerCopy.W = &(firstFormat.W)
   382  			bannerCopy.H = &(firstFormat.H)
   383  		}
   384  		imp.Banner = &bannerCopy
   385  	}
   386  
   387  	// Populate imp.displaymanagerver if the SDK failed to do it.
   388  	if len(imp.DisplayManagerVer) == 0 && len(displayManagerVer) > 0 {
   389  		imp.DisplayManagerVer = displayManagerVer
   390  	}
   391  
   392  	impExt := impExt{Appnexus: impExtAppnexus{
   393  		PlacementID:       int(appnexusExt.PlacementId),
   394  		TrafficSourceCode: appnexusExt.TrafficSourceCode,
   395  		Keywords:          appnexusExt.Keywords.String(),
   396  		UsePmtRule:        appnexusExt.UsePaymentRule,
   397  		PrivateSizes:      appnexusExt.PrivateSizes,
   398  		ExtInvCode:        appnexusExt.ExtInvCode,
   399  		ExternalImpID:     appnexusExt.ExternalImpId,
   400  	}}
   401  
   402  	var err error
   403  	imp.Ext, err = json.Marshal(&impExt)
   404  
   405  	return err
   406  }
   407  
   408  // getMediaTypeForBid determines which type of bid.
   409  func getMediaTypeForBid(bid *bidExt) (openrtb_ext.BidType, error) {
   410  	switch bid.Appnexus.BidType {
   411  	case 0:
   412  		return openrtb_ext.BidTypeBanner, nil
   413  	case 1:
   414  		return openrtb_ext.BidTypeVideo, nil
   415  	case 3:
   416  		return openrtb_ext.BidTypeNative, nil
   417  	default:
   418  		return "", fmt.Errorf("Unrecognized bid_ad_type in response from appnexus: %d", bid.Appnexus.BidType)
   419  	}
   420  }
   421  
   422  // getIabCategoryForBid maps an appnexus brand id to an IAB category.
   423  func (a *adapter) findIabCategoryForBid(bid *bidExt) (string, bool) {
   424  	brandIDString := strconv.Itoa(bid.Appnexus.BrandCategory)
   425  	iabCategory, ok := iabCategoryMap[brandIDString]
   426  	return iabCategory, ok
   427  }
   428  
   429  func appendMemberId(uri url.URL, memberId string) url.URL {
   430  	q := uri.Query()
   431  	q.Set("member_id", memberId)
   432  	uri.RawQuery = q.Encode()
   433  	return uri
   434  }
   435  
   436  func buildDisplayManageVer(req *openrtb2.BidRequest) string {
   437  	if req.App == nil {
   438  		return ""
   439  	}
   440  
   441  	source, err := jsonparser.GetString(req.App.Ext, openrtb_ext.PrebidExtKey, "source")
   442  	if err != nil {
   443  		return ""
   444  	}
   445  
   446  	version, err := jsonparser.GetString(req.App.Ext, openrtb_ext.PrebidExtKey, "version")
   447  	if err != nil {
   448  		return ""
   449  	}
   450  
   451  	return fmt.Sprintf("%s-%s", source, version)
   452  }
   453  
   454  // moveSupplyChain moves the supply chain object from source.ext.schain to ext.schain.
   455  func moveSupplyChain(request *openrtb2.BidRequest, extMap map[string]json.RawMessage) error {
   456  	if request == nil || request.Source == nil || len(request.Source.Ext) == 0 {
   457  		return nil
   458  	}
   459  
   460  	sourceExtMap := make(map[string]json.RawMessage)
   461  	if err := json.Unmarshal(request.Source.Ext, &sourceExtMap); err != nil {
   462  		return err
   463  	}
   464  
   465  	schainJson, exists := sourceExtMap["schain"]
   466  	if !exists {
   467  		return nil
   468  	}
   469  
   470  	delete(sourceExtMap, "schain")
   471  
   472  	request.Source = ptrutil.Clone(request.Source)
   473  
   474  	if len(sourceExtMap) > 0 {
   475  		ext, err := json.Marshal(sourceExtMap)
   476  		if err != nil {
   477  			return err
   478  		}
   479  		request.Source.Ext = ext
   480  	} else {
   481  		request.Source.Ext = nil
   482  	}
   483  
   484  	extMap["schain"] = schainJson
   485  
   486  	return nil
   487  }
   488  
   489  func (a *adapter) buildAdPodRequests(imps []openrtb2.Imp, request *openrtb2.BidRequest, requestExt map[string]json.RawMessage, requestExtAppnexus bidReqExtAppnexus, uri string) ([]*adapters.RequestData, []error) {
   490  	var errs []error
   491  	podImps := groupByPods(imps)
   492  	requests := make([]*adapters.RequestData, 0, len(podImps))
   493  	for _, podImps := range podImps {
   494  		requestExtAppnexus.AdPodID = fmt.Sprint(a.randomGenerator.GenerateInt63())
   495  
   496  		reqs, errors := splitRequests(podImps, request, requestExt, requestExtAppnexus, uri)
   497  		requests = append(requests, reqs...)
   498  		errs = append(errs, errors...)
   499  	}
   500  
   501  	return requests, errs
   502  }