Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions analytics/filesystem/file_module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ package filesystem
import (
"net/http"
"os"
"strconv"
"strings"
"testing"

"github.com/prebid/prebid-server/v4/analytics"
"github.com/prebid/prebid-server/v4/config"
"github.com/prebid/prebid-server/v4/errortypes"
"github.com/prebid/prebid-server/v4/hooks/hookanalytics"
"github.com/prebid/prebid-server/v4/hooks/hookexecution"
"github.com/stretchr/testify/mock"

"github.com/prebid/openrtb/v20/openrtb2"
Expand Down Expand Up @@ -48,6 +52,52 @@ func TestAuctionObject_ToJson(t *testing.T) {
}
}

func TestAuctionObject_ToJsonIncludesRulesEngineWarningAnalytics(t *testing.T) {
ao := &analytics.AuctionObject{
Status: http.StatusOK,
HookExecutionOutcome: []hookexecution.StageOutcome{
{
Groups: []hookexecution.GroupOutcome{
{
InvocationResults: []hookexecution.HookOutcome{
{
HookID: hookexecution.HookID{
ModuleCode: "prebid.rulesengine",
HookImplCode: "rulesengine",
},
AnalyticsTags: hookanalytics.Analytics{
Activities: []hookanalytics.Activity{{
Name: "rules_engine_bidder_filtering",
Status: hookanalytics.ActivityStatusSuccess,
Results: []hookanalytics.Result{{
Status: hookanalytics.ResultStatusBlock,
Values: map[string]interface{}{
"code": errortypes.RulesEngineBidderExcludedWarningCode,
"reason": "excluded_by_rule",
},
}},
}},
},
},
},
},
},
},
},
}

aoJSON := jsonifyAuctionObject(ao)

if strings.Contains(aoJSON, "Transactional Logs Error") {
t.Fatalf("AuctionObject failed to convert to json")
}
if !strings.Contains(aoJSON, `"name":"rules_engine_bidder_filtering"`) ||
!strings.Contains(aoJSON, `"code":`+strconv.Itoa(errortypes.RulesEngineBidderExcludedWarningCode)) ||
!strings.Contains(aoJSON, `"reason":"excluded_by_rule"`) {
t.Fatalf("AuctionObject json did not include structured rules engine warning analytics: %s", aoJSON)
}
}

func TestVideoObject_ToJson(t *testing.T) {
vo := &analytics.VideoObject{
Status: http.StatusOK,
Expand Down
47 changes: 47 additions & 0 deletions analytics/pubstack/helpers/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ package helpers

import (
"net/http"
"strconv"
"strings"
"testing"

"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v4/analytics"
"github.com/prebid/prebid-server/v4/errortypes"
"github.com/prebid/prebid-server/v4/hooks/hookanalytics"
"github.com/prebid/prebid-server/v4/hooks/hookexecution"
"github.com/stretchr/testify/assert"
)

Expand All @@ -18,6 +23,48 @@ func TestJsonifyAuctionObject(t *testing.T) {
assert.NoError(t, err)
}

func TestJsonifyAuctionObjectIncludesRulesEngineWarningAnalytics(t *testing.T) {
ao := &analytics.AuctionObject{
Status: http.StatusOK,
HookExecutionOutcome: []hookexecution.StageOutcome{
{
Groups: []hookexecution.GroupOutcome{
{
InvocationResults: []hookexecution.HookOutcome{
{
HookID: hookexecution.HookID{
ModuleCode: "prebid.rulesengine",
HookImplCode: "rulesengine",
},
AnalyticsTags: hookanalytics.Analytics{
Activities: []hookanalytics.Activity{{
Name: "rules_engine_bidder_filtering",
Status: hookanalytics.ActivityStatusSuccess,
Results: []hookanalytics.Result{{
Status: hookanalytics.ResultStatusBlock,
Values: map[string]interface{}{
"code": errortypes.RulesEngineBidderExcludedWarningCode,
"reason": "excluded_by_rule",
},
}},
}},
},
},
},
},
},
},
},
}

data, err := JsonifyAuctionObject(ao, "scopeId")

assert.NoError(t, err)
assert.True(t, strings.Contains(string(data), `"name":"rules_engine_bidder_filtering"`), string(data))
assert.True(t, strings.Contains(string(data), `"code":`+strconv.Itoa(errortypes.RulesEngineBidderExcludedWarningCode)), string(data))
assert.True(t, strings.Contains(string(data), `"reason":"excluded_by_rule"`), string(data))
}

func TestJsonifyVideoObject(t *testing.T) {
vo := &analytics.VideoObject{
Status: http.StatusOK,
Expand Down
2 changes: 2 additions & 0 deletions errortypes/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const (
TooLongTargetingPrefixWarningCode
TooShortTargetingPrefixWarningCode
BidderBlockedByPrivacySettings
RulesEngineBidderExcludedWarningCode
RulesEngineBidderNotInIncludeListWarningCode
)

// Coder provides an error or warning code with severity.
Expand Down
38 changes: 38 additions & 0 deletions hooks/hookexecution/enricher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,44 @@ func TestGetModulesJSON(t *testing.T) {
}
}

func TestGetModulesJSONIncludesHookWarnings(t *testing.T) {
stageOutcomes := []StageOutcome{
{
Stage: "processed_auction_request",
Groups: []GroupOutcome{
{
InvocationResults: []HookOutcome{
{
HookID: HookID{
ModuleCode: "prebid.rulesengine",
HookImplCode: "rulesengine",
},
Status: StatusSuccess,
Warnings: []string{"Bidder [testBidder] was removed from the request by the rules engine"},
},
},
},
},
},
}
bidRequest := &openrtb2.BidRequest{Test: 1, Ext: []byte(`{"prebid":{"trace":"basic"}}`)}
account := &config.Account{DebugAllow: true}

modules, warns, err := GetModulesJSON(stageOutcomes, bidRequest, account)

require.NoError(t, err)
assert.Empty(t, warns)
var modulesOutcome ModulesOutcome
require.NoError(t, jsonutil.UnmarshalValid(modules, &modulesOutcome))
assert.Equal(t, Messages{
"prebid.rulesengine": {
"rulesengine": {"Bidder [testBidder] was removed from the request by the rules engine"},
},
}, modulesOutcome.Warnings)
require.NotNil(t, modulesOutcome.Trace)
assert.Len(t, modulesOutcome.Trace.Stages, 1)
}

func getStageOutcomes(t *testing.T, file string) []StageOutcome {
var stageOutcomes []StageOutcome
var stageOutcomesTest []StageOutcomeTest
Expand Down
4 changes: 4 additions & 0 deletions modules/prebid/rulesengine/bidder_config_ruleset.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ func buildBidderConfigRuleSet(geoscopes map[string][]string, setDefinitions map[
return nil, err
}
crs.modelGroups[0].tree = *tree
// Propagate the analytics key and model version onto the tree so they are available in the
// ResultFunctionMeta at execution time (e.g. for surfacing them in exclusion warnings).
crs.modelGroups[0].tree.AnalyticsKey = crs.modelGroups[0].analyticsKey
crs.modelGroups[0].tree.ModelVersion = crs.modelGroups[0].version

return []cacheRuleSet[RequestWrapper, ProcessedAuctionHookResult]{crs}, nil
}
7 changes: 7 additions & 0 deletions modules/prebid/rulesengine/cache_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ func createCacheRuleSet(cfg *config.RuleSet) (cacheRuleSet[openrtb_ext.RequestWr
analyticsKey: modelGroup.AnalyticsKey,
tree: *tree,
}
// Propagate the ruleset name, analytics key and model version onto the tree so they are
// available in the ResultFunctionMeta at execution time (e.g. for surfacing them in exclusion
// warnings). The ruleset name is used for display; the analytics key stays exactly as
// configured (it identifies the model group for analytics).
cmg.tree.RulesetName = cfg.Name
cmg.tree.AnalyticsKey = modelGroup.AnalyticsKey
cmg.tree.ModelVersion = modelGroup.Version
crs.modelGroups = append(crs.modelGroups, cmg)
}

Expand Down
10 changes: 8 additions & 2 deletions modules/prebid/rulesengine/hook_processed_auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import (

hs "github.com/prebid/prebid-server/v4/hooks/hookstage"
"github.com/prebid/prebid-server/v4/openrtb_ext"
"github.com/prebid/prebid-server/v4/rules"
"github.com/prebid/prebid-server/v4/util/randomutil"
)

type RequestWrapper = openrtb_ext.RequestWrapper
type ModelGroup = cacheModelGroup[RequestWrapper, ProcessedAuctionHookResult]

type ProcessedAuctionHookResult struct {
HookResult hs.HookResult[hs.ProcessedAuctionRequestPayload]
AllowedBidders map[string]struct{}
HookResult hs.HookResult[hs.ProcessedAuctionRequestPayload]
AllowedBidders map[string]struct{}
IncludeContexts []rules.ResultFunctionMeta
}

func handleProcessedAuctionHook(
Expand Down Expand Up @@ -44,6 +46,10 @@ func handleProcessedAuctionHook(
}
}

// Once every ruleset has run the final allow-list is known, so surface a debug warning naming the
// bidders that were implicitly removed by include rules (present in the request but not allowed).
appendInclusionWarnings(payload.Request, &result)

return result.HookResult, nil
}

Expand Down
32 changes: 32 additions & 0 deletions modules/prebid/rulesengine/module_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package rulesengine

import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"time"

hs "github.com/prebid/prebid-server/v4/hooks/hookstage"
"github.com/prebid/prebid-server/v4/modules/moduledeps"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -107,6 +109,36 @@ func TestBuilderWithWorkingDir(t *testing.T) {

var sampleJsonConfig json.RawMessage = json.RawMessage(`{"enabled": true, "ruleSets": []}`)

// TestHandleProcessedAuctionHookNoConfig verifies that when the account has no rules engine
// configuration, the hook short-circuits and returns an empty result with no warnings or errors,
// without touching the cache or tree manager (so a zero-value Module is safe here).
func TestHandleProcessedAuctionHookNoConfig(t *testing.T) {
tests := []struct {
name string
accountConfig json.RawMessage
}{
{name: "nil_account_config", accountConfig: nil},
{name: "empty_account_config", accountConfig: json.RawMessage{}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := Module{}

result, err := m.HandleProcessedAuctionHook(
context.Background(),
hs.ModuleInvocationContext{AccountID: "account-1", AccountConfig: tt.accountConfig},
hs.ProcessedAuctionRequestPayload{},
)

assert.NoError(t, err)
assert.Equal(t, hs.HookResult[hs.ProcessedAuctionRequestPayload]{}, result)
assert.Empty(t, result.Warnings)
assert.Empty(t, result.Errors)
})
}
}

func TestConfigChanged(t *testing.T) {

testCases := []struct {
Expand Down
Loading
Loading