diff --git a/manifests/cpp.yml b/manifests/cpp.yml index d72dbcf9ecc..bab4fbb4a6a 100644 --- a/manifests/cpp.yml +++ b/manifests/cpp.yml @@ -94,6 +94,7 @@ tests/: Test_TracerSCITagging: missing_feature test_tracer_flare.py: missing_feature test_feature_flag_exposures.py: + Test_FFE_Config_Update: missing_feature Test_FFE_Exposure_Events: missing_feature Test_FFE_Exposure_Events_Empty: missing_feature Test_FFE_Exposure_Events_Errors: missing_feature diff --git a/manifests/cpp_httpd.yml b/manifests/cpp_httpd.yml index 846241a5484..70981991791 100644 --- a/manifests/cpp_httpd.yml +++ b/manifests/cpp_httpd.yml @@ -75,6 +75,7 @@ tests/: Test_Span_Links_From_Conflicting_Contexts: missing_feature (baggage should be implemented and conflicting trace contexts should generate span link) Test_Span_Links_Omit_Tracestate_From_Conflicting_Contexts: missing_feature (implementation specs have not been determined) test_feature_flag_exposures.py: + Test_FFE_Config_Update: missing_feature Test_FFE_Exposure_Events: missing_feature Test_FFE_Exposure_Events_Empty: missing_feature Test_FFE_Exposure_Events_Errors: missing_feature diff --git a/manifests/cpp_nginx.yml b/manifests/cpp_nginx.yml index 0071a50937e..daac7caa0de 100644 --- a/manifests/cpp_nginx.yml +++ b/manifests/cpp_nginx.yml @@ -274,6 +274,7 @@ tests/: Test_Span_Links_From_Conflicting_Contexts: missing_feature (baggage should be implemented and conflicting trace contexts should generate span link) Test_Span_Links_Omit_Tracestate_From_Conflicting_Contexts: missing_feature (implementation specs have not been determined) test_feature_flag_exposures.py: + Test_FFE_Config_Update: missing_feature Test_FFE_Exposure_Events: missing_feature Test_FFE_Exposure_Events_Empty: missing_feature Test_FFE_Exposure_Events_Errors: missing_feature diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index a2e1031e6ab..abd8413e988 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -692,6 +692,7 @@ tests/: Test_Span_Links_From_Conflicting_Contexts: v3.26.3 Test_Span_Links_Omit_Tracestate_From_Conflicting_Contexts: missing_feature (implementation specs have not been determined) test_feature_flag_exposures.py: + Test_FFE_Config_Update: missing_feature Test_FFE_Exposure_Events: missing_feature Test_FFE_Exposure_Events_Empty: missing_feature Test_FFE_Exposure_Events_Errors: missing_feature diff --git a/manifests/golang.yml b/manifests/golang.yml index 8868fd808e7..2e384c1c6f4 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -882,6 +882,7 @@ tests/: Test_Span_Links_Omit_Tracestate_From_Conflicting_Contexts: missing_feature (implementation specs have not been determined) Test_Synthetics_APM_Datadog: bug (APMAPI-901) # the incoming headers are considered invalid test_feature_flag_exposures.py: + Test_FFE_Config_Update: v2.5.0-dev Test_FFE_Exposure_Events: v2.5.0-dev Test_FFE_Exposure_Events_Empty: v2.5.0-dev Test_FFE_Exposure_Events_Errors: v2.5.0-dev diff --git a/manifests/java.yml b/manifests/java.yml index 47e15d53326..cd6ffbbe763 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -2331,6 +2331,9 @@ tests/: Test_Span_Links_From_Conflicting_Contexts: v1.43.0 Test_Span_Links_Omit_Tracestate_From_Conflicting_Contexts: missing_feature (implementation specs have not been determined) test_feature_flag_exposures.py: + Test_FFE_Config_Update: + '*': irrelevant + spring-boot: v1.56.0 Test_FFE_Exposure_Events: '*': irrelevant spring-boot: v1.56.0 diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index 20233033ab3..2eff44b0893 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -1482,6 +1482,9 @@ tests/: '*': *ref_5_25_0 nextjs: bug (APMAPI-939) # the nextjs weblog application changes the sampling priority from 1.0 to 2.0 test_feature_flag_exposures.py: + Test_FFE_Config_Update: + '*': incomplete_test_app + express4: *ref_5_77_0 # only target express 4 Test_FFE_Exposure_Events: '*': incomplete_test_app express4: *ref_5_77_0 # only target express 4 diff --git a/manifests/php.yml b/manifests/php.yml index ec38e5842af..5d941ece2a0 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -685,6 +685,7 @@ tests/: Test_Span_Links_From_Conflicting_Contexts: missing_feature Test_Span_Links_Omit_Tracestate_From_Conflicting_Contexts: missing_feature (implementation specs have not been determined) test_feature_flag_exposures.py: + Test_FFE_Config_Update: missing_feature Test_FFE_Exposure_Events: missing_feature Test_FFE_Exposure_Events_Empty: missing_feature Test_FFE_Exposure_Events_Errors: missing_feature diff --git a/manifests/python.yml b/manifests/python.yml index b33a9f7f149..46938485fdd 100644 --- a/manifests/python.yml +++ b/manifests/python.yml @@ -1292,6 +1292,7 @@ tests/: Test_Span_Links_From_Conflicting_Contexts: v2.17.0 Test_Span_Links_Omit_Tracestate_From_Conflicting_Contexts: missing_feature (implementation specs have not been determined) test_feature_flag_exposures.py: + Test_FFE_Config_Update: v4.0.0 Test_FFE_Exposure_Events: v4.0.0 Test_FFE_Exposure_Events_Empty: v4.0.0 Test_FFE_Exposure_Events_Errors: v4.0.0 diff --git a/manifests/ruby.yml b/manifests/ruby.yml index 9014959559b..70d3c2232be 100644 --- a/manifests/ruby.yml +++ b/manifests/ruby.yml @@ -879,11 +879,12 @@ tests/: Test_Span_Links_From_Conflicting_Contexts: missing_feature Test_Span_Links_Omit_Tracestate_From_Conflicting_Contexts: missing_feature (implementation specs have not been determined) test_feature_flag_exposures.py: - Test_FFE_Exposure_Events: missing_feature - Test_FFE_Exposure_Events_Empty: missing_feature - Test_FFE_Exposure_Events_Errors: missing_feature - Test_FFE_RC_Down_From_Start: missing_feature - Test_FFE_RC_Unavailable: missing_feature + Test_FFE_Config_Update: v2.23.0+6cb632a8089d21b2b5a7ca3ca5a198dcaa566bef + Test_FFE_Exposure_Events: v2.23.0+6cb632a8089d21b2b5a7ca3ca5a198dcaa566bef + Test_FFE_Exposure_Events_Empty: v2.23.0+6cb632a8089d21b2b5a7ca3ca5a198dcaa566bef + Test_FFE_Exposure_Events_Errors: v2.23.0+6cb632a8089d21b2b5a7ca3ca5a198dcaa566bef + Test_FFE_RC_Down_From_Start: v2.23.0+6cb632a8089d21b2b5a7ca3ca5a198dcaa566bef + Test_FFE_RC_Unavailable: v2.23.0+6cb632a8089d21b2b5a7ca3ca5a198dcaa566bef test_graphql.py: Test_GraphQLOperationErrorReporting: "*": missing_feature diff --git a/manifests/rust.yml b/manifests/rust.yml index 968e7ff912e..1082ee661a2 100644 --- a/manifests/rust.yml +++ b/manifests/rust.yml @@ -93,6 +93,7 @@ tests/: test_tracer_flare.py: TestTracerFlareV1: missing_feature test_feature_flag_exposures.py: + Test_FFE_Config_Update: missing_feature Test_FFE_Exposure_Events: missing_feature Test_FFE_Exposure_Events_Empty: missing_feature Test_FFE_Exposure_Events_Errors: missing_feature diff --git a/tests/test_feature_flag_exposures.py b/tests/test_feature_flag_exposures.py index 235e6c22c34..6c0fb2820d0 100644 --- a/tests/test_feature_flag_exposures.py +++ b/tests/test_feature_flag_exposures.py @@ -590,3 +590,119 @@ def test_ffe_rc_down_from_start(self): assert result["value"] == self.default_value, ( f"Expected default '{self.default_value}', got '{result['value']}'" ) + + +@scenarios.feature_flag_exposure +@features.feature_flag_exposure +class Test_FFE_Config_Update: + """Test that FFE correctly updates flag values when remote config is updated.""" + + def setup_ffe_config_update_changes_flag_value(self): + """Set up FFE with initial config, evaluate, update config, and evaluate again.""" + config_id = "ffe-update-test-config" + self.flag_key = "test-flag-updatable" + self.targeting_key = "test-user-update" + self.initial_value = "initial-value" + self.updated_value = "updated-value" + self.default_value = "default" + + # Initial configuration with first value + initial_config = { + "id": "1", + "createdAt": "2024-04-17T19:40:53.716Z", + "format": "SERVER", + "environment": {"name": "Test"}, + "flags": { + self.flag_key: { + "key": self.flag_key, + "enabled": True, + "variationType": "STRING", + "variations": { + "initial": {"key": "initial", "value": self.initial_value}, + "updated": {"key": "updated", "value": self.updated_value}, + }, + "allocations": [ + { + "key": "default-allocation", + "rules": [], + "splits": [{"variationKey": "initial", "shards": []}], + "doLog": True, + } + ], + } + }, + } + + # Reset and apply initial config + rc.rc_state.reset().set_config(f"{RC_PATH}/{config_id}/config", initial_config).apply() + + # Evaluate flag with initial config + self.r1 = weblog.post( + "/ffe", + json={ + "flag": self.flag_key, + "variationType": "STRING", + "defaultValue": self.default_value, + "targetingKey": self.targeting_key, + "attributes": {}, + }, + ) + + # Updated configuration with second value + updated_config = { + "id": "2", + "createdAt": "2024-04-17T19:41:00.000Z", + "format": "SERVER", + "environment": {"name": "Test"}, + "flags": { + self.flag_key: { + "key": self.flag_key, + "enabled": True, + "variationType": "STRING", + "variations": { + "initial": {"key": "initial", "value": self.initial_value}, + "updated": {"key": "updated", "value": self.updated_value}, + }, + "allocations": [ + { + "key": "default-allocation", + "rules": [], + "splits": [{"variationKey": "updated", "shards": []}], + "doLog": True, + } + ], + } + }, + } + + # Apply updated config + rc.rc_state.set_config(f"{RC_PATH}/{config_id}/config", updated_config).apply() + + # Evaluate flag with updated config + self.r2 = weblog.post( + "/ffe", + json={ + "flag": self.flag_key, + "variationType": "STRING", + "defaultValue": self.default_value, + "targetingKey": self.targeting_key, + "attributes": {}, + }, + ) + + def test_ffe_config_update_changes_flag_value(self): + """Test that flag evaluation returns updated value after config update.""" + assert self.r1.status_code == 200, f"First flag evaluation failed: {self.r1.text}" + assert self.r2.status_code == 200, f"Second flag evaluation failed: {self.r2.text}" + + # Verify first evaluation returned initial value + result1 = json.loads(self.r1.text) + assert result1["value"] == self.initial_value, ( + f"First evaluation: expected '{self.initial_value}', got '{result1['value']}'" + ) + + # Verify second evaluation returned updated value + result2 = json.loads(self.r2.text) + assert result2["value"] == self.updated_value, ( + f"Second evaluation: expected '{self.updated_value}', got '{result2['value']}'" + )