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