github.com/prebid/prebid-server/v2@v2.18.0/exchange/auction.go (about)

     1  package exchange
     2  
     3  import (
     4  	"context"
     5  	"encoding/xml"
     6  	"errors"
     7  	"fmt"
     8  	"regexp"
     9  	"sort"
    10  	"strings"
    11  	"time"
    12  
    13  	uuid "github.com/gofrs/uuid"
    14  	"github.com/prebid/openrtb/v20/openrtb2"
    15  	"github.com/prebid/prebid-server/v2/config"
    16  	"github.com/prebid/prebid-server/v2/exchange/entities"
    17  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    18  	"github.com/prebid/prebid-server/v2/prebid_cache_client"
    19  	"github.com/prebid/prebid-server/v2/util/jsonutil"
    20  )
    21  
    22  const (
    23  	DebugOverrideHeader string = "x-pbs-debug-override"
    24  )
    25  
    26  type DebugLog struct {
    27  	Enabled       bool
    28  	CacheType     prebid_cache_client.PayloadType
    29  	Data          DebugData
    30  	TTL           int64
    31  	CacheKey      string
    32  	CacheString   string
    33  	Regexp        *regexp.Regexp
    34  	DebugOverride bool
    35  	//little optimization, it stores value of debugLog.Enabled || debugLog.DebugOverride
    36  	DebugEnabledOrOverridden bool
    37  }
    38  
    39  type DebugData struct {
    40  	Request  string
    41  	Headers  string
    42  	Response string
    43  }
    44  
    45  func (d *DebugLog) BuildCacheString() {
    46  	if d.Regexp != nil {
    47  		d.Data.Request = fmt.Sprintf(d.Regexp.ReplaceAllString(d.Data.Request, ""))
    48  		d.Data.Headers = fmt.Sprintf(d.Regexp.ReplaceAllString(d.Data.Headers, ""))
    49  		d.Data.Response = fmt.Sprintf(d.Regexp.ReplaceAllString(d.Data.Response, ""))
    50  	}
    51  
    52  	d.Data.Request = fmt.Sprintf("<Request>%s</Request>", d.Data.Request)
    53  	d.Data.Headers = fmt.Sprintf("<Headers>%s</Headers>", d.Data.Headers)
    54  	d.Data.Response = fmt.Sprintf("<Response>%s</Response>", d.Data.Response)
    55  
    56  	d.CacheString = fmt.Sprintf("%s<Log>%s%s%s</Log>", xml.Header, d.Data.Request, d.Data.Headers, d.Data.Response)
    57  }
    58  
    59  func IsDebugOverrideEnabled(debugHeader, configOverrideToken string) bool {
    60  	return configOverrideToken != "" && debugHeader == configOverrideToken
    61  }
    62  
    63  func (d *DebugLog) PutDebugLogError(cache prebid_cache_client.Client, timeout int, errors []error) error {
    64  	if len(d.Data.Response) == 0 && len(errors) == 0 {
    65  		d.Data.Response = "No response or errors created"
    66  	}
    67  
    68  	if len(errors) > 0 {
    69  		errStrings := []string{}
    70  		for _, err := range errors {
    71  			errStrings = append(errStrings, err.Error())
    72  		}
    73  		d.Data.Response = fmt.Sprintf("%s\nErrors:\n%s", d.Data.Response, strings.Join(errStrings, "\n"))
    74  	}
    75  
    76  	d.BuildCacheString()
    77  
    78  	if len(d.CacheKey) == 0 {
    79  		rawUUID, err := uuid.NewV4()
    80  		if err != nil {
    81  			return err
    82  		}
    83  		d.CacheKey = rawUUID.String()
    84  	}
    85  
    86  	data, err := jsonutil.Marshal(d.CacheString)
    87  	if err != nil {
    88  		return err
    89  	}
    90  
    91  	toCache := []prebid_cache_client.Cacheable{
    92  		{
    93  			Type:       d.CacheType,
    94  			Data:       data,
    95  			TTLSeconds: d.TTL,
    96  			Key:        "log_" + d.CacheKey,
    97  		},
    98  	}
    99  
   100  	if cache != nil {
   101  		ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Duration(timeout)*time.Millisecond))
   102  		defer cancel()
   103  		cache.PutJson(ctx, toCache)
   104  	}
   105  
   106  	return nil
   107  }
   108  
   109  func newAuction(seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, numImps int, preferDeals bool) *auction {
   110  	winningBids := make(map[string]*entities.PbsOrtbBid, numImps)
   111  	allBidsByBidder := make(map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid, numImps)
   112  
   113  	for bidderName, seatBid := range seatBids {
   114  		if seatBid != nil {
   115  			for _, bid := range seatBid.Bids {
   116  				wbid, ok := winningBids[bid.Bid.ImpID]
   117  				if !ok || isNewWinningBid(bid.Bid, wbid.Bid, preferDeals) {
   118  					winningBids[bid.Bid.ImpID] = bid
   119  				}
   120  
   121  				if bidMap, ok := allBidsByBidder[bid.Bid.ImpID]; ok {
   122  					bidMap[bidderName] = append(bidMap[bidderName], bid)
   123  				} else {
   124  					allBidsByBidder[bid.Bid.ImpID] = map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
   125  						bidderName: {bid},
   126  					}
   127  				}
   128  			}
   129  		}
   130  	}
   131  
   132  	return &auction{
   133  		winningBids:     winningBids,
   134  		allBidsByBidder: allBidsByBidder,
   135  	}
   136  }
   137  
   138  // isNewWinningBid calculates if the new bid (nbid) will win against the current winning bid (wbid) given preferDeals.
   139  func isNewWinningBid(bid, wbid *openrtb2.Bid, preferDeals bool) bool {
   140  	if preferDeals {
   141  		if len(wbid.DealID) > 0 && len(bid.DealID) == 0 {
   142  			return false
   143  		}
   144  		if len(wbid.DealID) == 0 && len(bid.DealID) > 0 {
   145  			return true
   146  		}
   147  	}
   148  	return bid.Price > wbid.Price
   149  }
   150  
   151  func (a *auction) validateAndUpdateMultiBid(adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, preferDeals bool, accountDefaultBidLimit int) {
   152  	bidsSnipped := false
   153  	// sort bids for multibid targeting
   154  	for _, topBidsPerBidder := range a.allBidsByBidder {
   155  		for bidder, topBids := range topBidsPerBidder {
   156  			sort.Slice(topBids, func(i, j int) bool {
   157  				return isNewWinningBid(topBids[i].Bid, topBids[j].Bid, preferDeals)
   158  			})
   159  
   160  			// assert hard limit on bids count per imp, per adapter.
   161  			if accountDefaultBidLimit != 0 && len(topBids) > accountDefaultBidLimit {
   162  				for i := accountDefaultBidLimit; i < len(topBids); i++ {
   163  					topBids[i].Bid = nil
   164  					topBids[i] = nil
   165  					bidsSnipped = true
   166  				}
   167  
   168  				topBidsPerBidder[bidder] = topBids[:accountDefaultBidLimit]
   169  			}
   170  		}
   171  	}
   172  
   173  	if bidsSnipped { // remove the marked bids from original references
   174  		for _, seatBid := range adapterBids {
   175  			if seatBid != nil {
   176  				bids := make([]*entities.PbsOrtbBid, 0, accountDefaultBidLimit)
   177  				for i := 0; i < len(seatBid.Bids); i++ {
   178  					if seatBid.Bids[i].Bid != nil {
   179  						bids = append(bids, seatBid.Bids[i])
   180  					}
   181  				}
   182  				seatBid.Bids = bids
   183  			}
   184  		}
   185  	}
   186  }
   187  
   188  func (a *auction) setRoundedPrices(targetingData targetData) {
   189  	roundedPrices := make(map[*entities.PbsOrtbBid]string, 5*len(a.winningBids))
   190  	for _, topBidsPerImp := range a.allBidsByBidder {
   191  		for _, topBidsPerBidder := range topBidsPerImp {
   192  			for _, topBid := range topBidsPerBidder {
   193  				roundedPrices[topBid] = GetPriceBucket(*topBid.Bid, targetingData)
   194  			}
   195  		}
   196  	}
   197  	a.roundedPrices = roundedPrices
   198  }
   199  
   200  func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, targData *targetData, evTracking *eventTracking, bidRequest *openrtb2.BidRequest, ttlBuffer int64, defaultTTLs *config.DefaultTTLs, bidCategory map[string]string, debugLog *DebugLog) []error {
   201  	var bids, vast, includeBidderKeys, includeWinners bool = targData.includeCacheBids, targData.includeCacheVast, targData.includeBidderKeys, targData.includeWinners
   202  	if !((bids || vast) && (includeBidderKeys || includeWinners)) {
   203  		return nil
   204  	}
   205  	var errs []error
   206  	expectNumBids := valOrZero(bids, len(a.roundedPrices))
   207  	expectNumVast := valOrZero(vast, len(a.roundedPrices))
   208  	bidIndices := make(map[int]*openrtb2.Bid, expectNumBids)
   209  	vastIndices := make(map[int]*openrtb2.Bid, expectNumVast)
   210  	toCache := make([]prebid_cache_client.Cacheable, 0, expectNumBids+expectNumVast)
   211  	expByImp := make(map[string]int64)
   212  	competitiveExclusion := false
   213  	var hbCacheID string
   214  	if len(bidCategory) > 0 {
   215  		// assert:  category of winning bids never duplicated
   216  		if rawUuid, err := uuid.NewV4(); err == nil {
   217  			hbCacheID = rawUuid.String()
   218  			competitiveExclusion = true
   219  		} else {
   220  			errs = append(errs, errors.New("failed to create custom cache key"))
   221  		}
   222  	}
   223  
   224  	// Grab the imp TTLs
   225  	for _, imp := range bidRequest.Imp {
   226  		expByImp[imp.ID] = imp.Exp
   227  	}
   228  	for impID, topBidsPerImp := range a.allBidsByBidder {
   229  		for bidderName, topBidsPerBidder := range topBidsPerImp {
   230  			for _, topBid := range topBidsPerBidder {
   231  				isOverallWinner := a.winningBids[impID] == topBid
   232  				if !includeBidderKeys && !isOverallWinner {
   233  					continue
   234  				}
   235  				var customCacheKey string
   236  				var catDur string
   237  				useCustomCacheKey := false
   238  				if competitiveExclusion && isOverallWinner || includeBidderKeys {
   239  					// set custom cache key for winning bid when competitive exclusion applies
   240  					catDur = bidCategory[topBid.Bid.ID]
   241  					if len(catDur) > 0 {
   242  						customCacheKey = fmt.Sprintf("%s_%s", catDur, hbCacheID)
   243  						useCustomCacheKey = true
   244  					}
   245  				}
   246  				if bids {
   247  					if jsonBytes, err := jsonutil.Marshal(topBid.Bid); err == nil {
   248  						jsonBytes, err = evTracking.modifyBidJSON(topBid, bidderName, jsonBytes)
   249  						if err != nil {
   250  							errs = append(errs, err)
   251  						}
   252  						if useCustomCacheKey {
   253  							// not allowed if bids is true; log error and cache normally
   254  							errs = append(errs, errors.New("cannot use custom cache key for non-vast bids"))
   255  						}
   256  						toCache = append(toCache, prebid_cache_client.Cacheable{
   257  							Type:       prebid_cache_client.TypeJSON,
   258  							Data:       jsonBytes,
   259  							TTLSeconds: cacheTTL(expByImp[impID], topBid.Bid.Exp, defTTL(topBid.BidType, defaultTTLs), ttlBuffer),
   260  						})
   261  						bidIndices[len(toCache)-1] = topBid.Bid
   262  					} else {
   263  						errs = append(errs, err)
   264  					}
   265  				}
   266  				if vast && topBid.BidType == openrtb_ext.BidTypeVideo {
   267  					vastXML := makeVAST(topBid.Bid)
   268  					if jsonBytes, err := jsonutil.Marshal(vastXML); err == nil {
   269  						if useCustomCacheKey {
   270  							toCache = append(toCache, prebid_cache_client.Cacheable{
   271  								Type:       prebid_cache_client.TypeXML,
   272  								Data:       jsonBytes,
   273  								TTLSeconds: cacheTTL(expByImp[impID], topBid.Bid.Exp, defTTL(topBid.BidType, defaultTTLs), ttlBuffer),
   274  								Key:        customCacheKey,
   275  							})
   276  						} else {
   277  							toCache = append(toCache, prebid_cache_client.Cacheable{
   278  								Type:       prebid_cache_client.TypeXML,
   279  								Data:       jsonBytes,
   280  								TTLSeconds: cacheTTL(expByImp[impID], topBid.Bid.Exp, defTTL(topBid.BidType, defaultTTLs), ttlBuffer),
   281  							})
   282  						}
   283  						vastIndices[len(toCache)-1] = topBid.Bid
   284  					} else {
   285  						errs = append(errs, err)
   286  					}
   287  				}
   288  			}
   289  		}
   290  	}
   291  
   292  	if len(toCache) > 0 && debugLog != nil && debugLog.DebugEnabledOrOverridden {
   293  		debugLog.CacheKey = hbCacheID
   294  		debugLog.BuildCacheString()
   295  		if jsonBytes, err := jsonutil.Marshal(debugLog.CacheString); err == nil {
   296  			toCache = append(toCache, prebid_cache_client.Cacheable{
   297  				Type:       debugLog.CacheType,
   298  				Data:       jsonBytes,
   299  				TTLSeconds: debugLog.TTL,
   300  				Key:        "log_" + debugLog.CacheKey,
   301  			})
   302  		}
   303  	}
   304  
   305  	ids, err := cache.PutJson(ctx, toCache)
   306  	if err != nil {
   307  		errs = append(errs, err...)
   308  	}
   309  
   310  	if bids {
   311  		a.cacheIds = make(map[*openrtb2.Bid]string, len(bidIndices))
   312  		for index, bid := range bidIndices {
   313  			if ids[index] != "" {
   314  				a.cacheIds[bid] = ids[index]
   315  			}
   316  		}
   317  	}
   318  	if vast {
   319  		a.vastCacheIds = make(map[*openrtb2.Bid]string, len(vastIndices))
   320  		for index, bid := range vastIndices {
   321  			if ids[index] != "" {
   322  				if competitiveExclusion && strings.HasSuffix(ids[index], hbCacheID) {
   323  					// omit the pb_cat_dur_ portion of cache ID
   324  					a.vastCacheIds[bid] = hbCacheID
   325  				} else {
   326  					a.vastCacheIds[bid] = ids[index]
   327  				}
   328  			}
   329  		}
   330  	}
   331  	return errs
   332  }
   333  
   334  // makeVAST returns some VAST XML for the given bid. If AdM is defined,
   335  // it takes precedence. Otherwise the Nurl will be wrapped in a redirect tag.
   336  func makeVAST(bid *openrtb2.Bid) string {
   337  	if bid.AdM == "" {
   338  		return `<VAST version="3.0"><Ad><Wrapper>` +
   339  			`<AdSystem>prebid.org wrapper</AdSystem>` +
   340  			`<VASTAdTagURI><![CDATA[` + bid.NURL + `]]></VASTAdTagURI>` +
   341  			`<Impression></Impression><Creatives></Creatives>` +
   342  			`</Wrapper></Ad></VAST>`
   343  	}
   344  	return bid.AdM
   345  }
   346  
   347  func valOrZero(useVal bool, val int) int {
   348  	if useVal {
   349  		return val
   350  	}
   351  	return 0
   352  }
   353  
   354  func cacheTTL(impTTL int64, bidTTL int64, defTTL int64, buffer int64) (ttl int64) {
   355  	if impTTL <= 0 && bidTTL <= 0 {
   356  		// Only use default if there is no imp nor bid TTL provided. We don't want the default
   357  		// to cut short a requested longer TTL.
   358  		return addBuffer(defTTL, buffer)
   359  	}
   360  	if impTTL <= 0 {
   361  		// Use <= to handle the case of someone sending a negative ttl. We treat it as zero
   362  		return addBuffer(bidTTL, buffer)
   363  	}
   364  	if bidTTL <= 0 {
   365  		return addBuffer(impTTL, buffer)
   366  	}
   367  	if impTTL < bidTTL {
   368  		return addBuffer(impTTL, buffer)
   369  	}
   370  	return addBuffer(bidTTL, buffer)
   371  }
   372  
   373  func addBuffer(base int64, buffer int64) int64 {
   374  	if base <= 0 {
   375  		return 0
   376  	}
   377  	return base + buffer
   378  }
   379  
   380  func defTTL(bidType openrtb_ext.BidType, defaultTTLs *config.DefaultTTLs) (ttl int64) {
   381  	switch bidType {
   382  	case openrtb_ext.BidTypeBanner:
   383  		return int64(defaultTTLs.Banner)
   384  	case openrtb_ext.BidTypeVideo:
   385  		return int64(defaultTTLs.Video)
   386  	case openrtb_ext.BidTypeNative:
   387  		return int64(defaultTTLs.Native)
   388  	case openrtb_ext.BidTypeAudio:
   389  		return int64(defaultTTLs.Audio)
   390  	}
   391  	return 0
   392  }
   393  
   394  type auction struct {
   395  	// winningBids is a map from imp.id to the highest overall CPM bid in that imp.
   396  	winningBids map[string]*entities.PbsOrtbBid
   397  	// allBidsByBidder is map from ImpID to another map that maps bidderName to all bids from that bidder.
   398  	allBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid
   399  	// roundedPrices stores the price strings rounded for each bid according to the price granularity.
   400  	roundedPrices map[*entities.PbsOrtbBid]string
   401  	// cacheIds stores the UUIDs from Prebid Cache for fetching the full bid JSON.
   402  	cacheIds map[*openrtb2.Bid]string
   403  	// vastCacheIds stores UUIDS from Prebid cache for fetching the VAST markup to video bids.
   404  	vastCacheIds map[*openrtb2.Bid]string
   405  }