Skip to content

Commit 0d843a4

Browse files
authored
[FSSDK-11178] Update impression event for CMAB (#408)
* add cmab impresison event logic * remove unnecessary unmarshaling from a unit test
1 parent 87f0718 commit 0d843a4

10 files changed

+376
-16
lines changed

pkg/client/client.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ func (o *OptimizelyClient) decide(userContext *OptimizelyUserContext, key string
218218

219219
if !allOptions.DisableDecisionEvent {
220220
if ue, ok := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, featureDecision.Experiment,
221-
featureDecision.Variation, usrContext, key, featureDecision.Experiment.Key, featureDecision.Source, flagEnabled); ok {
221+
featureDecision.Variation, usrContext, key, featureDecision.Experiment.Key, featureDecision.Source, flagEnabled, featureDecision.CmabUUID); ok {
222222
o.EventProcessor.ProcessEvent(ue)
223223
eventSent = true
224224
}
@@ -460,7 +460,7 @@ func (o *OptimizelyClient) Activate(experimentKey string, userContext entities.U
460460
// send an impression event
461461
result = experimentDecision.Variation.Key
462462
if ue, ok := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, *decisionContext.Experiment,
463-
experimentDecision.Variation, userContext, "", experimentKey, "experiment", true); ok {
463+
experimentDecision.Variation, userContext, "", experimentKey, "experiment", true, experimentDecision.CmabUUID); ok {
464464
o.EventProcessor.ProcessEvent(ue)
465465
}
466466
}
@@ -518,7 +518,7 @@ func (o *OptimizelyClient) IsFeatureEnabled(featureKey string, userContext entit
518518
}
519519

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

@@ -883,7 +883,7 @@ func (o *OptimizelyClient) GetDetailedFeatureDecisionUnsafe(featureKey string, u
883883
if !disableTracking {
884884
// send impression event for feature tests
885885
if ue, ok := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, featureDecision.Experiment,
886-
featureDecision.Variation, userContext, featureKey, featureDecision.Experiment.Key, featureDecision.Source, decisionInfo.Enabled); ok {
886+
featureDecision.Variation, userContext, featureKey, featureDecision.Experiment.Key, featureDecision.Source, decisionInfo.Enabled, featureDecision.CmabUUID); ok {
887887
o.EventProcessor.ProcessEvent(ue)
888888
}
889889
}

pkg/decision/entities.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ type FeatureDecision struct {
6868
Source Source
6969
Experiment entities.Experiment
7070
Variation *entities.Variation
71+
CmabUUID *string
7172
}
7273

7374
// ExperimentDecision contains the decision information about an experiment

pkg/decision/experiment_cmab_service.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ func (s *ExperimentCmabService) GetDecision(decisionContext ExperimentDecisionCo
175175
decision.Variation = &variationCopy
176176
decision.Reason = pkgReasons.CmabVariationAssigned
177177

178+
// Store CMAB UUID in the decision
179+
if cmabDecision.CmabUUID != "" {
180+
cmabUUIDCopy := cmabDecision.CmabUUID
181+
decision.CmabUUID = &cmabUUIDCopy
182+
}
183+
178184
message := fmt.Sprintf("User bucketed into variation %s by CMAB service", variation.Key)
179185
decisionReasons.AddInfo(message)
180186
return decision, decisionReasons, nil

pkg/decision/experiment_cmab_service_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,58 @@ func (s *ExperimentCmabTestSuite) TestCreateCmabExperimentEmptyFields() {
677677
s.Equal(5000, result.TrafficAllocation[0].EndOfRange)
678678
}
679679

680+
func (s *ExperimentCmabTestSuite) TestGetDecisionWithCmabUUID() {
681+
// Create decision context with CMAB experiment
682+
decisionContext := ExperimentDecisionContext{
683+
Experiment: &s.cmabExperiment,
684+
ProjectConfig: s.mockProjectConfig,
685+
}
686+
687+
// Expected UUID that should be propagated
688+
expectedUUID := "test-uuid-12345"
689+
690+
// Mock bucketer to return CMAB dummy entity ID (so traffic allocation passes)
691+
s.mockExperimentBucketer.On("BucketToEntityID", mock.Anything, mock.AnythingOfType("entities.Experiment"), mock.Anything).
692+
Return(CmabDummyEntityID, reasons.BucketedIntoVariation, nil)
693+
694+
// Setup mock CMAB service to return a decision with UUID
695+
cmabDecision := cmab.Decision{
696+
VariationID: "var1", // This matches an existing variation in s.cmabExperiment
697+
CmabUUID: expectedUUID,
698+
}
699+
s.mockCmabService.On("GetDecision", s.mockProjectConfig, s.testUserContext, "cmab_exp_1", s.options).
700+
Return(cmabDecision, nil)
701+
702+
// Get decision
703+
decision, decisionReasons, err := s.experimentCmabService.GetDecision(decisionContext, s.testUserContext, s.options)
704+
705+
// Verify basic results
706+
s.NoError(err, "Should not return an error")
707+
s.NotNil(decision.Variation, "Should return a variation")
708+
s.Equal("var1", decision.Variation.ID, "Should return the correct variation ID")
709+
s.Equal(reasons.CmabVariationAssigned, decision.Reason, "Should have the correct reason")
710+
711+
// Verify CMAB UUID was captured
712+
s.NotNil(decision.CmabUUID, "CMAB UUID should not be nil")
713+
s.Equal(expectedUUID, *decision.CmabUUID, "CMAB UUID should match the expected value")
714+
715+
// Check for the message in the reasons
716+
report := decisionReasons.ToReport()
717+
s.NotEmpty(report, "Decision reasons report should not be empty")
718+
messageFound := false
719+
for _, msg := range report {
720+
if strings.Contains(msg, "User bucketed into variation") {
721+
messageFound = true
722+
break
723+
}
724+
}
725+
s.True(messageFound, "Expected bucketing message not found in decision reasons")
726+
727+
// Verify mock expectations
728+
s.mockCmabService.AssertExpectations(s.T())
729+
s.mockExperimentBucketer.AssertExpectations(s.T())
730+
}
731+
680732
func TestExperimentCmabTestSuite(t *testing.T) {
681733
suite.Run(t, new(ExperimentCmabTestSuite))
682734
}

pkg/decision/feature_experiment_service.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func (f FeatureExperimentService) GetDecision(decisionContext FeatureDecisionCon
8383
Decision: experimentDecision.Decision,
8484
Variation: experimentDecision.Variation,
8585
Source: FeatureTest,
86+
CmabUUID: experimentDecision.CmabUUID,
8687
}
8788

8889
return featureDecision, reasons, err

pkg/decision/feature_experiment_service_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,59 @@ func (s *FeatureExperimentServiceTestSuite) TestNewFeatureExperimentService() {
177177
s.IsType(compositeExperimentService, featureExperimentService.compositeExperimentService)
178178
}
179179

180+
func (s *FeatureExperimentServiceTestSuite) TestGetDecisionWithCmabUUID() {
181+
testUserContext := entities.UserContext{
182+
ID: "test_user_1",
183+
}
184+
185+
// Create test UUID
186+
testUUID := "test-cmab-uuid-12345"
187+
188+
// Create experiment decision with UUID
189+
expectedVariation := testExp1113.Variations["2223"]
190+
returnExperimentDecision := ExperimentDecision{
191+
Variation: &expectedVariation,
192+
CmabUUID: &testUUID,
193+
}
194+
195+
// Setup experiment decision context
196+
testExperimentDecisionContext := ExperimentDecisionContext{
197+
Experiment: &testExp1113,
198+
ProjectConfig: s.mockConfig,
199+
}
200+
201+
// Setup mock to return experiment decision with UUID
202+
s.mockExperimentService.On("GetDecision", testExperimentDecisionContext, testUserContext, s.options).
203+
Return(returnExperimentDecision, s.reasons, nil)
204+
205+
// Create service under test
206+
featureExperimentService := &FeatureExperimentService{
207+
compositeExperimentService: s.mockExperimentService,
208+
logger: logging.GetLogger("sdkKey", "FeatureExperimentService"),
209+
}
210+
211+
// Create expected feature decision with propagated UUID
212+
expectedFeatureDecision := FeatureDecision{
213+
Experiment: *testExperimentDecisionContext.Experiment,
214+
Variation: &expectedVariation,
215+
Source: FeatureTest,
216+
CmabUUID: &testUUID, // UUID should be propagated
217+
}
218+
219+
// Call GetDecision
220+
actualFeatureDecision, _, err := featureExperimentService.GetDecision(s.testFeatureDecisionContext, testUserContext, s.options)
221+
222+
// Verify results
223+
s.NoError(err)
224+
s.Equal(expectedFeatureDecision, actualFeatureDecision)
225+
226+
// Verify CMAB UUID specifically
227+
s.NotNil(actualFeatureDecision.CmabUUID, "CmabUUID should not be nil")
228+
s.Equal(testUUID, *actualFeatureDecision.CmabUUID, "CmabUUID should match the expected value")
229+
230+
s.mockExperimentService.AssertExpectations(s.T())
231+
}
232+
180233
func TestFeatureExperimentServiceTestSuite(t *testing.T) {
181234
suite.Run(t, new(FeatureExperimentServiceTestSuite))
182235
}

pkg/event/events.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,12 @@ type ImpressionEvent struct {
5151

5252
// DecisionMetadata captures additional information regarding the decision
5353
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"`
58-
Enabled bool `json:"enabled"`
54+
FlagKey string `json:"flag_key"`
55+
RuleKey string `json:"rule_key"`
56+
RuleType string `json:"rule_type"`
57+
VariationKey string `json:"variation_key"`
58+
Enabled bool `json:"enabled"`
59+
CmabUUID *string `json:"cmab_uuid,omitempty"`
5960
}
6061

6162
// ConversionEvent represents a conversion event

pkg/event/factory.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,15 @@ func createImpressionEvent(
6565
variation *entities.Variation,
6666
attributes map[string]interface{},
6767
flagKey, ruleKey, ruleType string, enabled bool,
68+
cmabUUID *string,
6869
) ImpressionEvent {
6970

7071
metadata := DecisionMetadata{
7172
FlagKey: flagKey,
7273
RuleKey: ruleKey,
7374
RuleType: ruleType,
7475
Enabled: enabled,
76+
CmabUUID: cmabUUID,
7577
}
7678

7779
var variationID string
@@ -94,14 +96,31 @@ func createImpressionEvent(
9496
}
9597

9698
// CreateImpressionUserEvent creates and returns ImpressionEvent for user
97-
func CreateImpressionUserEvent(projectConfig config.ProjectConfig, experiment entities.Experiment,
98-
variation *entities.Variation, userContext entities.UserContext, flagKey, ruleKey, ruleType string, enabled bool) (UserEvent, bool) {
99+
func CreateImpressionUserEvent(
100+
projectConfig config.ProjectConfig,
101+
experiment entities.Experiment,
102+
variation *entities.Variation,
103+
userContext entities.UserContext,
104+
flagKey, ruleKey, ruleType string,
105+
enabled bool,
106+
cmabUUID *string,
107+
) (UserEvent, bool) {
99108

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

104-
impression := createImpressionEvent(projectConfig, experiment, variation, userContext.Attributes, flagKey, ruleKey, ruleType, enabled)
113+
impression := createImpressionEvent(
114+
projectConfig,
115+
experiment,
116+
variation,
117+
userContext.Attributes,
118+
flagKey,
119+
ruleKey,
120+
ruleType,
121+
enabled,
122+
cmabUUID,
123+
)
105124

106125
userEvent := UserEvent{}
107126
userEvent.Timestamp = makeTimestamp()

pkg/event/factory_test.go

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ var userContext = entities.UserContext{
103103

104104
func BuildTestImpressionEvent() UserEvent {
105105
tc := TestConfig{}
106-
impressionUserEvent, _ := CreateImpressionUserEvent(tc, testExperiment, &testVariation, userContext, "", testExperiment.Key, "experiment", true)
106+
impressionUserEvent, _ := CreateImpressionUserEvent(tc, testExperiment, &testVariation, userContext, "", testExperiment.Key, "experiment", true, nil)
107107
return impressionUserEvent
108108
}
109109

@@ -202,7 +202,7 @@ func TestCreateImpressionUserEvent(t *testing.T) {
202202
}
203203

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

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

217217
// nil variation should _always_ return false
218218
for _, scenario := range scenarios {
219-
userEvent, ok := CreateImpressionUserEvent(tc, testExperiment, nil, userContext, "", testExperiment.Key, scenario.flagType, false)
219+
userEvent, ok := CreateImpressionUserEvent(tc, testExperiment, nil, userContext, "", testExperiment.Key, scenario.flagType, false, nil)
220220
assert.False(t, ok)
221221
if ok {
222222
metaData := userEvent.Impression.Metadata
@@ -230,7 +230,7 @@ func TestCreateImpressionUserEvent(t *testing.T) {
230230
// should _always_ return true if sendFlagDecisions is set
231231
tc.sendFlagDecisions = true
232232
for _, scenario := range scenarios {
233-
userEvent, ok := CreateImpressionUserEvent(tc, testExperiment, nil, userContext, "", testExperiment.Key, scenario.flagType, true)
233+
userEvent, ok := CreateImpressionUserEvent(tc, testExperiment, nil, userContext, "", testExperiment.Key, scenario.flagType, true, nil)
234234
assert.True(t, ok)
235235
if ok {
236236
metaData := userEvent.Impression.Metadata
@@ -241,3 +241,91 @@ func TestCreateImpressionUserEvent(t *testing.T) {
241241
}
242242
}
243243
}
244+
245+
func TestCreateImpressionUserEventWithCmabUUID(t *testing.T) {
246+
tc := TestConfig{}
247+
248+
// Create a test UUID
249+
testUUID := "test-cmab-uuid-12345"
250+
251+
// Test with various rule types
252+
scenarios := []struct {
253+
flagType string
254+
enabled bool
255+
expected bool
256+
}{
257+
{decision.FeatureTest, true, true},
258+
{"experiment", true, true},
259+
{"anything-else", true, true},
260+
{decision.Rollout, true, false}, // Should return false for Rollout
261+
}
262+
263+
for _, scenario := range scenarios {
264+
// Call CreateImpressionUserEvent with CMAB UUID
265+
userEvent, ok := CreateImpressionUserEvent(
266+
tc,
267+
testExperiment,
268+
&testVariation,
269+
userContext,
270+
"test-flag",
271+
testExperiment.Key,
272+
scenario.flagType,
273+
scenario.enabled,
274+
&testUUID, // Add CMAB UUID
275+
)
276+
277+
assert.Equal(t, scenario.expected, ok)
278+
279+
if ok {
280+
// Verify basic metadata
281+
metaData := userEvent.Impression.Metadata
282+
assert.Equal(t, "test-flag", metaData.FlagKey)
283+
assert.Equal(t, testExperiment.Key, metaData.RuleKey)
284+
assert.Equal(t, scenario.flagType, metaData.RuleType)
285+
assert.Equal(t, scenario.enabled, metaData.Enabled)
286+
287+
// Verify CMAB UUID
288+
assert.NotNil(t, metaData.CmabUUID)
289+
assert.Equal(t, testUUID, *metaData.CmabUUID)
290+
}
291+
}
292+
293+
// Test with nil CMAB UUID - should still work but with nil UUID
294+
for _, scenario := range scenarios {
295+
userEvent, ok := CreateImpressionUserEvent(
296+
tc,
297+
testExperiment,
298+
&testVariation,
299+
userContext,
300+
"test-flag",
301+
testExperiment.Key,
302+
scenario.flagType,
303+
scenario.enabled,
304+
nil, // Nil CMAB UUID
305+
)
306+
307+
assert.Equal(t, scenario.expected, ok)
308+
309+
if ok {
310+
metaData := userEvent.Impression.Metadata
311+
assert.Nil(t, metaData.CmabUUID, "CmabUUID should be nil when not provided")
312+
}
313+
}
314+
315+
// Test with sendFlagDecisions=true
316+
tc.sendFlagDecisions = true
317+
userEvent, ok := CreateImpressionUserEvent(
318+
tc,
319+
testExperiment,
320+
&testVariation,
321+
userContext,
322+
"test-flag",
323+
testExperiment.Key,
324+
decision.Rollout, // This would normally return false
325+
true,
326+
&testUUID,
327+
)
328+
assert.True(t, ok)
329+
metaData := userEvent.Impression.Metadata
330+
assert.Equal(t, testUUID, *metaData.CmabUUID)
331+
}

0 commit comments

Comments
 (0)