Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 4 additions & 4 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ func (o *OptimizelyClient) decide(userContext *OptimizelyUserContext, key string

if !allOptions.DisableDecisionEvent {
if ue, ok := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, featureDecision.Experiment,
featureDecision.Variation, usrContext, key, featureDecision.Experiment.Key, featureDecision.Source, flagEnabled); ok {
featureDecision.Variation, usrContext, key, featureDecision.Experiment.Key, featureDecision.Source, flagEnabled, featureDecision.CmabUUID); ok {
o.EventProcessor.ProcessEvent(ue)
eventSent = true
}
Expand Down Expand Up @@ -460,7 +460,7 @@ func (o *OptimizelyClient) Activate(experimentKey string, userContext entities.U
// send an impression event
result = experimentDecision.Variation.Key
if ue, ok := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, *decisionContext.Experiment,
experimentDecision.Variation, userContext, "", experimentKey, "experiment", true); ok {
experimentDecision.Variation, userContext, "", experimentKey, "experiment", true, experimentDecision.CmabUUID); ok {
o.EventProcessor.ProcessEvent(ue)
}
}
Expand Down Expand Up @@ -518,7 +518,7 @@ func (o *OptimizelyClient) IsFeatureEnabled(featureKey string, userContext entit
}

if ue, ok := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, featureDecision.Experiment,
featureDecision.Variation, userContext, featureKey, featureDecision.Experiment.Key, featureDecision.Source, result); ok && featureDecision.Source != "" {
featureDecision.Variation, userContext, featureKey, featureDecision.Experiment.Key, featureDecision.Source, result, featureDecision.CmabUUID); ok && featureDecision.Source != "" {
o.EventProcessor.ProcessEvent(ue)
}

Expand Down Expand Up @@ -883,7 +883,7 @@ func (o *OptimizelyClient) GetDetailedFeatureDecisionUnsafe(featureKey string, u
if !disableTracking {
// send impression event for feature tests
if ue, ok := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, featureDecision.Experiment,
featureDecision.Variation, userContext, featureKey, featureDecision.Experiment.Key, featureDecision.Source, decisionInfo.Enabled); ok {
featureDecision.Variation, userContext, featureKey, featureDecision.Experiment.Key, featureDecision.Source, decisionInfo.Enabled, featureDecision.CmabUUID); ok {
o.EventProcessor.ProcessEvent(ue)
}
}
Expand Down
1 change: 1 addition & 0 deletions pkg/decision/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type FeatureDecision struct {
Source Source
Experiment entities.Experiment
Variation *entities.Variation
CmabUUID *string
}

// ExperimentDecision contains the decision information about an experiment
Expand Down
6 changes: 6 additions & 0 deletions pkg/decision/experiment_cmab_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ func (s *ExperimentCmabService) GetDecision(decisionContext ExperimentDecisionCo
decision.Variation = &variationCopy
decision.Reason = pkgReasons.CmabVariationAssigned

// Store CMAB UUID in the decision
if cmabDecision.CmabUUID != "" {
cmabUUIDCopy := cmabDecision.CmabUUID
decision.CmabUUID = &cmabUUIDCopy
}

message := fmt.Sprintf("User bucketed into variation %s by CMAB service", variation.Key)
decisionReasons.AddInfo(message)
return decision, decisionReasons, nil
Expand Down
52 changes: 52 additions & 0 deletions pkg/decision/experiment_cmab_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,58 @@ func (s *ExperimentCmabTestSuite) TestCreateCmabExperimentEmptyFields() {
s.Equal(5000, result.TrafficAllocation[0].EndOfRange)
}

func (s *ExperimentCmabTestSuite) TestGetDecisionWithCmabUUID() {
// Create decision context with CMAB experiment
decisionContext := ExperimentDecisionContext{
Experiment: &s.cmabExperiment,
ProjectConfig: s.mockProjectConfig,
}

// Expected UUID that should be propagated
expectedUUID := "test-uuid-12345"

// Mock bucketer to return CMAB dummy entity ID (so traffic allocation passes)
s.mockExperimentBucketer.On("BucketToEntityID", mock.Anything, mock.AnythingOfType("entities.Experiment"), mock.Anything).
Return(CmabDummyEntityID, reasons.BucketedIntoVariation, nil)

// Setup mock CMAB service to return a decision with UUID
cmabDecision := cmab.Decision{
VariationID: "var1", // This matches an existing variation in s.cmabExperiment
CmabUUID: expectedUUID,
}
s.mockCmabService.On("GetDecision", s.mockProjectConfig, s.testUserContext, "cmab_exp_1", s.options).
Return(cmabDecision, nil)

// Get decision
decision, decisionReasons, err := s.experimentCmabService.GetDecision(decisionContext, s.testUserContext, s.options)

// Verify basic results
s.NoError(err, "Should not return an error")
s.NotNil(decision.Variation, "Should return a variation")
s.Equal("var1", decision.Variation.ID, "Should return the correct variation ID")
s.Equal(reasons.CmabVariationAssigned, decision.Reason, "Should have the correct reason")

// Verify CMAB UUID was captured
s.NotNil(decision.CmabUUID, "CMAB UUID should not be nil")
s.Equal(expectedUUID, *decision.CmabUUID, "CMAB UUID should match the expected value")

// Check for the message in the reasons
report := decisionReasons.ToReport()
s.NotEmpty(report, "Decision reasons report should not be empty")
messageFound := false
for _, msg := range report {
if strings.Contains(msg, "User bucketed into variation") {
messageFound = true
break
}
}
s.True(messageFound, "Expected bucketing message not found in decision reasons")

// Verify mock expectations
s.mockCmabService.AssertExpectations(s.T())
s.mockExperimentBucketer.AssertExpectations(s.T())
}

func TestExperimentCmabTestSuite(t *testing.T) {
suite.Run(t, new(ExperimentCmabTestSuite))
}
1 change: 1 addition & 0 deletions pkg/decision/feature_experiment_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func (f FeatureExperimentService) GetDecision(decisionContext FeatureDecisionCon
Decision: experimentDecision.Decision,
Variation: experimentDecision.Variation,
Source: FeatureTest,
CmabUUID: experimentDecision.CmabUUID,
}

return featureDecision, reasons, err
Expand Down
53 changes: 53 additions & 0 deletions pkg/decision/feature_experiment_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,59 @@ func (s *FeatureExperimentServiceTestSuite) TestNewFeatureExperimentService() {
s.IsType(compositeExperimentService, featureExperimentService.compositeExperimentService)
}

func (s *FeatureExperimentServiceTestSuite) TestGetDecisionWithCmabUUID() {
testUserContext := entities.UserContext{
ID: "test_user_1",
}

// Create test UUID
testUUID := "test-cmab-uuid-12345"

// Create experiment decision with UUID
expectedVariation := testExp1113.Variations["2223"]
returnExperimentDecision := ExperimentDecision{
Variation: &expectedVariation,
CmabUUID: &testUUID,
}

// Setup experiment decision context
testExperimentDecisionContext := ExperimentDecisionContext{
Experiment: &testExp1113,
ProjectConfig: s.mockConfig,
}

// Setup mock to return experiment decision with UUID
s.mockExperimentService.On("GetDecision", testExperimentDecisionContext, testUserContext, s.options).
Return(returnExperimentDecision, s.reasons, nil)

// Create service under test
featureExperimentService := &FeatureExperimentService{
compositeExperimentService: s.mockExperimentService,
logger: logging.GetLogger("sdkKey", "FeatureExperimentService"),
}

// Create expected feature decision with propagated UUID
expectedFeatureDecision := FeatureDecision{
Experiment: *testExperimentDecisionContext.Experiment,
Variation: &expectedVariation,
Source: FeatureTest,
CmabUUID: &testUUID, // UUID should be propagated
}

// Call GetDecision
actualFeatureDecision, _, err := featureExperimentService.GetDecision(s.testFeatureDecisionContext, testUserContext, s.options)

// Verify results
s.NoError(err)
s.Equal(expectedFeatureDecision, actualFeatureDecision)

// Verify CMAB UUID specifically
s.NotNil(actualFeatureDecision.CmabUUID, "CmabUUID should not be nil")
s.Equal(testUUID, *actualFeatureDecision.CmabUUID, "CmabUUID should match the expected value")

s.mockExperimentService.AssertExpectations(s.T())
}

func TestFeatureExperimentServiceTestSuite(t *testing.T) {
suite.Run(t, new(FeatureExperimentServiceTestSuite))
}
11 changes: 6 additions & 5 deletions pkg/event/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,12 @@ type ImpressionEvent struct {

// DecisionMetadata captures additional information regarding the decision
type DecisionMetadata struct {
FlagKey string `json:"flag_key"`
RuleKey string `json:"rule_key"`
RuleType string `json:"rule_type"`
VariationKey string `json:"variation_key"`
Enabled bool `json:"enabled"`
FlagKey string `json:"flag_key"`
RuleKey string `json:"rule_key"`
RuleType string `json:"rule_type"`
VariationKey string `json:"variation_key"`
Enabled bool `json:"enabled"`
CmabUUID *string `json:"cmab_uuid,omitempty"`
}

// ConversionEvent represents a conversion event
Expand Down
25 changes: 22 additions & 3 deletions pkg/event/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,15 @@ func createImpressionEvent(
variation *entities.Variation,
attributes map[string]interface{},
flagKey, ruleKey, ruleType string, enabled bool,
cmabUUID *string,
) ImpressionEvent {

metadata := DecisionMetadata{
FlagKey: flagKey,
RuleKey: ruleKey,
RuleType: ruleType,
Enabled: enabled,
CmabUUID: cmabUUID,
}

var variationID string
Expand All @@ -94,14 +96,31 @@ func createImpressionEvent(
}

// CreateImpressionUserEvent creates and returns ImpressionEvent for user
func CreateImpressionUserEvent(projectConfig config.ProjectConfig, experiment entities.Experiment,
variation *entities.Variation, userContext entities.UserContext, flagKey, ruleKey, ruleType string, enabled bool) (UserEvent, bool) {
func CreateImpressionUserEvent(
projectConfig config.ProjectConfig,
experiment entities.Experiment,
variation *entities.Variation,
userContext entities.UserContext,
flagKey, ruleKey, ruleType string,
enabled bool,
cmabUUID *string,
) (UserEvent, bool) {

if (ruleType == decisionPkg.Rollout || variation == nil) && !projectConfig.SendFlagDecisions() {
return UserEvent{}, false
}

impression := createImpressionEvent(projectConfig, experiment, variation, userContext.Attributes, flagKey, ruleKey, ruleType, enabled)
impression := createImpressionEvent(
projectConfig,
experiment,
variation,
userContext.Attributes,
flagKey,
ruleKey,
ruleType,
enabled,
cmabUUID,
)

userEvent := UserEvent{}
userEvent.Timestamp = makeTimestamp()
Expand Down
96 changes: 92 additions & 4 deletions pkg/event/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ var userContext = entities.UserContext{

func BuildTestImpressionEvent() UserEvent {
tc := TestConfig{}
impressionUserEvent, _ := CreateImpressionUserEvent(tc, testExperiment, &testVariation, userContext, "", testExperiment.Key, "experiment", true)
impressionUserEvent, _ := CreateImpressionUserEvent(tc, testExperiment, &testVariation, userContext, "", testExperiment.Key, "experiment", true, nil)
return impressionUserEvent
}

Expand Down Expand Up @@ -202,7 +202,7 @@ func TestCreateImpressionUserEvent(t *testing.T) {
}

for _, scenario := range scenarios {
userEvent, ok := CreateImpressionUserEvent(tc, testExperiment, &testVariation, userContext, "", testExperiment.Key, scenario.flagType, true)
userEvent, ok := CreateImpressionUserEvent(tc, testExperiment, &testVariation, userContext, "", testExperiment.Key, scenario.flagType, true, nil)
assert.Equal(t, ok, scenario.expected)

if ok {
Expand All @@ -216,7 +216,7 @@ func TestCreateImpressionUserEvent(t *testing.T) {

// nil variation should _always_ return false
for _, scenario := range scenarios {
userEvent, ok := CreateImpressionUserEvent(tc, testExperiment, nil, userContext, "", testExperiment.Key, scenario.flagType, false)
userEvent, ok := CreateImpressionUserEvent(tc, testExperiment, nil, userContext, "", testExperiment.Key, scenario.flagType, false, nil)
assert.False(t, ok)
if ok {
metaData := userEvent.Impression.Metadata
Expand All @@ -230,7 +230,7 @@ func TestCreateImpressionUserEvent(t *testing.T) {
// should _always_ return true if sendFlagDecisions is set
tc.sendFlagDecisions = true
for _, scenario := range scenarios {
userEvent, ok := CreateImpressionUserEvent(tc, testExperiment, nil, userContext, "", testExperiment.Key, scenario.flagType, true)
userEvent, ok := CreateImpressionUserEvent(tc, testExperiment, nil, userContext, "", testExperiment.Key, scenario.flagType, true, nil)
assert.True(t, ok)
if ok {
metaData := userEvent.Impression.Metadata
Expand All @@ -241,3 +241,91 @@ func TestCreateImpressionUserEvent(t *testing.T) {
}
}
}

func TestCreateImpressionUserEventWithCmabUUID(t *testing.T) {
tc := TestConfig{}

// Create a test UUID
testUUID := "test-cmab-uuid-12345"

// Test with various rule types
scenarios := []struct {
flagType string
enabled bool
expected bool
}{
{decision.FeatureTest, true, true},
{"experiment", true, true},
{"anything-else", true, true},
{decision.Rollout, true, false}, // Should return false for Rollout
}

for _, scenario := range scenarios {
// Call CreateImpressionUserEvent with CMAB UUID
userEvent, ok := CreateImpressionUserEvent(
tc,
testExperiment,
&testVariation,
userContext,
"test-flag",
testExperiment.Key,
scenario.flagType,
scenario.enabled,
&testUUID, // Add CMAB UUID
)

assert.Equal(t, scenario.expected, ok)

if ok {
// Verify basic metadata
metaData := userEvent.Impression.Metadata
assert.Equal(t, "test-flag", metaData.FlagKey)
assert.Equal(t, testExperiment.Key, metaData.RuleKey)
assert.Equal(t, scenario.flagType, metaData.RuleType)
assert.Equal(t, scenario.enabled, metaData.Enabled)

// Verify CMAB UUID
assert.NotNil(t, metaData.CmabUUID)
assert.Equal(t, testUUID, *metaData.CmabUUID)
}
}

// Test with nil CMAB UUID - should still work but with nil UUID
for _, scenario := range scenarios {
userEvent, ok := CreateImpressionUserEvent(
tc,
testExperiment,
&testVariation,
userContext,
"test-flag",
testExperiment.Key,
scenario.flagType,
scenario.enabled,
nil, // Nil CMAB UUID
)

assert.Equal(t, scenario.expected, ok)

if ok {
metaData := userEvent.Impression.Metadata
assert.Nil(t, metaData.CmabUUID, "CmabUUID should be nil when not provided")
}
}

// Test with sendFlagDecisions=true
tc.sendFlagDecisions = true
userEvent, ok := CreateImpressionUserEvent(
tc,
testExperiment,
&testVariation,
userContext,
"test-flag",
testExperiment.Key,
decision.Rollout, // This would normally return false
true,
&testUUID,
)
assert.True(t, ok)
metaData := userEvent.Impression.Metadata
assert.Equal(t, testUUID, *metaData.CmabUUID)
}
Loading