github.com/prebid/prebid-server@v0.275.0/firstpartydata/first_party_data.go (about) 1 package firstpartydata 2 3 import ( 4 "encoding/json" 5 "fmt" 6 7 "github.com/prebid/openrtb/v19/openrtb2" 8 jsonpatch "gopkg.in/evanphx/json-patch.v4" 9 10 "github.com/prebid/prebid-server/errortypes" 11 "github.com/prebid/prebid-server/openrtb_ext" 12 "github.com/prebid/prebid-server/ortb" 13 "github.com/prebid/prebid-server/util/ptrutil" 14 ) 15 16 const ( 17 siteKey = "site" 18 appKey = "app" 19 userKey = "user" 20 dataKey = "data" 21 22 userDataKey = "userData" 23 appContentDataKey = "appContentData" 24 siteContentDataKey = "siteContentData" 25 ) 26 27 type ResolvedFirstPartyData struct { 28 Site *openrtb2.Site 29 App *openrtb2.App 30 User *openrtb2.User 31 } 32 33 // ExtractGlobalFPD extracts request level FPD from the request and removes req.{site,app,user}.ext.data if exists 34 func ExtractGlobalFPD(req *openrtb_ext.RequestWrapper) (map[string][]byte, error) { 35 fpdReqData := make(map[string][]byte, 3) 36 37 siteExt, err := req.GetSiteExt() 38 if err != nil { 39 return nil, err 40 } 41 refreshExt := false 42 43 if len(siteExt.GetExt()[dataKey]) > 0 { 44 newSiteExt := siteExt.GetExt() 45 fpdReqData[siteKey] = newSiteExt[dataKey] 46 delete(newSiteExt, dataKey) 47 siteExt.SetExt(newSiteExt) 48 refreshExt = true 49 } 50 51 appExt, err := req.GetAppExt() 52 if err != nil { 53 return nil, err 54 } 55 if len(appExt.GetExt()[dataKey]) > 0 { 56 newAppExt := appExt.GetExt() 57 fpdReqData[appKey] = newAppExt[dataKey] 58 delete(newAppExt, dataKey) 59 appExt.SetExt(newAppExt) 60 refreshExt = true 61 } 62 63 userExt, err := req.GetUserExt() 64 if err != nil { 65 return nil, err 66 } 67 if len(userExt.GetExt()[dataKey]) > 0 { 68 newUserExt := userExt.GetExt() 69 fpdReqData[userKey] = newUserExt[dataKey] 70 delete(newUserExt, dataKey) 71 userExt.SetExt(newUserExt) 72 refreshExt = true 73 } 74 if refreshExt { 75 // need to keep site/app/user ext clean in case bidder is not in global fpd bidder list 76 // rebuild/resync the request in the request wrapper. 77 if err := req.RebuildRequest(); err != nil { 78 return nil, err 79 } 80 } 81 82 return fpdReqData, nil 83 } 84 85 // ExtractOpenRtbGlobalFPD extracts and deletes user.data and {app/site}.content.data from request 86 func ExtractOpenRtbGlobalFPD(bidRequest *openrtb2.BidRequest) map[string][]openrtb2.Data { 87 openRtbGlobalFPD := make(map[string][]openrtb2.Data, 3) 88 if bidRequest.User != nil && len(bidRequest.User.Data) > 0 { 89 openRtbGlobalFPD[userDataKey] = bidRequest.User.Data 90 bidRequest.User.Data = nil 91 } 92 93 if bidRequest.Site != nil && bidRequest.Site.Content != nil && len(bidRequest.Site.Content.Data) > 0 { 94 openRtbGlobalFPD[siteContentDataKey] = bidRequest.Site.Content.Data 95 bidRequest.Site.Content.Data = nil 96 } 97 98 if bidRequest.App != nil && bidRequest.App.Content != nil && len(bidRequest.App.Content.Data) > 0 { 99 openRtbGlobalFPD[appContentDataKey] = bidRequest.App.Content.Data 100 bidRequest.App.Content.Data = nil 101 } 102 103 return openRtbGlobalFPD 104 } 105 106 // ResolveFPD consolidates First Party Data from different sources and returns valid FPD that will be applied to bidders later or returns errors 107 func ResolveFPD(bidRequest *openrtb2.BidRequest, fpdBidderConfigData map[openrtb_ext.BidderName]*openrtb_ext.ORTB2, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, biddersWithGlobalFPD []string) (map[openrtb_ext.BidderName]*ResolvedFirstPartyData, []error) { 108 var errL []error 109 110 resolvedFpd := make(map[openrtb_ext.BidderName]*ResolvedFirstPartyData) 111 112 allBiddersTable := make(map[string]struct{}) 113 114 if biddersWithGlobalFPD == nil { 115 // add all bidders in bidder configs to receive global data and bidder specific data 116 for bidderName := range fpdBidderConfigData { 117 if _, present := allBiddersTable[string(bidderName)]; !present { 118 allBiddersTable[string(bidderName)] = struct{}{} 119 } 120 } 121 } else { 122 // only bidders in global bidder list will receive global data and bidder specific data 123 for _, bidderName := range biddersWithGlobalFPD { 124 if _, present := allBiddersTable[string(bidderName)]; !present { 125 allBiddersTable[string(bidderName)] = struct{}{} 126 } 127 } 128 } 129 130 for bidderName := range allBiddersTable { 131 fpdConfig := fpdBidderConfigData[openrtb_ext.BidderName(bidderName)] 132 133 resolvedFpdConfig := &ResolvedFirstPartyData{} 134 135 newUser, err := resolveUser(fpdConfig, bidRequest.User, globalFPD, openRtbGlobalFPD, bidderName) 136 if err != nil { 137 errL = append(errL, err) 138 } 139 resolvedFpdConfig.User = newUser 140 141 newApp, err := resolveApp(fpdConfig, bidRequest.App, globalFPD, openRtbGlobalFPD, bidderName) 142 if err != nil { 143 errL = append(errL, err) 144 } 145 resolvedFpdConfig.App = newApp 146 147 newSite, err := resolveSite(fpdConfig, bidRequest.Site, globalFPD, openRtbGlobalFPD, bidderName) 148 if err != nil { 149 errL = append(errL, err) 150 } 151 resolvedFpdConfig.Site = newSite 152 153 if len(errL) == 0 { 154 resolvedFpd[openrtb_ext.BidderName(bidderName)] = resolvedFpdConfig 155 } 156 } 157 return resolvedFpd, errL 158 } 159 160 func resolveUser(fpdConfig *openrtb_ext.ORTB2, bidRequestUser *openrtb2.User, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.User, error) { 161 var fpdConfigUser json.RawMessage 162 163 if fpdConfig != nil && fpdConfig.User != nil { 164 fpdConfigUser = fpdConfig.User 165 } 166 167 if bidRequestUser == nil && fpdConfigUser == nil { 168 return nil, nil 169 } 170 171 var newUser *openrtb2.User 172 if bidRequestUser != nil { 173 newUser = ptrutil.Clone(bidRequestUser) 174 } else { 175 newUser = &openrtb2.User{} 176 } 177 178 //apply global fpd 179 if len(globalFPD[userKey]) > 0 { 180 extData := buildExtData(globalFPD[userKey]) 181 if len(newUser.Ext) > 0 { 182 var err error 183 newUser.Ext, err = jsonpatch.MergePatch(newUser.Ext, extData) 184 if err != nil { 185 return nil, err 186 } 187 } else { 188 newUser.Ext = extData 189 } 190 } 191 if openRtbGlobalFPD != nil && len(openRtbGlobalFPD[userDataKey]) > 0 { 192 newUser.Data = openRtbGlobalFPD[userDataKey] 193 } 194 if fpdConfigUser != nil { 195 if err := mergeUser(newUser, fpdConfigUser); err != nil { 196 return nil, err 197 } 198 } 199 200 return newUser, nil 201 } 202 203 func mergeUser(v *openrtb2.User, overrideJSON json.RawMessage) error { 204 *v = *ortb.CloneUser(v) 205 206 // Track EXTs 207 // It's not necessary to track `ext` fields in array items because the array 208 // items will be replaced entirely with the override JSON, so no merge is required. 209 var ext, extGeo extMerger 210 ext.Track(&v.Ext) 211 if v.Geo != nil { 212 extGeo.Track(&v.Geo.Ext) 213 } 214 215 // Merge 216 if err := json.Unmarshal(overrideJSON, &v); err != nil { 217 return err 218 } 219 220 // Merge EXTs 221 if err := ext.Merge(); err != nil { 222 return err 223 } 224 if err := extGeo.Merge(); err != nil { 225 return err 226 } 227 228 return nil 229 } 230 231 func resolveSite(fpdConfig *openrtb_ext.ORTB2, bidRequestSite *openrtb2.Site, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.Site, error) { 232 var fpdConfigSite json.RawMessage 233 234 if fpdConfig != nil && fpdConfig.Site != nil { 235 fpdConfigSite = fpdConfig.Site 236 } 237 238 if bidRequestSite == nil && fpdConfigSite == nil { 239 return nil, nil 240 } 241 if bidRequestSite == nil && fpdConfigSite != nil { 242 return nil, &errortypes.BadInput{ 243 Message: fmt.Sprintf("incorrect First Party Data for bidder %s: Site object is not defined in request, but defined in FPD config", bidderName), 244 } 245 } 246 247 var newSite *openrtb2.Site 248 if bidRequestSite != nil { 249 newSite = ptrutil.Clone(bidRequestSite) 250 } else { 251 newSite = &openrtb2.Site{} 252 } 253 254 //apply global fpd 255 if len(globalFPD[siteKey]) > 0 { 256 extData := buildExtData(globalFPD[siteKey]) 257 if len(newSite.Ext) > 0 { 258 var err error 259 newSite.Ext, err = jsonpatch.MergePatch(newSite.Ext, extData) 260 if err != nil { 261 return nil, err 262 } 263 } else { 264 newSite.Ext = extData 265 } 266 } 267 // apply global openRTB fpd if exists 268 if len(openRtbGlobalFPD) > 0 && len(openRtbGlobalFPD[siteContentDataKey]) > 0 { 269 if newSite.Content == nil { 270 newSite.Content = &openrtb2.Content{} 271 } else { 272 contentCopy := *newSite.Content 273 newSite.Content = &contentCopy 274 } 275 newSite.Content.Data = openRtbGlobalFPD[siteContentDataKey] 276 } 277 if fpdConfigSite != nil { 278 if err := mergeSite(newSite, fpdConfigSite, bidderName); err != nil { 279 return nil, err 280 } 281 } 282 return newSite, nil 283 } 284 285 func mergeSite(v *openrtb2.Site, overrideJSON json.RawMessage, bidderName string) error { 286 *v = *ortb.CloneSite(v) 287 288 // Track EXTs 289 // It's not necessary to track `ext` fields in array items because the array 290 // items will be replaced entirely with the override JSON, so no merge is required. 291 var ext, extPublisher, extContent, extContentProducer, extContentNetwork, extContentChannel extMerger 292 ext.Track(&v.Ext) 293 if v.Publisher != nil { 294 extPublisher.Track(&v.Publisher.Ext) 295 } 296 if v.Content != nil { 297 extContent.Track(&v.Content.Ext) 298 } 299 if v.Content != nil && v.Content.Producer != nil { 300 extContentProducer.Track(&v.Content.Producer.Ext) 301 } 302 if v.Content != nil && v.Content.Network != nil { 303 extContentNetwork.Track(&v.Content.Network.Ext) 304 } 305 if v.Content != nil && v.Content.Channel != nil { 306 extContentChannel.Track(&v.Content.Channel.Ext) 307 } 308 309 // Merge 310 if err := json.Unmarshal(overrideJSON, &v); err != nil { 311 return err 312 } 313 314 // Merge EXTs 315 if err := ext.Merge(); err != nil { 316 return err 317 } 318 if err := extPublisher.Merge(); err != nil { 319 return err 320 } 321 if err := extContent.Merge(); err != nil { 322 return err 323 } 324 if err := extContentProducer.Merge(); err != nil { 325 return err 326 } 327 if err := extContentNetwork.Merge(); err != nil { 328 return err 329 } 330 if err := extContentChannel.Merge(); err != nil { 331 return err 332 } 333 334 // Re-Validate Site 335 if v.ID == "" && v.Page == "" { 336 return &errortypes.BadInput{ 337 Message: fmt.Sprintf("incorrect First Party Data for bidder %s: Site object cannot set empty page if req.site.id is empty", bidderName), 338 } 339 } 340 341 return nil 342 } 343 344 func resolveApp(fpdConfig *openrtb_ext.ORTB2, bidRequestApp *openrtb2.App, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.App, error) { 345 var fpdConfigApp json.RawMessage 346 347 if fpdConfig != nil { 348 fpdConfigApp = fpdConfig.App 349 } 350 351 if bidRequestApp == nil && fpdConfigApp == nil { 352 return nil, nil 353 } 354 355 if bidRequestApp == nil && fpdConfigApp != nil { 356 return nil, &errortypes.BadInput{ 357 Message: fmt.Sprintf("incorrect First Party Data for bidder %s: App object is not defined in request, but defined in FPD config", bidderName), 358 } 359 } 360 361 var newApp *openrtb2.App 362 if bidRequestApp != nil { 363 newApp = ptrutil.Clone(bidRequestApp) 364 } else { 365 newApp = &openrtb2.App{} 366 } 367 368 //apply global fpd if exists 369 if len(globalFPD[appKey]) > 0 { 370 extData := buildExtData(globalFPD[appKey]) 371 if len(newApp.Ext) > 0 { 372 var err error 373 newApp.Ext, err = jsonpatch.MergePatch(newApp.Ext, extData) 374 if err != nil { 375 return nil, err 376 } 377 } else { 378 newApp.Ext = extData 379 } 380 } 381 382 // apply global openRTB fpd if exists 383 if len(openRtbGlobalFPD) > 0 && len(openRtbGlobalFPD[appContentDataKey]) > 0 { 384 if newApp.Content == nil { 385 newApp.Content = &openrtb2.Content{} 386 } else { 387 contentCopy := *newApp.Content 388 newApp.Content = &contentCopy 389 } 390 newApp.Content.Data = openRtbGlobalFPD[appContentDataKey] 391 } 392 393 if fpdConfigApp != nil { 394 if err := mergeApp(newApp, fpdConfigApp); err != nil { 395 return nil, err 396 } 397 } 398 399 return newApp, nil 400 } 401 402 func mergeApp(v *openrtb2.App, overrideJSON json.RawMessage) error { 403 *v = *ortb.CloneApp(v) 404 405 // Track EXTs 406 // It's not necessary to track `ext` fields in array items because the array 407 // items will be replaced entirely with the override JSON, so no merge is required. 408 var ext, extPublisher, extContent, extContentProducer, extContentNetwork, extContentChannel extMerger 409 ext.Track(&v.Ext) 410 if v.Publisher != nil { 411 extPublisher.Track(&v.Publisher.Ext) 412 } 413 if v.Content != nil { 414 extContent.Track(&v.Content.Ext) 415 } 416 if v.Content != nil && v.Content.Producer != nil { 417 extContentProducer.Track(&v.Content.Producer.Ext) 418 } 419 if v.Content != nil && v.Content.Network != nil { 420 extContentNetwork.Track(&v.Content.Network.Ext) 421 } 422 if v.Content != nil && v.Content.Channel != nil { 423 extContentChannel.Track(&v.Content.Channel.Ext) 424 } 425 426 // Merge 427 if err := json.Unmarshal(overrideJSON, &v); err != nil { 428 return err 429 } 430 431 // Merge EXTs 432 if err := ext.Merge(); err != nil { 433 return err 434 } 435 if err := extPublisher.Merge(); err != nil { 436 return err 437 } 438 if err := extContent.Merge(); err != nil { 439 return err 440 } 441 if err := extContentProducer.Merge(); err != nil { 442 return err 443 } 444 if err := extContentNetwork.Merge(); err != nil { 445 return err 446 } 447 if err := extContentChannel.Merge(); err != nil { 448 return err 449 } 450 451 return nil 452 } 453 454 func buildExtData(data []byte) []byte { 455 res := make([]byte, 0, len(data)+len(`"{"data":}"`)) 456 res = append(res, []byte(`{"data":`)...) 457 res = append(res, data...) 458 res = append(res, []byte(`}`)...) 459 return res 460 } 461 462 // ExtractBidderConfigFPD extracts bidder specific configs from req.ext.prebid.bidderconfig 463 func ExtractBidderConfigFPD(reqExt *openrtb_ext.RequestExt) (map[openrtb_ext.BidderName]*openrtb_ext.ORTB2, error) { 464 fpd := make(map[openrtb_ext.BidderName]*openrtb_ext.ORTB2) 465 reqExtPrebid := reqExt.GetPrebid() 466 if reqExtPrebid != nil { 467 for _, bidderConfig := range reqExtPrebid.BidderConfigs { 468 for _, bidder := range bidderConfig.Bidders { 469 if _, present := fpd[openrtb_ext.BidderName(bidder)]; present { 470 //if bidder has duplicated config - throw an error 471 return nil, &errortypes.BadInput{ 472 Message: fmt.Sprintf("multiple First Party Data bidder configs provided for bidder: %s", bidder), 473 } 474 } 475 476 fpdBidderData := &openrtb_ext.ORTB2{} 477 478 if bidderConfig.Config != nil && bidderConfig.Config.ORTB2 != nil { 479 if bidderConfig.Config.ORTB2.Site != nil { 480 fpdBidderData.Site = bidderConfig.Config.ORTB2.Site 481 } 482 if bidderConfig.Config.ORTB2.App != nil { 483 fpdBidderData.App = bidderConfig.Config.ORTB2.App 484 } 485 if bidderConfig.Config.ORTB2.User != nil { 486 fpdBidderData.User = bidderConfig.Config.ORTB2.User 487 } 488 } 489 490 fpd[openrtb_ext.BidderName(bidder)] = fpdBidderData 491 } 492 } 493 reqExtPrebid.BidderConfigs = nil 494 reqExt.SetPrebid(reqExtPrebid) 495 } 496 return fpd, nil 497 } 498 499 // ExtractFPDForBidders extracts FPD data from request if specified 500 func ExtractFPDForBidders(req *openrtb_ext.RequestWrapper) (map[openrtb_ext.BidderName]*ResolvedFirstPartyData, []error) { 501 reqExt, err := req.GetRequestExt() 502 if err != nil { 503 return nil, []error{err} 504 } 505 if reqExt == nil || reqExt.GetPrebid() == nil { 506 return nil, nil 507 } 508 var biddersWithGlobalFPD []string 509 510 extPrebid := reqExt.GetPrebid() 511 if extPrebid.Data != nil { 512 biddersWithGlobalFPD = extPrebid.Data.Bidders 513 extPrebid.Data.Bidders = nil 514 reqExt.SetPrebid(extPrebid) 515 } 516 517 fbdBidderConfigData, err := ExtractBidderConfigFPD(reqExt) 518 if err != nil { 519 return nil, []error{err} 520 } 521 522 var globalFpd map[string][]byte 523 var openRtbGlobalFPD map[string][]openrtb2.Data 524 525 if biddersWithGlobalFPD != nil { 526 //global fpd data should not be extracted and removed from request if global bidder list is nil. 527 //Bidders that don't have any fpd config should receive request data as is 528 globalFpd, err = ExtractGlobalFPD(req) 529 if err != nil { 530 return nil, []error{err} 531 } 532 openRtbGlobalFPD = ExtractOpenRtbGlobalFPD(req.BidRequest) 533 } 534 535 return ResolveFPD(req.BidRequest, fbdBidderConfigData, globalFpd, openRtbGlobalFPD, biddersWithGlobalFPD) 536 }