From a6a130dd445017c19d87ccacedd89efa9cd278d1 Mon Sep 17 00:00:00 2001 From: Sahil Date: Tue, 19 May 2026 11:13:24 +0530 Subject: [PATCH 1/3] Enhance EFV audit logs --- api/features/versioning/tasks.py | 38 ++++++++- .../unit/audit/test_unit_audit_services.py | 82 +++++++++++++++++++ 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/api/features/versioning/tasks.py b/api/features/versioning/tasks.py index abb885a88ad7..0783f9996c85 100644 --- a/api/features/versioning/tasks.py +++ b/api/features/versioning/tasks.py @@ -261,6 +261,18 @@ def trigger_update_version_webhooks(environment_feature_version_uuid: str) -> No ) +def _build_feature_state_change_summary(fs: FeatureState) -> str: + if fs.identity_id: + scope = f"Identity override ({fs.identity.identifier})" # type: ignore[union-attr] + elif fs.feature_segment_id: + scope = f"Segment override ({fs.feature_segment.segment.name})" # type: ignore[union-attr] + else: + scope = "Environment default" + + state = "enabled" if fs.enabled else "disabled" + return f"{scope}: {state}" + + @register_task_handler() def create_environment_feature_version_published_audit_log_task( environment_feature_version_uuid: str, @@ -269,12 +281,34 @@ def create_environment_feature_version_published_audit_log_task( "environment", "feature" ).get(uuid=environment_feature_version_uuid) + header = ( + ENVIRONMENT_FEATURE_VERSION_PUBLISHED_MESSAGE + % environment_feature_version.feature.name + ) + + changed_states = get_updated_feature_states_for_version( + environment_feature_version + ) + + if changed_states: + changed_states = list( + FeatureState.objects.filter( + id__in=[fs.id for fs in changed_states] + ).select_related("feature_segment__segment", "identity") + ) + change_lines = "\n".join( + f"- {_build_feature_state_change_summary(fs)}" + for fs in changed_states + ) + log = f"{header}\n{change_lines}" + else: + log = header + AuditLog.objects.create( environment=environment_feature_version.environment, related_object_type=RelatedObjectType.EF_VERSION.name, related_object_uuid=environment_feature_version.uuid, - log=ENVIRONMENT_FEATURE_VERSION_PUBLISHED_MESSAGE - % environment_feature_version.feature.name, + log=log, author_id=environment_feature_version.published_by_id, master_api_key_id=environment_feature_version.published_by_api_key_id, ) diff --git a/api/tests/unit/audit/test_unit_audit_services.py b/api/tests/unit/audit/test_unit_audit_services.py index 48308bc97dd1..b028e1c7571f 100644 --- a/api/tests/unit/audit/test_unit_audit_services.py +++ b/api/tests/unit/audit/test_unit_audit_services.py @@ -98,6 +98,88 @@ def test_get_audited_instance_from_audit_log_record__historical_record__return_e assert instance == change_request +def test_create_environment_feature_version_published_audit_log_task__no_changes__uses_header_only( + environment_v2_versioning: Environment, + feature: Feature, +) -> None: + # Given + version = EnvironmentFeatureVersion.objects.create( + feature=feature, + environment=environment_v2_versioning, + ) + + # When + create_environment_feature_version_published_audit_log_task(str(version.uuid)) + + # Then + audit_log = AuditLog.objects.get( + related_object_type=RelatedObjectType.EF_VERSION.name, + related_object_uuid=version.uuid, + ) + assert audit_log.log == f"New version published for feature: {feature.name}" + + +def test_create_environment_feature_version_published_audit_log_task__environment_default_changed__includes_detail( + environment_v2_versioning: Environment, + feature: Feature, +) -> None: + # Given + version = EnvironmentFeatureVersion.objects.create( + feature=feature, + environment=environment_v2_versioning, + ) + fs = version.feature_states.filter(feature=feature).first() + assert fs is not None + fs.enabled = not fs.enabled + fs.save() + + # When + create_environment_feature_version_published_audit_log_task(str(version.uuid)) + + # Then + audit_log = AuditLog.objects.get( + related_object_type=RelatedObjectType.EF_VERSION.name, + related_object_uuid=version.uuid, + ) + assert f"New version published for feature: {feature.name}" in audit_log.log + assert "Environment default:" in audit_log.log + + +def test_create_environment_feature_version_published_audit_log_task__segment_override_changed__includes_segment_name( + environment_v2_versioning: Environment, + feature: Feature, + segment: Segment, +) -> None: + # Given + version = EnvironmentFeatureVersion.objects.create( + feature=feature, + environment=environment_v2_versioning, + ) + feature_segment = FeatureSegment.objects.create( + feature=feature, + segment=segment, + environment=environment_v2_versioning, + environment_feature_version=version, + ) + FeatureState.objects.create( + feature=feature, + environment=environment_v2_versioning, + feature_segment=feature_segment, + environment_feature_version=version, + enabled=True, + ) + + # When + create_environment_feature_version_published_audit_log_task(str(version.uuid)) + + # Then + audit_log = AuditLog.objects.get( + related_object_type=RelatedObjectType.EF_VERSION.name, + related_object_uuid=version.uuid, + ) + assert f"Segment override ({segment.name})" in audit_log.log + + def test_get_audited_instance_from_audit_log_record__unexpected_audit_log__return_none( change_request: ChangeRequest, ) -> None: From 3e965522081bc4223ae667127a415ffd3730a519 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 06:09:22 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- api/features/versioning/tasks.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/api/features/versioning/tasks.py b/api/features/versioning/tasks.py index 0783f9996c85..26f56e70e909 100644 --- a/api/features/versioning/tasks.py +++ b/api/features/versioning/tasks.py @@ -286,9 +286,7 @@ def create_environment_feature_version_published_audit_log_task( % environment_feature_version.feature.name ) - changed_states = get_updated_feature_states_for_version( - environment_feature_version - ) + changed_states = get_updated_feature_states_for_version(environment_feature_version) if changed_states: changed_states = list( @@ -297,8 +295,7 @@ def create_environment_feature_version_published_audit_log_task( ).select_related("feature_segment__segment", "identity") ) change_lines = "\n".join( - f"- {_build_feature_state_change_summary(fs)}" - for fs in changed_states + f"- {_build_feature_state_change_summary(fs)}" for fs in changed_states ) log = f"{header}\n{change_lines}" else: From 3eb06b636b432f78ac501384f3f8694c0f75931c Mon Sep 17 00:00:00 2001 From: Sahil Date: Tue, 19 May 2026 11:42:04 +0530 Subject: [PATCH 3/3] fix: Add missing Segment import in test --- .../unit/audit/test_unit_audit_services.py | 31 +++++++++++++++++++ .../versioning/test_unit_versioning_views.py | 5 ++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/api/tests/unit/audit/test_unit_audit_services.py b/api/tests/unit/audit/test_unit_audit_services.py index b028e1c7571f..e0ce6b067b0b 100644 --- a/api/tests/unit/audit/test_unit_audit_services.py +++ b/api/tests/unit/audit/test_unit_audit_services.py @@ -5,6 +5,7 @@ create_feature_state_updated_by_change_request_audit_log, create_segment_priorities_changed_audit_log, ) +from environments.identities.models import Identity from environments.models import Environment from features.models import Feature, FeatureSegment, FeatureState from features.versioning.models import EnvironmentFeatureVersion @@ -12,6 +13,7 @@ create_environment_feature_version_published_audit_log_task, ) from features.workflows.core.models import ChangeRequest +from segments.models import Segment def test_get_audited_instance_from_audit_log_record__change_request__return_expected( @@ -180,6 +182,35 @@ def test_create_environment_feature_version_published_audit_log_task__segment_ov assert f"Segment override ({segment.name})" in audit_log.log +def test_create_environment_feature_version_published_audit_log_task__identity_override_changed__includes_identity( + environment_v2_versioning: Environment, + feature: Feature, + identity: Identity, +) -> None: + # Given + version = EnvironmentFeatureVersion.objects.create( + feature=feature, + environment=environment_v2_versioning, + ) + FeatureState.objects.create( + feature=feature, + environment=environment_v2_versioning, + identity=identity, + environment_feature_version=version, + enabled=True, + ) + + # When + create_environment_feature_version_published_audit_log_task(str(version.uuid)) + + # Then + audit_log = AuditLog.objects.get( + related_object_type=RelatedObjectType.EF_VERSION.name, + related_object_uuid=version.uuid, + ) + assert f"Identity override ({identity.identifier})" in audit_log.log + + def test_get_audited_instance_from_audit_log_record__unexpected_audit_log__return_none( change_request: ChangeRequest, ) -> None: diff --git a/api/tests/unit/features/versioning/test_unit_versioning_views.py b/api/tests/unit/features/versioning/test_unit_versioning_views.py index ba5f4003ac14..6f4cfe2a6d68 100644 --- a/api/tests/unit/features/versioning/test_unit_versioning_views.py +++ b/api/tests/unit/features/versioning/test_unit_versioning_views.py @@ -336,7 +336,10 @@ def test_publish_feature_version__unpublished_version__publishes_and_creates_aud related_object_uuid=environment_feature_version.uuid, ).first() assert record - assert record.log == ENVIRONMENT_FEATURE_VERSION_PUBLISHED_MESSAGE % feature.name + assert record.log == ( + f"{ENVIRONMENT_FEATURE_VERSION_PUBLISHED_MESSAGE % feature.name}\n" + f"- Environment default: disabled" + ) @pytest.mark.parametrize("live_from", (None, tomorrow))