github.com/prebid/prebid-server/v2@v2.18.0/firstpartydata/first_party_data.go (about) 1 package firstpartydata 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "strings" 8 9 "github.com/prebid/openrtb/v20/openrtb2" 10 jsonpatch "gopkg.in/evanphx/json-patch.v4" 11 12 "github.com/prebid/prebid-server/v2/errortypes" 13 "github.com/prebid/prebid-server/v2/openrtb_ext" 14 "github.com/prebid/prebid-server/v2/util/jsonutil" 15 "github.com/prebid/prebid-server/v2/util/ptrutil" 16 ) 17 18 var ( 19 ErrBadRequest = errors.New("invalid request ext") 20 ErrBadFPD = errors.New("invalid first party data ext") 21 ) 22 23 const ( 24 siteKey = "site" 25 appKey = "app" 26 userKey = "user" 27 dataKey = "data" 28 29 userDataKey = "userData" 30 appContentDataKey = "appContentData" 31 siteContentDataKey = "siteContentData" 32 ) 33 34 type ResolvedFirstPartyData struct { 35 Site *openrtb2.Site 36 App *openrtb2.App 37 User *openrtb2.User 38 } 39 40 // ExtractGlobalFPD extracts request level FPD from the request and removes req.{site,app,user}.ext.data if exists 41 func ExtractGlobalFPD(req *openrtb_ext.RequestWrapper) (map[string][]byte, error) { 42 fpdReqData := make(map[string][]byte, 3) 43 44 siteExt, err := req.GetSiteExt() 45 if err != nil { 46 return nil, err 47 } 48 refreshExt := false 49 50 if len(siteExt.GetExt()[dataKey]) > 0 { 51 newSiteExt := siteExt.GetExt() 52 fpdReqData[siteKey] = newSiteExt[dataKey] 53 delete(newSiteExt, dataKey) 54 siteExt.SetExt(newSiteExt) 55 refreshExt = true 56 } 57 58 appExt, err := req.GetAppExt() 59 if err != nil { 60 return nil, err 61 } 62 if len(appExt.GetExt()[dataKey]) > 0 { 63 newAppExt := appExt.GetExt() 64 fpdReqData[appKey] = newAppExt[dataKey] 65 delete(newAppExt, dataKey) 66 appExt.SetExt(newAppExt) 67 refreshExt = true 68 } 69 70 userExt, err := req.GetUserExt() 71 if err != nil { 72 return nil, err 73 } 74 if len(userExt.GetExt()[dataKey]) > 0 { 75 newUserExt := userExt.GetExt() 76 fpdReqData[userKey] = newUserExt[dataKey] 77 delete(newUserExt, dataKey) 78 userExt.SetExt(newUserExt) 79 refreshExt = true 80 } 81 if refreshExt { 82 // need to keep site/app/user ext clean in case bidder is not in global fpd bidder list 83 // rebuild/resync the request in the request wrapper. 84 if err := req.RebuildRequest(); err != nil { 85 return nil, err 86 } 87 } 88 89 return fpdReqData, nil 90 } 91 92 // ExtractOpenRtbGlobalFPD extracts and deletes user.data and {app/site}.content.data from request 93 func ExtractOpenRtbGlobalFPD(bidRequest *openrtb2.BidRequest) map[string][]openrtb2.Data { 94 openRtbGlobalFPD := make(map[string][]openrtb2.Data, 3) 95 if bidRequest.User != nil && len(bidRequest.User.Data) > 0 { 96 openRtbGlobalFPD[userDataKey] = bidRequest.User.Data 97 bidRequest.User.Data = nil 98 } 99 100 if bidRequest.Site != nil && bidRequest.Site.Content != nil && len(bidRequest.Site.Content.Data) > 0 { 101 openRtbGlobalFPD[siteContentDataKey] = bidRequest.Site.Content.Data 102 bidRequest.Site.Content.Data = nil 103 } 104 105 if bidRequest.App != nil && bidRequest.App.Content != nil && len(bidRequest.App.Content.Data) > 0 { 106 openRtbGlobalFPD[appContentDataKey] = bidRequest.App.Content.Data 107 bidRequest.App.Content.Data = nil 108 } 109 110 return openRtbGlobalFPD 111 } 112 113 // ResolveFPD consolidates First Party Data from different sources and returns valid FPD that will be applied to bidders later or returns errors 114 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) { 115 var errL []error 116 117 resolvedFpd := make(map[openrtb_ext.BidderName]*ResolvedFirstPartyData) 118 119 allBiddersTable := make(map[string]struct{}) 120 121 if biddersWithGlobalFPD == nil { 122 // add all bidders in bidder configs to receive global data and bidder specific data 123 for bidderName := range fpdBidderConfigData { 124 if _, present := allBiddersTable[string(bidderName)]; !present { 125 allBiddersTable[string(bidderName)] = struct{}{} 126 } 127 } 128 } else { 129 // only bidders in global bidder list will receive global data and bidder specific data 130 for _, bidder := range biddersWithGlobalFPD { 131 bidderName := openrtb_ext.NormalizeBidderNameOrUnchanged(bidder) 132 133 if _, present := allBiddersTable[string(bidderName)]; !present { 134 allBiddersTable[string(bidderName)] = struct{}{} 135 } 136 } 137 } 138 139 for bidderName := range allBiddersTable { 140 fpdConfig := fpdBidderConfigData[openrtb_ext.BidderName(bidderName)] 141 142 resolvedFpdConfig := &ResolvedFirstPartyData{} 143 144 newUser, err := resolveUser(fpdConfig, bidRequest.User, globalFPD, openRtbGlobalFPD, bidderName) 145 if err != nil { 146 errL = append(errL, err) 147 } 148 resolvedFpdConfig.User = newUser 149 150 newApp, err := resolveApp(fpdConfig, bidRequest.App, globalFPD, openRtbGlobalFPD, bidderName) 151 if err != nil { 152 errL = append(errL, err) 153 } 154 resolvedFpdConfig.App = newApp 155 156 newSite, err := resolveSite(fpdConfig, bidRequest.Site, globalFPD, openRtbGlobalFPD, bidderName) 157 if err != nil { 158 errL = append(errL, err) 159 } 160 resolvedFpdConfig.Site = newSite 161 162 if len(errL) == 0 { 163 resolvedFpd[openrtb_ext.BidderName(bidderName)] = resolvedFpdConfig 164 } 165 } 166 return resolvedFpd, errL 167 } 168 169 func resolveUser(fpdConfig *openrtb_ext.ORTB2, bidRequestUser *openrtb2.User, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.User, error) { 170 var fpdConfigUser json.RawMessage 171 172 if fpdConfig != nil && fpdConfig.User != nil { 173 fpdConfigUser = fpdConfig.User 174 } 175 176 if bidRequestUser == nil && fpdConfigUser == nil { 177 return nil, nil 178 } 179 180 var newUser *openrtb2.User 181 if bidRequestUser != nil { 182 newUser = ptrutil.Clone(bidRequestUser) 183 } else { 184 newUser = &openrtb2.User{} 185 } 186 187 //apply global fpd 188 if len(globalFPD[userKey]) > 0 { 189 extData := buildExtData(globalFPD[userKey]) 190 if len(newUser.Ext) > 0 { 191 var err error 192 newUser.Ext, err = jsonpatch.MergePatch(newUser.Ext, extData) 193 if err != nil { 194 return nil, formatMergePatchError(err) 195 } 196 } else { 197 newUser.Ext = extData 198 } 199 } 200 if openRtbGlobalFPD != nil && len(openRtbGlobalFPD[userDataKey]) > 0 { 201 newUser.Data = openRtbGlobalFPD[userDataKey] 202 } 203 if fpdConfigUser != nil { 204 if err := jsonutil.MergeClone(newUser, fpdConfigUser); err != nil { 205 return nil, formatMergeCloneError(err) 206 } 207 } 208 209 return newUser, nil 210 } 211 212 func resolveSite(fpdConfig *openrtb_ext.ORTB2, bidRequestSite *openrtb2.Site, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.Site, error) { 213 var fpdConfigSite json.RawMessage 214 215 if fpdConfig != nil && fpdConfig.Site != nil { 216 fpdConfigSite = fpdConfig.Site 217 } 218 219 if bidRequestSite == nil && fpdConfigSite == nil { 220 return nil, nil 221 } 222 if bidRequestSite == nil && fpdConfigSite != nil { 223 return nil, &errortypes.BadInput{ 224 Message: fmt.Sprintf("incorrect First Party Data for bidder %s: Site object is not defined in request, but defined in FPD config", bidderName), 225 } 226 } 227 228 var newSite *openrtb2.Site 229 if bidRequestSite != nil { 230 newSite = ptrutil.Clone(bidRequestSite) 231 } else { 232 newSite = &openrtb2.Site{} 233 } 234 235 //apply global fpd 236 if len(globalFPD[siteKey]) > 0 { 237 extData := buildExtData(globalFPD[siteKey]) 238 if len(newSite.Ext) > 0 { 239 var err error 240 newSite.Ext, err = jsonpatch.MergePatch(newSite.Ext, extData) 241 if err != nil { 242 return nil, formatMergePatchError(err) 243 } 244 } else { 245 newSite.Ext = extData 246 } 247 } 248 // apply global openRTB fpd if exists 249 if len(openRtbGlobalFPD) > 0 && len(openRtbGlobalFPD[siteContentDataKey]) > 0 { 250 if newSite.Content == nil { 251 newSite.Content = &openrtb2.Content{} 252 } else { 253 contentCopy := *newSite.Content 254 newSite.Content = &contentCopy 255 } 256 newSite.Content.Data = openRtbGlobalFPD[siteContentDataKey] 257 } 258 if fpdConfigSite != nil { 259 if err := jsonutil.MergeClone(newSite, fpdConfigSite); err != nil { 260 return nil, formatMergeCloneError(err) 261 } 262 263 // Re-Validate Site 264 if newSite.ID == "" && newSite.Page == "" { 265 return nil, &errortypes.BadInput{ 266 Message: fmt.Sprintf("incorrect First Party Data for bidder %s: Site object cannot set empty page if req.site.id is empty", bidderName), 267 } 268 } 269 } 270 return newSite, nil 271 } 272 273 func formatMergePatchError(err error) error { 274 if errors.Is(err, jsonpatch.ErrBadJSONDoc) { 275 return ErrBadRequest 276 } 277 278 if errors.Is(err, jsonpatch.ErrBadJSONPatch) { 279 return ErrBadFPD 280 } 281 282 return err 283 } 284 285 func formatMergeCloneError(err error) error { 286 if strings.Contains(err.Error(), "invalid json on existing object") { 287 return ErrBadRequest 288 } 289 return ErrBadFPD 290 } 291 292 func resolveApp(fpdConfig *openrtb_ext.ORTB2, bidRequestApp *openrtb2.App, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.App, error) { 293 var fpdConfigApp json.RawMessage 294 295 if fpdConfig != nil { 296 fpdConfigApp = fpdConfig.App 297 } 298 299 if bidRequestApp == nil && fpdConfigApp == nil { 300 return nil, nil 301 } 302 303 if bidRequestApp == nil && fpdConfigApp != nil { 304 return nil, &errortypes.BadInput{ 305 Message: fmt.Sprintf("incorrect First Party Data for bidder %s: App object is not defined in request, but defined in FPD config", bidderName), 306 } 307 } 308 309 var newApp *openrtb2.App 310 if bidRequestApp != nil { 311 newApp = ptrutil.Clone(bidRequestApp) 312 } else { 313 newApp = &openrtb2.App{} 314 } 315 316 //apply global fpd if exists 317 if len(globalFPD[appKey]) > 0 { 318 extData := buildExtData(globalFPD[appKey]) 319 if len(newApp.Ext) > 0 { 320 var err error 321 newApp.Ext, err = jsonpatch.MergePatch(newApp.Ext, extData) 322 if err != nil { 323 return nil, formatMergePatchError(err) 324 } 325 } else { 326 newApp.Ext = extData 327 } 328 } 329 330 // apply global openRTB fpd if exists 331 if len(openRtbGlobalFPD) > 0 && len(openRtbGlobalFPD[appContentDataKey]) > 0 { 332 if newApp.Content == nil { 333 newApp.Content = &openrtb2.Content{} 334 } else { 335 contentCopy := *newApp.Content 336 newApp.Content = &contentCopy 337 } 338 newApp.Content.Data = openRtbGlobalFPD[appContentDataKey] 339 } 340 341 if fpdConfigApp != nil { 342 if err := jsonutil.MergeClone(newApp, fpdConfigApp); err != nil { 343 return nil, formatMergeCloneError(err) 344 } 345 } 346 347 return newApp, nil 348 } 349 350 func buildExtData(data []byte) []byte { 351 res := make([]byte, 0, len(data)+len(`"{"data":}"`)) 352 res = append(res, []byte(`{"data":`)...) 353 res = append(res, data...) 354 res = append(res, []byte(`}`)...) 355 return res 356 } 357 358 // ExtractBidderConfigFPD extracts bidder specific configs from req.ext.prebid.bidderconfig 359 func ExtractBidderConfigFPD(reqExt *openrtb_ext.RequestExt) (map[openrtb_ext.BidderName]*openrtb_ext.ORTB2, error) { 360 fpd := make(map[openrtb_ext.BidderName]*openrtb_ext.ORTB2) 361 362 reqExtPrebid := reqExt.GetPrebid() 363 if reqExtPrebid != nil { 364 for _, bidderConfig := range reqExtPrebid.BidderConfigs { 365 for _, bidder := range bidderConfig.Bidders { 366 bidderName := openrtb_ext.NormalizeBidderNameOrUnchanged(bidder) 367 368 if _, duplicate := fpd[bidderName]; duplicate { 369 return nil, &errortypes.BadInput{ 370 Message: fmt.Sprintf("multiple First Party Data bidder configs provided for bidder: %s", bidder), 371 } 372 } 373 374 fpdBidderData := &openrtb_ext.ORTB2{} 375 376 if bidderConfig.Config != nil && bidderConfig.Config.ORTB2 != nil { 377 fpdBidderData.Site = bidderConfig.Config.ORTB2.Site 378 fpdBidderData.App = bidderConfig.Config.ORTB2.App 379 fpdBidderData.User = bidderConfig.Config.ORTB2.User 380 } 381 382 fpd[bidderName] = fpdBidderData 383 } 384 } 385 reqExtPrebid.BidderConfigs = nil 386 reqExt.SetPrebid(reqExtPrebid) 387 } 388 return fpd, nil 389 } 390 391 // ExtractFPDForBidders extracts FPD data from request if specified 392 func ExtractFPDForBidders(req *openrtb_ext.RequestWrapper) (map[openrtb_ext.BidderName]*ResolvedFirstPartyData, []error) { 393 reqExt, err := req.GetRequestExt() 394 if err != nil { 395 return nil, []error{err} 396 } 397 if reqExt == nil || reqExt.GetPrebid() == nil { 398 return nil, nil 399 } 400 var biddersWithGlobalFPD []string 401 402 extPrebid := reqExt.GetPrebid() 403 if extPrebid.Data != nil { 404 biddersWithGlobalFPD = extPrebid.Data.Bidders 405 extPrebid.Data.Bidders = nil 406 reqExt.SetPrebid(extPrebid) 407 } 408 409 fbdBidderConfigData, err := ExtractBidderConfigFPD(reqExt) 410 if err != nil { 411 return nil, []error{err} 412 } 413 414 var globalFpd map[string][]byte 415 var openRtbGlobalFPD map[string][]openrtb2.Data 416 417 if biddersWithGlobalFPD != nil { 418 //global fpd data should not be extracted and removed from request if global bidder list is nil. 419 //Bidders that don't have any fpd config should receive request data as is 420 globalFpd, err = ExtractGlobalFPD(req) 421 if err != nil { 422 return nil, []error{err} 423 } 424 openRtbGlobalFPD = ExtractOpenRtbGlobalFPD(req.BidRequest) 425 } 426 427 return ResolveFPD(req.BidRequest, fbdBidderConfigData, globalFpd, openRtbGlobalFPD, biddersWithGlobalFPD) 428 }