diff --git a/pkg/client/client.go b/pkg/client/client.go index 1f1ce75f..f55ddbba 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -873,9 +873,9 @@ func (o *OptimizelyClient) GetDetailedFeatureDecisionUnsafe(featureKey string, u if featureDecision.Variation != nil { decisionInfo.Enabled = featureDecision.Variation.FeatureEnabled - // This information is only necessary for feature tests. - // For rollouts experiments and variations are an implementation detail only. - if featureDecision.Source == decision.FeatureTest { + // Include experiment and variation info for feature tests and holdouts. + // For rollouts, experiments and variations are an implementation detail only. + if featureDecision.Source == decision.FeatureTest || featureDecision.Source == decision.Holdout { decisionInfo.VariationKey = featureDecision.Variation.Key decisionInfo.ExperimentKey = featureDecision.Experiment.Key } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 10b30ffb..c1b2f873 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -2131,6 +2131,56 @@ func TestGetDetailedFeatureDecisionUnsafeWithFeatureTestAndTrackingEnabled(t *te assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } +func TestGetDetailedFeatureDecisionUnsafeWithHoldoutAndTrackingEnabled(t *testing.T) { + mockConfig := new(MockProjectConfig) + mockConfigManager := new(MockProjectConfigManager) + mockDecisionService := new(MockDecisionService) + mockEventProcessor := new(MockEventProcessor) + testUserContext := entities.UserContext{ID: "test_user_1"} + + // Test holdout decision path + testVariation := makeTestVariation("control", true) + testExperiment := makeTestExperimentWithVariations("holdout_1", []entities.Variation{testVariation}) + testFeature := makeTestFeatureWithExperiment("feature_1", testExperiment) + mockConfig.On("GetFeatureByKey", testFeature.Key).Return(testFeature, nil) + mockConfigManager.On("GetConfig").Return(mockConfig, nil) + mockEventProcessor.On("ProcessEvent", mock.AnythingOfType("event.UserEvent")) + + // Set up the mock decision service with holdout source + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := decision.FeatureDecision{ + Experiment: testExperiment, + Variation: &testVariation, + Source: decision.Holdout, + } + + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, &decide.Options{}).Return(expectedFeatureDecision, decide.NewDecisionReasons(nil), nil) + + client := OptimizelyClient{ + ConfigManager: mockConfigManager, + DecisionService: mockDecisionService, + EventProcessor: mockEventProcessor, + logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, + } + + decision, err := client.GetDetailedFeatureDecisionUnsafe(testFeature.Key, testUserContext, false) + assert.NoError(t, err) + assert.True(t, decision.Enabled) + assert.Equal(t, decision.ExperimentKey, "holdout_1") + assert.Equal(t, decision.VariationKey, "control") + + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) + mockEventProcessor.AssertExpectations(t) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) +} + func TestGetAllFeatureVariables(t *testing.T) { testFeatureKey := "test_feature_key" testUserContext := entities.UserContext{ID: "test_user_1"}