Skip to content

Commit 754558b

Browse files
author
Mike Davis
authored
Add support for sending flag decisions along with decision metadata. (#292)
* Sends decisions regardless of decision source.
1 parent abfc2b1 commit 754558b

File tree

10 files changed

+189
-80
lines changed

10 files changed

+189
-80
lines changed

pkg/client/client.go

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ func (o *OptimizelyClient) Activate(experimentKey string, userContext entities.U
7676
if experimentDecision.Variation != nil && decisionContext.Experiment != nil {
7777
// send an impression event
7878
result = experimentDecision.Variation.Key
79-
impressionEvent := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, *decisionContext.Experiment, *experimentDecision.Variation, userContext)
80-
o.EventProcessor.ProcessEvent(impressionEvent)
79+
if ue, ok := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, *decisionContext.Experiment,
80+
experimentDecision.Variation, userContext, "", experimentKey, "experiment"); ok {
81+
o.EventProcessor.ProcessEvent(ue)
82+
}
8183
}
8284

8385
return result, err
@@ -129,11 +131,11 @@ func (o *OptimizelyClient) IsFeatureEnabled(featureKey string, userContext entit
129131
}
130132
}
131133

132-
if featureDecision.Source == decision.FeatureTest && featureDecision.Variation != nil {
133-
// send impression event for feature tests
134-
impressionEvent := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, featureDecision.Experiment, *featureDecision.Variation, userContext)
135-
o.EventProcessor.ProcessEvent(impressionEvent)
134+
if ue, ok := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, featureDecision.Experiment,
135+
featureDecision.Variation, userContext, featureKey, featureDecision.Experiment.Key, featureDecision.Source); ok {
136+
o.EventProcessor.ProcessEvent(ue)
136137
}
138+
137139
return result, err
138140
}
139141

@@ -463,14 +465,19 @@ func (o *OptimizelyClient) GetDetailedFeatureDecisionUnsafe(featureKey string, u
463465
if featureDecision.Variation != nil {
464466
decisionInfo.Enabled = featureDecision.Variation.FeatureEnabled
465467

468+
// This information is only necessary for feature tests.
469+
// For rollouts experiments and variations are an implementation detail only.
466470
if featureDecision.Source == decision.FeatureTest {
467471
decisionInfo.VariationKey = featureDecision.Variation.Key
468472
decisionInfo.ExperimentKey = featureDecision.Experiment.Key
469-
// Triggers impression events when applicable
470-
if !disableTracking {
471-
// send impression event for feature tests
472-
impressionEvent := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, featureDecision.Experiment, *featureDecision.Variation, userContext)
473-
o.EventProcessor.ProcessEvent(impressionEvent)
473+
}
474+
475+
// Triggers impression events when applicable
476+
if !disableTracking {
477+
// send impression event for feature tests
478+
if ue, ok := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, featureDecision.Experiment,
479+
featureDecision.Variation, userContext, featureKey, featureDecision.Experiment.Key, featureDecision.Source); ok {
480+
o.EventProcessor.ProcessEvent(ue)
474481
}
475482
}
476483
}
@@ -624,12 +631,14 @@ func (o *OptimizelyClient) getFeatureDecision(featureKey, variableKey string, us
624631
return decisionContext, featureDecision, e
625632
}
626633

634+
decisionContext.ProjectConfig = projectConfig
627635
feature, e := projectConfig.GetFeatureByKey(featureKey)
628636
if e != nil {
629637
o.logger.Warning(fmt.Sprintf(`Could not get feature for key "%s": %s`, featureKey, e))
630638
return decisionContext, featureDecision, nil
631639
}
632640

641+
decisionContext.Feature = &feature
633642
variable := entities.Variable{}
634643
if variableKey != "" {
635644
variable, err = projectConfig.GetVariableByKey(feature.Key, variableKey)
@@ -639,12 +648,7 @@ func (o *OptimizelyClient) getFeatureDecision(featureKey, variableKey string, us
639648
}
640649
}
641650

642-
decisionContext = decision.FeatureDecisionContext{
643-
Feature: &feature,
644-
ProjectConfig: projectConfig,
645-
Variable: variable,
646-
}
647-
651+
decisionContext.Variable = variable
648652
featureDecision, err = o.DecisionService.GetFeatureDecision(decisionContext, userContext)
649653
if err != nil {
650654
o.logger.Warning(fmt.Sprintf(`Received error while making a decision for feature "%s": %s`, featureKey, err))

pkg/client/client_test.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2320,7 +2320,7 @@ func (s *ClientTestSuiteFM) TestIsFeatureEnabledWithDecisionError() {
23202320
s.mockDecisionService.AssertExpectations(s.T())
23212321
}
23222322

2323-
func (s *ClientTestSuiteFM) TestIsFeatureEnabledErrorCases() {
2323+
func (s *ClientTestSuiteFM) TestIsFeatureEnabledErrorConfig() {
23242324
testUserContext := entities.UserContext{ID: "test_user_1"}
23252325
testFeatureKey := "test_feature_key"
23262326

@@ -2335,13 +2335,22 @@ func (s *ClientTestSuiteFM) TestIsFeatureEnabledErrorCases() {
23352335
result, _ := client.IsFeatureEnabled(testFeatureKey, testUserContext)
23362336
s.False(result)
23372337
s.mockDecisionService.AssertNotCalled(s.T(), "GetFeatureDecision")
2338+
}
2339+
2340+
func (s *ClientTestSuiteFM) TestIsFeatureEnabledErrorFeatureKey() {
2341+
testUserContext := entities.UserContext{ID: "test_user_1"}
2342+
testFeatureKey := "test_feature_key"
23382343

23392344
// Test invalid feature key
23402345
expectedError := errors.New("Invalid feature key")
23412346
s.mockConfig.On("GetFeatureByKey", testFeatureKey).Return(entities.Feature{}, expectedError)
2347+
s.mockConfig.On("GetBotFiltering").Return(true)
23422348
s.mockConfigManager.On("GetConfig").Return(s.mockConfig, nil)
23432349

2344-
client = OptimizelyClient{
2350+
s.mockEventProcessor.On("ProcessEvent", mock.Anything).Return(true)
2351+
2352+
client := OptimizelyClient{
2353+
EventProcessor: s.mockEventProcessor,
23452354
ConfigManager: s.mockConfigManager,
23462355
DecisionService: s.mockDecisionService,
23472356
logger: logging.GetLogger("", ""),
@@ -2813,9 +2822,18 @@ func (s *ClientTestSuiteTrackNotification) TestRemoveOnTrackThrowsErrorWhenRemov
28132822
mockNotificationCenter.AssertExpectations(s.T())
28142823
}
28152824

2816-
func TestClientTestSuite(t *testing.T) {
2825+
func TestClientTestSuiteAB(t *testing.T) {
28172826
suite.Run(t, new(ClientTestSuiteAB))
2827+
}
2828+
2829+
func TestClientTestSuiteFM(t *testing.T) {
28182830
suite.Run(t, new(ClientTestSuiteFM))
2831+
}
2832+
2833+
func TestClientTestSuiteTrackEvent(t *testing.T) {
28192834
suite.Run(t, new(ClientTestSuiteTrackEvent))
2835+
}
2836+
2837+
func TestClientTestSuiteTrackNotification(t *testing.T) {
28202838
suite.Run(t, new(ClientTestSuiteTrackNotification))
28212839
}

pkg/client/fixtures_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ func (c *MockProjectConfig) GetAnonymizeIP() bool {
7777
func (c *MockProjectConfig) GetBotFiltering() bool {
7878
return false
7979
}
80+
func (c *MockProjectConfig) SendFlagDecisions() bool {
81+
return false
82+
}
8083

8184
type MockProjectConfigManager struct {
8285
projectConfig config.ProjectConfig

pkg/config/datafileprojectconfig/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type DatafileProjectConfig struct {
4747
rolloutMap map[string]entities.Rollout
4848
anonymizeIP bool
4949
botFiltering bool
50+
sendFlagDecisions bool
5051
}
5152

5253
// GetDatafile returns a string representation of the environment's datafile
@@ -178,6 +179,11 @@ func (c DatafileProjectConfig) GetGroupByID(groupID string) (entities.Group, err
178179
return entities.Group{}, fmt.Errorf(`group with ID "%s" not found`, groupID)
179180
}
180181

182+
// SendFlagDecisions determines whether impressions events are sent for ALL decision types
183+
func (c DatafileProjectConfig) SendFlagDecisions() bool {
184+
return c.sendFlagDecisions
185+
}
186+
181187
// NewDatafileProjectConfig initializes a new datafile from a json byte array using the default JSON datafile parser
182188
func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogProducer) (*DatafileProjectConfig, error) {
183189
datafile, err := Parse(jsonDatafile)
@@ -217,6 +223,7 @@ func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogP
217223
projectID: datafile.ProjectID,
218224
revision: datafile.Revision,
219225
rolloutMap: rolloutMap,
226+
sendFlagDecisions: datafile.SendFlagDecisions,
220227
}
221228

222229
logger.Info("Datafile is valid.")

pkg/config/datafileprojectconfig/entities/entities.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -106,19 +106,20 @@ type Rollout struct {
106106

107107
// Datafile represents the datafile we get from Optimizely
108108
type Datafile struct {
109-
Attributes []Attribute `json:"attributes"`
110-
Audiences []Audience `json:"audiences"`
111-
Experiments []Experiment `json:"experiments"`
112-
Groups []Group `json:"groups"`
113-
FeatureFlags []FeatureFlag `json:"featureFlags"`
114-
Events []Event `json:"events"`
115-
Rollouts []Rollout `json:"rollouts"`
116-
TypedAudiences []Audience `json:"typedAudiences"`
117-
Variables []string `json:"variables"`
118-
AccountID string `json:"accountId"`
119-
ProjectID string `json:"projectId"`
120-
Revision string `json:"revision"`
121-
Version string `json:"version"`
122-
AnonymizeIP bool `json:"anonymizeIP"`
123-
BotFiltering bool `json:"botFiltering"`
109+
Attributes []Attribute `json:"attributes"`
110+
Audiences []Audience `json:"audiences"`
111+
Experiments []Experiment `json:"experiments"`
112+
Groups []Group `json:"groups"`
113+
FeatureFlags []FeatureFlag `json:"featureFlags"`
114+
Events []Event `json:"events"`
115+
Rollouts []Rollout `json:"rollouts"`
116+
TypedAudiences []Audience `json:"typedAudiences"`
117+
Variables []string `json:"variables"`
118+
AccountID string `json:"accountId"`
119+
ProjectID string `json:"projectId"`
120+
Revision string `json:"revision"`
121+
Version string `json:"version"`
122+
AnonymizeIP bool `json:"anonymizeIP"`
123+
BotFiltering bool `json:"botFiltering"`
124+
SendFlagDecisions bool `json:"sendFlagDecisions"`
124125
}

pkg/config/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type ProjectConfig interface {
4141
GetGroupByID(string) (entities.Group, error)
4242
GetProjectID() string
4343
GetRevision() string
44+
SendFlagDecisions() bool
4445
}
4546

4647
// ProjectConfigManager maintains an instance of the ProjectConfig

pkg/decision/entities.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ type UnsafeFeatureDecisionInfo struct {
4545
}
4646

4747
// Source is where the decision came from
48-
type Source string
48+
type Source = string
4949

5050
const (
5151
// Rollout - the decision came from a rollout

pkg/event/events.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,21 @@ type UserEvent struct {
4040

4141
// ImpressionEvent represents an impression event
4242
type ImpressionEvent struct {
43-
EntityID string `json:"entity_id"`
44-
Key string `json:"key"`
4543
Attributes []VisitorAttribute
46-
VariationID string `json:"variation_id"`
47-
CampaignID string `json:"campaign_id"`
48-
ExperimentID string `json:"experiment_id"`
44+
CampaignID string `json:"campaign_id"`
45+
EntityID string `json:"entity_id"`
46+
ExperimentID string `json:"experiment_id"`
47+
Key string `json:"key"`
48+
Metadata DecisionMetadata `json:"metadata"`
49+
VariationID string `json:"variation_id"`
50+
}
51+
52+
// DecisionMetadata captures additional information regarding the decision
53+
type DecisionMetadata struct {
54+
FlagKey string `json:"flag_key"`
55+
RuleKey string `json:"rule_key"`
56+
RuleType string `json:"rule_type"`
57+
VariationKey string `json:"variation_key"`
4958
}
5059

5160
// ConversionEvent represents a conversion event
@@ -101,9 +110,10 @@ type Snapshot struct {
101110

102111
// Decision represents a decision of a snapshot
103112
type Decision struct {
104-
VariationID string `json:"variation_id"`
105-
CampaignID string `json:"campaign_id"`
106-
ExperimentID string `json:"experiment_id"`
113+
VariationID string `json:"variation_id"`
114+
CampaignID string `json:"campaign_id"`
115+
ExperimentID string `json:"experiment_id"`
116+
Metadata DecisionMetadata `json:"metadata"`
107117
}
108118

109119
// SnapshotEvent represents an event of a snapshot

pkg/event/factory.go

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import (
2323
"time"
2424

2525
guuid "github.com/google/uuid"
26+
2627
"github.com/optimizely/go-sdk/pkg/config"
28+
decisionPkg "github.com/optimizely/go-sdk/pkg/decision"
2729
"github.com/optimizely/go-sdk/pkg/entities"
2830
"github.com/optimizely/go-sdk/pkg/utils"
2931
)
@@ -57,26 +59,48 @@ func CreateEventContext(projectConfig config.ProjectConfig) Context {
5759
return context
5860
}
5961

60-
func createImpressionEvent(projectConfig config.ProjectConfig, experiment entities.Experiment,
61-
variation entities.Variation, attributes map[string]interface{}) ImpressionEvent {
62+
func createImpressionEvent(
63+
projectConfig config.ProjectConfig,
64+
experiment entities.Experiment,
65+
variation *entities.Variation,
66+
attributes map[string]interface{},
67+
flagKey, ruleKey, ruleType string,
68+
) ImpressionEvent {
69+
70+
metadata := DecisionMetadata{
71+
FlagKey: flagKey,
72+
RuleKey: ruleKey,
73+
RuleType: ruleType,
74+
}
75+
76+
var variationID string
77+
if variation != nil {
78+
metadata.VariationKey = variation.Key
79+
variationID = variation.ID
80+
}
6281

63-
impression := ImpressionEvent{}
64-
impression.Key = impressionKey
65-
impression.EntityID = experiment.LayerID
66-
impression.Attributes = getEventAttributes(projectConfig, attributes)
67-
impression.VariationID = variation.ID
68-
impression.ExperimentID = experiment.ID
69-
impression.CampaignID = experiment.LayerID
82+
event := ImpressionEvent{
83+
Attributes: getEventAttributes(projectConfig, attributes),
84+
CampaignID: experiment.LayerID,
85+
EntityID: experiment.LayerID,
86+
ExperimentID: experiment.ID,
87+
Key: impressionKey,
88+
Metadata: metadata,
89+
VariationID: variationID,
90+
}
7091

71-
return impression
92+
return event
7293
}
7394

7495
// CreateImpressionUserEvent creates and returns ImpressionEvent for user
7596
func CreateImpressionUserEvent(projectConfig config.ProjectConfig, experiment entities.Experiment,
76-
variation entities.Variation,
77-
userContext entities.UserContext) UserEvent {
97+
variation *entities.Variation, userContext entities.UserContext, flagKey, ruleKey, ruleType string) (UserEvent, bool) {
98+
99+
if (ruleType == decisionPkg.Rollout || variation == nil) && !projectConfig.SendFlagDecisions() {
100+
return UserEvent{}, false
101+
}
78102

79-
impression := createImpressionEvent(projectConfig, experiment, variation, userContext.Attributes)
103+
impression := createImpressionEvent(projectConfig, experiment, variation, userContext.Attributes, flagKey, ruleKey, ruleType)
80104

81105
userEvent := UserEvent{}
82106
userEvent.Timestamp = makeTimestamp()
@@ -85,7 +109,7 @@ func CreateImpressionUserEvent(projectConfig config.ProjectConfig, experiment en
85109
userEvent.Impression = &impression
86110
userEvent.EventContext = CreateEventContext(projectConfig)
87111

88-
return userEvent
112+
return userEvent, true
89113
}
90114

91115
// create an impression visitor
@@ -94,6 +118,7 @@ func createImpressionVisitor(userEvent UserEvent) Visitor {
94118
decision.CampaignID = userEvent.Impression.CampaignID
95119
decision.ExperimentID = userEvent.Impression.ExperimentID
96120
decision.VariationID = userEvent.Impression.VariationID
121+
decision.Metadata = userEvent.Impression.Metadata
97122

98123
dispatchEvent := SnapshotEvent{}
99124
dispatchEvent.Timestamp = makeTimestamp()

0 commit comments

Comments
 (0)