github.com/prebid/prebid-server/v2@v2.18.0/floors/rule.go (about) 1 package floors 2 3 import ( 4 "fmt" 5 "math/bits" 6 "regexp" 7 "sort" 8 "strings" 9 10 "github.com/golang/glog" 11 "github.com/prebid/openrtb/v20/openrtb2" 12 "github.com/prebid/prebid-server/v2/currency" 13 "github.com/prebid/prebid-server/v2/openrtb_ext" 14 "github.com/prebid/prebid-server/v2/util/ptrutil" 15 ) 16 17 const ( 18 SiteDomain string = "siteDomain" 19 PubDomain string = "pubDomain" 20 Domain string = "domain" 21 Bundle string = "bundle" 22 Channel string = "channel" 23 MediaType string = "mediaType" 24 Size string = "size" 25 GptSlot string = "gptSlot" 26 AdUnitCode string = "adUnitCode" 27 Country string = "country" 28 DeviceType string = "deviceType" 29 Tablet string = "tablet" 30 Desktop string = "desktop" 31 Phone string = "phone" 32 BannerMedia string = "banner" 33 VideoMedia string = "video" 34 VideoOutstreamMedia string = "video-outstream" 35 AudioMedia string = "audio" 36 NativeMedia string = "native" 37 ) 38 39 // getFloorCurrency returns floors currency provided in floors JSON, 40 // if currency is not provided then defaults to USD 41 func getFloorCurrency(floorExt *openrtb_ext.PriceFloorRules) string { 42 floorCur := defaultCurrency 43 if floorExt != nil && floorExt.Data != nil { 44 if floorExt.Data.Currency != "" { 45 floorCur = floorExt.Data.Currency 46 } 47 48 if len(floorExt.Data.ModelGroups) > 0 && floorExt.Data.ModelGroups[0].Currency != "" { 49 floorCur = floorExt.Data.ModelGroups[0].Currency 50 } 51 } 52 53 return floorCur 54 } 55 56 // getMinFloorValue returns floorMin and floorMinCur, 57 // values provided in impression extension are considered over floors JSON. 58 func getMinFloorValue(floorExt *openrtb_ext.PriceFloorRules, imp *openrtb_ext.ImpWrapper, conversions currency.Conversions) (float64, string, error) { 59 var err error 60 var rate float64 61 var floorCur string 62 floorMin := roundToFourDecimals(floorExt.FloorMin) 63 floorMinCur := floorExt.FloorMinCur 64 65 impFloorMin, impFloorCur, err := getFloorMinAndCurFromImp(imp) 66 if err == nil { 67 if impFloorMin > 0.0 { 68 floorMin = impFloorMin 69 } 70 if impFloorCur != "" { 71 floorMinCur = impFloorCur 72 } 73 74 floorCur = getFloorCurrency(floorExt) 75 if floorMin > 0.0 && floorMinCur != "" { 76 if floorExt.FloorMinCur != "" && impFloorCur != "" && floorExt.FloorMinCur != impFloorCur { 77 glog.Warning("FloorMinCur are different in floorExt and ImpExt") 78 } 79 if floorCur != "" && floorMinCur != floorCur { 80 rate, err = conversions.GetRate(floorMinCur, floorCur) 81 floorMin = rate * floorMin 82 } 83 } 84 floorMin = roundToFourDecimals(floorMin) 85 } 86 if err != nil { 87 return floorMin, floorCur, fmt.Errorf("Error in getting FloorMin value : '%v'", err.Error()) 88 } else { 89 return floorMin, floorCur, err 90 } 91 } 92 93 // getFloorMinAndCurFromImp returns floorMin and floorMinCur from impression extension 94 func getFloorMinAndCurFromImp(imp *openrtb_ext.ImpWrapper) (float64, string, error) { 95 var floorMin float64 96 var floorMinCur string 97 98 impExt, err := imp.GetImpExt() 99 if impExt != nil { 100 impExtPrebid := impExt.GetPrebid() 101 if impExtPrebid != nil && impExtPrebid.Floors != nil { 102 if impExtPrebid.Floors.FloorMin > 0.0 { 103 floorMin = impExtPrebid.Floors.FloorMin 104 } 105 106 if impExtPrebid.Floors.FloorMinCur != "" { 107 floorMinCur = impExtPrebid.Floors.FloorMinCur 108 } 109 } 110 } 111 return floorMin, floorMinCur, err 112 } 113 114 // updateImpExtWithFloorDetails updates floors related details into imp.ext.prebid.floors 115 func updateImpExtWithFloorDetails(imp *openrtb_ext.ImpWrapper, matchedRule string, floorRuleVal, floorVal float64) error { 116 impExt, err := imp.GetImpExt() 117 if err != nil { 118 return err 119 } 120 extImpPrebid := impExt.GetPrebid() 121 if extImpPrebid == nil { 122 extImpPrebid = &openrtb_ext.ExtImpPrebid{} 123 } 124 extImpPrebid.Floors = &openrtb_ext.ExtImpPrebidFloors{ 125 FloorRule: matchedRule, 126 FloorRuleValue: floorRuleVal, 127 FloorValue: floorVal, 128 } 129 impExt.SetPrebid(extImpPrebid) 130 return err 131 } 132 133 // selectFloorModelGroup selects one modelgroup based on modelweight out of multiple modelgroups, if provided into floors JSON. 134 func selectFloorModelGroup(modelGroups []openrtb_ext.PriceFloorModelGroup, f func(int) int) []openrtb_ext.PriceFloorModelGroup { 135 totalModelWeight := 0 136 for i := 0; i < len(modelGroups); i++ { 137 if modelGroups[i].ModelWeight == nil { 138 modelGroups[i].ModelWeight = new(int) 139 *modelGroups[i].ModelWeight = 1 140 } 141 totalModelWeight += *modelGroups[i].ModelWeight 142 143 } 144 145 sort.SliceStable(modelGroups, func(i, j int) bool { 146 if modelGroups[i].ModelWeight != nil && modelGroups[j].ModelWeight != nil { 147 return *modelGroups[i].ModelWeight < *modelGroups[j].ModelWeight 148 } 149 return false 150 }) 151 152 winWeight := f(totalModelWeight + 1) 153 for i, modelGroup := range modelGroups { 154 winWeight -= *modelGroup.ModelWeight 155 if winWeight <= 0 { 156 modelGroups[0], modelGroups[i] = modelGroups[i], modelGroups[0] 157 return modelGroups[:1] 158 } 159 160 } 161 return modelGroups[:1] 162 } 163 164 // shouldSkipFloors returns flag to decide skipping of floors singalling based on skipRate provided 165 func shouldSkipFloors(ModelGroupsSkipRate, DataSkipRate, RootSkipRate int, f func(int) int) bool { 166 skipRate := 0 167 168 if ModelGroupsSkipRate > 0 { 169 skipRate = ModelGroupsSkipRate 170 } else if DataSkipRate > 0 { 171 skipRate = DataSkipRate 172 } else { 173 skipRate = RootSkipRate 174 } 175 176 if skipRate == 0 { 177 return false 178 } 179 return skipRate >= f(skipRateMax+1) 180 } 181 182 // findRule prepares rule combinations based on schema dimensions provided in floors data, request values associated with these fields and 183 // does matching with rules provided in floors data and returns matched rule 184 func findRule(ruleValues map[string]float64, delimiter string, desiredRuleKey []string) (string, bool) { 185 ruleKeys := prepareRuleCombinations(desiredRuleKey, delimiter) 186 for i := 0; i < len(ruleKeys); i++ { 187 if _, ok := ruleValues[ruleKeys[i]]; ok { 188 return ruleKeys[i], true 189 } 190 } 191 return "", false 192 } 193 194 // createRuleKey prepares rule keys based on schema dimension and values present in request 195 func createRuleKey(floorSchema openrtb_ext.PriceFloorSchema, request *openrtb_ext.RequestWrapper, imp *openrtb_ext.ImpWrapper) []string { 196 var ruleKeys []string 197 198 for _, field := range floorSchema.Fields { 199 value := catchAll 200 switch field { 201 case MediaType: 202 value = getMediaType(imp.Imp) 203 case Size: 204 value = getSizeValue(imp.Imp) 205 case Domain: 206 value = getDomain(request) 207 case SiteDomain: 208 value = getSiteDomain(request) 209 case Bundle: 210 value = getBundle(request) 211 case PubDomain: 212 value = getPublisherDomain(request) 213 case Country: 214 value = getDeviceCountry(request) 215 case DeviceType: 216 value = getDeviceType(request) 217 case Channel: 218 value = getChannelName(request) 219 case GptSlot: 220 value = getGptSlot(imp) 221 case AdUnitCode: 222 value = getAdUnitCode(imp) 223 } 224 ruleKeys = append(ruleKeys, value) 225 } 226 return ruleKeys 227 } 228 229 // getDeviceType returns device type provided into request 230 func getDeviceType(request *openrtb_ext.RequestWrapper) string { 231 value := catchAll 232 if request.Device == nil || len(request.Device.UA) == 0 { 233 return value 234 } 235 if isMobileDevice(request.Device.UA) { 236 value = Phone 237 } else if isTabletDevice(request.Device.UA) { 238 value = Tablet 239 } else { 240 value = Desktop 241 } 242 return value 243 } 244 245 // getDeviceCountry returns device country provided into request 246 func getDeviceCountry(request *openrtb_ext.RequestWrapper) string { 247 value := catchAll 248 if request.Device != nil && request.Device.Geo != nil { 249 value = request.Device.Geo.Country 250 } 251 return value 252 } 253 254 // getMediaType returns media type for give impression 255 func getMediaType(imp *openrtb2.Imp) string { 256 value := catchAll 257 formatCount := 0 258 259 if imp.Banner != nil { 260 formatCount++ 261 value = BannerMedia 262 } 263 if imp.Video != nil && imp.Video.Placement != 1 { 264 formatCount++ 265 value = VideoOutstreamMedia 266 } 267 if imp.Video != nil && imp.Video.Placement == 1 { 268 formatCount++ 269 value = VideoMedia 270 } 271 if imp.Audio != nil { 272 formatCount++ 273 value = AudioMedia 274 } 275 if imp.Native != nil { 276 formatCount++ 277 value = NativeMedia 278 } 279 280 if formatCount > 1 { 281 return catchAll 282 } 283 return value 284 } 285 286 // getSizeValue returns size for given media type in WxH format 287 func getSizeValue(imp *openrtb2.Imp) string { 288 size := catchAll 289 width := int64(0) 290 height := int64(0) 291 292 if imp.Banner != nil { 293 width, height = getBannerSize(imp) 294 } else if imp.Video != nil { 295 width = ptrutil.ValueOrDefault(imp.Video.W) 296 height = ptrutil.ValueOrDefault(imp.Video.H) 297 } 298 299 if width != 0 && height != 0 { 300 size = fmt.Sprintf("%dx%d", width, height) 301 } 302 return size 303 } 304 305 // getBannerSize returns width and height for given banner impression 306 func getBannerSize(imp *openrtb2.Imp) (int64, int64) { 307 width := int64(0) 308 height := int64(0) 309 310 if len(imp.Banner.Format) == 1 { 311 return imp.Banner.Format[0].W, imp.Banner.Format[0].H 312 } else if len(imp.Banner.Format) > 1 { 313 return width, height 314 } else if imp.Banner.W != nil && imp.Banner.H != nil { 315 width = *imp.Banner.W 316 height = *imp.Banner.H 317 } 318 return width, height 319 } 320 321 // getDomain returns domain provided into site or app object 322 func getDomain(request *openrtb_ext.RequestWrapper) string { 323 value := catchAll 324 if request.Site != nil { 325 if len(request.Site.Domain) > 0 { 326 value = request.Site.Domain 327 } else if request.Site.Publisher != nil && len(request.Site.Publisher.Domain) > 0 { 328 value = request.Site.Publisher.Domain 329 } 330 } else if request.App != nil { 331 if len(request.App.Domain) > 0 { 332 value = request.App.Domain 333 } else if request.App.Publisher != nil && len(request.App.Publisher.Domain) > 0 { 334 value = request.App.Publisher.Domain 335 } 336 } 337 return value 338 } 339 340 // getSiteDomain returns domain provided into site object 341 func getSiteDomain(request *openrtb_ext.RequestWrapper) string { 342 value := catchAll 343 if request.Site != nil && len(request.Site.Domain) > 0 { 344 value = request.Site.Domain 345 } else if request.App != nil && len(request.App.Domain) > 0 { 346 value = request.App.Domain 347 } 348 return value 349 } 350 351 // getPublisherDomain returns publisher domain provided into site or app object 352 func getPublisherDomain(request *openrtb_ext.RequestWrapper) string { 353 value := catchAll 354 if request.Site != nil && request.Site.Publisher != nil && len(request.Site.Publisher.Domain) > 0 { 355 value = request.Site.Publisher.Domain 356 } else if request.App != nil && request.App.Publisher != nil && len(request.App.Publisher.Domain) > 0 { 357 value = request.App.Publisher.Domain 358 } 359 return value 360 } 361 362 // getBundle returns app bundle type 363 func getBundle(request *openrtb_ext.RequestWrapper) string { 364 value := catchAll 365 if request.App != nil && len(request.App.Bundle) > 0 { 366 value = request.App.Bundle 367 } 368 return value 369 } 370 371 // getGptSlot returns gptSlot 372 func getGptSlot(imp *openrtb_ext.ImpWrapper) string { 373 value := catchAll 374 375 impExt, err := imp.GetImpExt() 376 if err == nil { 377 extData := impExt.GetData() 378 if extData != nil { 379 if extData.AdServer != nil && extData.AdServer.Name == "gam" { 380 gptSlot := extData.AdServer.AdSlot 381 if gptSlot != "" { 382 value = gptSlot 383 } 384 } else if extData.PbAdslot != "" { 385 value = extData.PbAdslot 386 } 387 } 388 } 389 return value 390 } 391 392 // getChannelName returns channel name 393 func getChannelName(bidRequest *openrtb_ext.RequestWrapper) string { 394 reqExt, err := bidRequest.GetRequestExt() 395 if err == nil && reqExt != nil { 396 prebidExt := reqExt.GetPrebid() 397 if prebidExt != nil && prebidExt.Channel != nil { 398 return prebidExt.Channel.Name 399 } 400 } 401 return catchAll 402 } 403 404 // getAdUnitCode returns adUnit code 405 func getAdUnitCode(imp *openrtb_ext.ImpWrapper) string { 406 adUnitCode := catchAll 407 408 impExt, err := imp.GetImpExt() 409 if err == nil && impExt != nil && impExt.GetGpId() != "" { 410 return impExt.GetGpId() 411 } 412 413 if imp.TagID != "" { 414 return imp.TagID 415 } 416 417 if impExt != nil { 418 impExtData := impExt.GetData() 419 if impExtData != nil && impExtData.PbAdslot != "" { 420 return impExtData.PbAdslot 421 } 422 423 prebidExt := impExt.GetPrebid() 424 if prebidExt != nil && prebidExt.StoredRequest.ID != "" { 425 return prebidExt.StoredRequest.ID 426 } 427 } 428 429 return adUnitCode 430 } 431 432 // isMobileDevice returns true if device is mobile 433 func isMobileDevice(userAgent string) bool { 434 isMobile, err := regexp.MatchString("(?i)Phone|iPhone|Android.*Mobile|Mobile.*Android", userAgent) 435 if err != nil { 436 return false 437 } 438 return isMobile 439 } 440 441 // isTabletDevice returns true if device is tablet 442 func isTabletDevice(userAgent string) bool { 443 isTablet, err := regexp.MatchString("(?i)tablet|iPad|touch.*Windows NT|Windows NT.*touch|Android", userAgent) 444 if err != nil { 445 return false 446 } 447 return isTablet 448 } 449 450 // prepareRuleCombinations prepares rule combinations based on schema dimensions and request fields 451 func prepareRuleCombinations(keys []string, delimiter string) []string { 452 var schemaFields []string 453 454 numSchemaFields := len(keys) 455 ruleKey := newFloorRuleKeys(delimiter) 456 for i := 0; i < numSchemaFields; i++ { 457 schemaFields = append(schemaFields, strings.ToLower(keys[i])) 458 } 459 ruleKey.appendRuleKey(schemaFields) 460 461 for numWildCard := 1; numWildCard <= numSchemaFields; numWildCard++ { 462 newComb := generateCombinations(numSchemaFields, numWildCard) 463 sortCombinations(newComb, numSchemaFields) 464 465 for i := 0; i < len(newComb); i++ { 466 eachSet := make([]string, numSchemaFields) 467 copy(eachSet, schemaFields) 468 for j := 0; j < len(newComb[i]); j++ { 469 eachSet[newComb[i][j]] = catchAll 470 } 471 ruleKey.appendRuleKey(eachSet) 472 } 473 } 474 return ruleKey.getAllRuleKeys() 475 } 476 477 // generateCombinations generates every permutation for the given number of fields with the specified number of 478 // wildcards. Permutations are returned as a list of integer lists where each integer list represents a single 479 // permutation with each integer indicating the position of the fields that are wildcards 480 // source: https://docs.prebid.org/dev-docs/modules/floors.html#rule-selection-process 481 func generateCombinations(numSchemaFields int, numWildCard int) (comb [][]int) { 482 483 for subsetBits := 1; subsetBits < (1 << numSchemaFields); subsetBits++ { 484 if bits.OnesCount(uint(subsetBits)) != numWildCard { 485 continue 486 } 487 var subset []int 488 for object := 0; object < numSchemaFields; object++ { 489 if (subsetBits>>object)&1 == 1 { 490 subset = append(subset, object) 491 } 492 } 493 comb = append(comb, subset) 494 } 495 return comb 496 } 497 498 // sortCombinations sorts the list of combinations from most specific to least specific. A combination is considered more specific than 499 // another combination if it has more exact values (less wildcards). If two combinations have the same number of wildcards, a combination 500 // is considered more specific than another if its left-most fields are more exact. 501 func sortCombinations(comb [][]int, numSchemaFields int) { 502 totalComb := 1 << numSchemaFields 503 504 sort.SliceStable(comb, func(i, j int) bool { 505 wt1 := 0 506 for k := 0; k < len(comb[i]); k++ { 507 wt1 += 1 << (totalComb - comb[i][k]) 508 } 509 510 wt2 := 0 511 for k := 0; k < len(comb[j]); k++ { 512 wt2 += 1 << (totalComb - comb[j][k]) 513 } 514 return wt1 < wt2 515 }) 516 } 517 518 // ruleKeys defines struct used for maintaining rule combinations generated from schema fields and reqeust values. 519 type ruleKeys struct { 520 keyMap map[string]bool 521 keys []string 522 delimiter string 523 } 524 525 // newFloorRuleKeys allocates and initialise ruleKeys 526 func newFloorRuleKeys(delimiter string) *ruleKeys { 527 rulekey := new(ruleKeys) 528 rulekey.delimiter = delimiter 529 rulekey.keyMap = map[string]bool{} 530 return rulekey 531 } 532 533 // appendRuleKey appends unique rules keys into ruleKeys array 534 func (r *ruleKeys) appendRuleKey(rawKey []string) { 535 var key string 536 key = rawKey[0] 537 for j := 1; j < len(rawKey); j++ { 538 key += r.delimiter + rawKey[j] 539 } 540 541 if _, found := r.keyMap[key]; !found { 542 r.keyMap[key] = true 543 r.keys = append(r.keys, key) 544 } 545 } 546 547 // getAllRuleKeys returns all the rules prepared 548 func (r *ruleKeys) getAllRuleKeys() []string { 549 return r.keys 550 }