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 }