github.com/prebid/prebid-server/v2@v2.18.0/hooks/hookexecution/enricher.go (about)

     1  package hookexecution
     2  
     3  import (
     4  	"encoding/json"
     5  
     6  	"github.com/buger/jsonparser"
     7  	"github.com/prebid/openrtb/v20/openrtb2"
     8  	"github.com/prebid/prebid-server/v2/config"
     9  	"github.com/prebid/prebid-server/v2/util/jsonutil"
    10  	jsonpatch "gopkg.in/evanphx/json-patch.v4"
    11  )
    12  
    13  const (
    14  	// traceLevelBasic excludes debug_messages and analytic_tags from output
    15  	traceLevelBasic trace = "basic"
    16  	// traceLevelVerbose sets maximum level of output information
    17  	traceLevelVerbose trace = "verbose"
    18  )
    19  
    20  // Trace controls the level of detail in the output information returned from executing hooks.
    21  type trace string
    22  
    23  func (t trace) isBasicOrHigher() bool {
    24  	return t == traceLevelBasic || t.isVerbose()
    25  }
    26  
    27  func (t trace) isVerbose() bool {
    28  	return t == traceLevelVerbose
    29  }
    30  
    31  type extPrebid struct {
    32  	Prebid extModules `json:"prebid"`
    33  }
    34  
    35  type extModules struct {
    36  	Modules json.RawMessage `json:"modules"`
    37  }
    38  
    39  // EnrichExtBidResponse adds debug and trace information returned from executing hooks to the ext argument.
    40  // In response the outcome is visible under the key response.ext.prebid.modules.
    41  //
    42  // Debug information is added only if the debug mode is enabled by request and allowed by account (if provided).
    43  // The details of the trace output depends on the value in the bidRequest.ext.prebid.trace field.
    44  // Warnings returned if bidRequest contains unexpected types for debug fields controlling debug output.
    45  func EnrichExtBidResponse(
    46  	ext json.RawMessage,
    47  	stageOutcomes []StageOutcome,
    48  	bidRequest *openrtb2.BidRequest,
    49  	account *config.Account,
    50  ) (json.RawMessage, []error, error) {
    51  	modules, warnings, err := GetModulesJSON(stageOutcomes, bidRequest, account)
    52  	if err != nil || modules == nil {
    53  		return ext, warnings, err
    54  	}
    55  
    56  	response, err := jsonutil.Marshal(extPrebid{Prebid: extModules{Modules: modules}})
    57  	if err != nil {
    58  		return ext, warnings, err
    59  	}
    60  
    61  	if ext != nil {
    62  		response, err = jsonpatch.MergePatch(ext, response)
    63  	}
    64  
    65  	return response, warnings, err
    66  }
    67  
    68  // GetModulesJSON returns debug and trace information produced from executing hooks.
    69  // Debug information is returned only if the debug mode is enabled by request and allowed by account (if provided).
    70  // The details of the trace output depends on the value in the bidRequest.ext.prebid.trace field.
    71  // Warnings returned if bidRequest contains unexpected types for debug fields controlling debug output.
    72  func GetModulesJSON(
    73  	stageOutcomes []StageOutcome,
    74  	bidRequest *openrtb2.BidRequest,
    75  	account *config.Account,
    76  ) (json.RawMessage, []error, error) {
    77  	if len(stageOutcomes) == 0 {
    78  		return nil, nil, nil
    79  	}
    80  
    81  	trace, isDebugEnabled, warnings := getDebugContext(bidRequest, account)
    82  	modulesOutcome := getModulesOutcome(stageOutcomes, trace, isDebugEnabled)
    83  	if modulesOutcome == nil {
    84  		return nil, warnings, nil
    85  	}
    86  
    87  	data, err := jsonutil.Marshal(modulesOutcome)
    88  
    89  	return data, warnings, err
    90  }
    91  
    92  func getDebugContext(bidRequest *openrtb2.BidRequest, account *config.Account) (trace, bool, []error) {
    93  	var traceLevel string
    94  	var isDebugEnabled bool
    95  	var warnings []error
    96  	var err error
    97  
    98  	if bidRequest != nil {
    99  		traceLevel, err = jsonparser.GetString(bidRequest.Ext, "prebid", "trace")
   100  		if err != nil && err != jsonparser.KeyPathNotFoundError {
   101  			warnings = append(warnings, err)
   102  		}
   103  
   104  		isDebug, err := jsonparser.GetBoolean(bidRequest.Ext, "prebid", "debug")
   105  		if err != nil && err != jsonparser.KeyPathNotFoundError {
   106  			warnings = append(warnings, err)
   107  		}
   108  
   109  		isDebugEnabled = bidRequest.Test == 1 || isDebug
   110  		if account != nil {
   111  			isDebugEnabled = isDebugEnabled && account.DebugAllow
   112  		}
   113  	}
   114  
   115  	return trace(traceLevel), isDebugEnabled, warnings
   116  }
   117  
   118  func getModulesOutcome(stageOutcomes []StageOutcome, trace trace, isDebugEnabled bool) *ModulesOutcome {
   119  	var modulesOutcome ModulesOutcome
   120  	stages := make(map[string]Stage)
   121  	stageNames := make([]string, 0)
   122  
   123  	for _, stageOutcome := range stageOutcomes {
   124  		if len(stageOutcome.Groups) == 0 {
   125  			continue
   126  		}
   127  
   128  		prepareModulesOutcome(&modulesOutcome, stageOutcome.Groups, trace, isDebugEnabled)
   129  		if !trace.isBasicOrHigher() {
   130  			continue
   131  		}
   132  
   133  		stage, ok := stages[stageOutcome.Stage]
   134  		if !ok {
   135  			stageNames = append(stageNames, stageOutcome.Stage)
   136  			stage = Stage{
   137  				Stage:    stageOutcome.Stage,
   138  				Outcomes: []StageOutcome{},
   139  			}
   140  		}
   141  
   142  		stage.Outcomes = append(stage.Outcomes, stageOutcome)
   143  		if stageOutcome.ExecutionTimeMillis > stage.ExecutionTimeMillis {
   144  			stage.ExecutionTimeMillis = stageOutcome.ExecutionTimeMillis
   145  		}
   146  
   147  		stages[stageOutcome.Stage] = stage
   148  	}
   149  
   150  	if modulesOutcome.Errors == nil && modulesOutcome.Warnings == nil && len(stages) == 0 {
   151  		return nil
   152  	}
   153  
   154  	if len(stages) > 0 {
   155  		modulesOutcome.Trace = &TraceOutcome{}
   156  		modulesOutcome.Trace.Stages = make([]Stage, 0, len(stages))
   157  
   158  		for _, stage := range stageNames {
   159  			modulesOutcome.Trace.ExecutionTimeMillis += stages[stage].ExecutionTimeMillis
   160  			modulesOutcome.Trace.Stages = append(modulesOutcome.Trace.Stages, stages[stage])
   161  		}
   162  	}
   163  
   164  	return &modulesOutcome
   165  }
   166  
   167  func prepareModulesOutcome(modulesOutcome *ModulesOutcome, groups []GroupOutcome, trace trace, isDebugEnabled bool) {
   168  	for _, group := range groups {
   169  		for i, hookOutcome := range group.InvocationResults {
   170  			if !trace.isVerbose() {
   171  				group.InvocationResults[i].DebugMessages = nil
   172  			}
   173  
   174  			if isDebugEnabled {
   175  				modulesOutcome.Errors = fillMessages(modulesOutcome.Errors, hookOutcome.Errors, hookOutcome.HookID)
   176  				modulesOutcome.Warnings = fillMessages(modulesOutcome.Warnings, hookOutcome.Warnings, hookOutcome.HookID)
   177  			}
   178  		}
   179  	}
   180  }
   181  
   182  func fillMessages(messages Messages, values []string, hookID HookID) Messages {
   183  	if len(values) == 0 {
   184  		return messages
   185  	}
   186  
   187  	if messages == nil {
   188  		return Messages{hookID.ModuleCode: {hookID.HookImplCode: values}}
   189  	}
   190  
   191  	if _, ok := messages[hookID.ModuleCode]; !ok {
   192  		messages[hookID.ModuleCode] = map[string][]string{hookID.HookImplCode: values}
   193  		return messages
   194  	}
   195  
   196  	if prevValues, ok := messages[hookID.ModuleCode][hookID.HookImplCode]; ok {
   197  		values = append(prevValues, values...)
   198  	}
   199  
   200  	messages[hookID.ModuleCode][hookID.HookImplCode] = values
   201  
   202  	return messages
   203  }