diff --git a/api/environments/migrations/0035_add_use_identity_overrides_in_local_eval.py b/api/environments/migrations/0035_add_use_identity_overrides_in_local_eval.py new file mode 100644 index 000000000000..33514ab7c193 --- /dev/null +++ b/api/environments/migrations/0035_add_use_identity_overrides_in_local_eval.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.15 on 2024-09-04 05:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("environments", "0034_alter_environment_project"), + ] + + operations = [ + migrations.AddField( + model_name="environment", + name="use_identity_overrides_in_local_eval", + field=models.BooleanField( + default=False, + help_text="When enabled, identity overrides will be included in the environment document", + ), + ), + migrations.AddField( + model_name="historicalenvironment", + name="use_identity_overrides_in_local_eval", + field=models.BooleanField( + default=False, + help_text="When enabled, identity overrides will be included in the environment document", + ), + ), + # Update the default to true for new environments + migrations.AlterField( + model_name="environment", + name="use_identity_overrides_in_local_eval", + field=models.BooleanField( + default=True, + help_text="When enabled, identity overrides will be included in the environment document", + ), + ), + migrations.AlterField( + model_name="historicalenvironment", + name="use_identity_overrides_in_local_eval", + field=models.BooleanField( + default=True, + help_text="When enabled, identity overrides will be included in the environment document", + ), + ), + ] diff --git a/api/environments/models.py b/api/environments/models.py index dd58ce9fa605..f85b6851d877 100644 --- a/api/environments/models.py +++ b/api/environments/models.py @@ -131,9 +131,12 @@ class Environment( help_text="If true, will hide sensitive data(e.g: traits, description etc) from the SDK endpoints", ) - objects = EnvironmentManager() - use_v2_feature_versioning = models.BooleanField(default=False) + use_identity_overrides_in_local_eval = models.BooleanField( + default=True, + help_text="When enabled, identity overrides will be included in the environment document", + ) + objects = EnvironmentManager() class Meta: ordering = ["id"] diff --git a/api/environments/serializers.py b/api/environments/serializers.py index df033f80dec2..dffc9d13b468 100644 --- a/api/environments/serializers.py +++ b/api/environments/serializers.py @@ -53,6 +53,7 @@ class Meta: "use_identity_composite_key_for_hashing", "hide_sensitive_data", "use_v2_feature_versioning", + "use_identity_overrides_in_local_eval", ) read_only_fields = ("use_v2_feature_versioning",) diff --git a/api/poetry.lock b/api/poetry.lock index 5651e6e22fb7..d5d5e9e7be57 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -1370,12 +1370,12 @@ resolved_reference = "f3809f6d592b2c6cfdfa88e0b345ce722ac47727" [[package]] name = "flagsmith-flag-engine" -version = "5.1.1" +version = "5.2.0" description = "Flag engine for the Flagsmith API." optional = false python-versions = "*" files = [ - {file = "flagsmith-flag-engine-5.1.1.tar.gz", hash = "sha256:a97d001ac50fcddee273a25d8c88442b33797fde5b4d657f3e83e1493aa4f536"}, + {file = "flagsmith-flag-engine-5.2.0.tar.gz", hash = "sha256:247da79a9fee55a55de1440fcd112de9b56cef9d5feb8c783319a9e0620e3a72"}, ] [package.dependencies] @@ -2150,16 +2150,6 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -3343,7 +3333,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -4187,4 +4176,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.11, <3.13" -content-hash = "5c0c058d06a663f5fc5ac21721d16de133bb73782ddaaa42316b4f1a1e2a7726" +content-hash = "992535d7cf696ab58b19e8fa256392af1265d516865b3d9e93aeddc954245184" diff --git a/api/pyproject.toml b/api/pyproject.toml index 4c8649690edc..9fbede74bf50 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -142,7 +142,7 @@ environs = "~9.2.0" django-lifecycle = "~1.0.0" drf-writable-nested = "~0.6.2" django-filter = "~2.4.0" -flagsmith-flag-engine = "^5.1.1" +flagsmith-flag-engine = "^5.2.0" boto3 = "~1.28.78" slack-sdk = "~3.9.0" asgiref = "~3.8.1" diff --git a/api/tests/integration/conftest.py b/api/tests/integration/conftest.py index 5173596ab746..806703c2d0bd 100644 --- a/api/tests/integration/conftest.py +++ b/api/tests/integration/conftest.py @@ -432,6 +432,7 @@ def identity_document( } return { "composite_key": f"{environment_api_key}_user_1_test", + "dashboard_alias": None, "identity_traits": identity_traits, "identity_features": [ _environment_feature_state_1_document, @@ -457,6 +458,7 @@ def identity_document_without_fs(environment_api_key, identity_traits): "environment_api_key": environment_api_key, "identity_uuid": "59efa2a7-6a45-46d6-b953-a7073a90eacf", "django_id": None, + "dashboard_alias": None, } diff --git a/api/tests/unit/edge_api/identities/conftest.py b/api/tests/unit/edge_api/identities/conftest.py index d1fc3b3b0cb6..ab0aed1bdc83 100644 --- a/api/tests/unit/edge_api/identities/conftest.py +++ b/api/tests/unit/edge_api/identities/conftest.py @@ -41,6 +41,7 @@ def identity_document_without_fs(environment): "environment_api_key": environment.api_key, "identity_uuid": "59efa2a7-6a45-46d6-b953-a7073a90eacf", "django_id": None, + "dashboard_alias": None, } diff --git a/api/tests/unit/edge_api/identities/test_unit_edge_api_identities_tasks.py b/api/tests/unit/edge_api/identities/test_unit_edge_api_identities_tasks.py index d5b6d29cea51..3b3907073c38 100644 --- a/api/tests/unit/edge_api/identities/test_unit_edge_api_identities_tasks.py +++ b/api/tests/unit/edge_api/identities/test_unit_edge_api_identities_tasks.py @@ -257,6 +257,7 @@ def test_sync_identity_document_features_removes_deleted_features( "feature_state_value": "feature_1_value", "featurestate_uuid": "4a8fbe06-d4cd-4686-a184-d924844bb422", "django_id": 1, + "dashboard_alias": None, "feature": { "name": "feature_that_does_not_exists", "type": "STANDARD", diff --git a/api/tests/unit/environments/test_unit_environments_views.py b/api/tests/unit/environments/test_unit_environments_views.py index c03448bee9b2..39486bd3c06d 100644 --- a/api/tests/unit/environments/test_unit_environments_views.py +++ b/api/tests/unit/environments/test_unit_environments_views.py @@ -521,6 +521,7 @@ def test_should_create_environments( assert response.status_code == status.HTTP_201_CREATED assert response.json()["description"] == description assert response.json()["use_mv_v2_evaluation"] is True + assert response.json()["use_identity_overrides_in_local_eval"] is True assert response.json()["use_identity_composite_key_for_hashing"] is True # and user is admin @@ -787,6 +788,7 @@ def test_audit_log_entry_created_when_environment_updated( banner_colour = "#FF0000" hide_disabled_flags = True use_identity_composite_key_for_hashing = True + use_identity_overrides_in_local_eval = True hide_sensitive_data = True data = { @@ -796,6 +798,7 @@ def test_audit_log_entry_created_when_environment_updated( "banner_colour": banner_colour, "hide_disabled_flags": hide_disabled_flags, "use_identity_composite_key_for_hashing": use_identity_composite_key_for_hashing, + "use_identity_overrides_in_local_eval": use_identity_overrides_in_local_eval, "hide_sensitive_data": hide_sensitive_data, } @@ -816,6 +819,10 @@ def test_audit_log_entry_created_when_environment_updated( assert response.json()["banner_colour"] == banner_colour assert response.json()["hide_disabled_flags"] == hide_disabled_flags assert response.json()["hide_sensitive_data"] == hide_sensitive_data + assert ( + response.json()["use_identity_overrides_in_local_eval"] + == use_identity_overrides_in_local_eval + ) assert ( response.json()["use_identity_composite_key_for_hashing"] == use_identity_composite_key_for_hashing diff --git a/api/tests/unit/util/mappers/test_unit_mappers_dynamodb.py b/api/tests/unit/util/mappers/test_unit_mappers_dynamodb.py index 9bd6049dbd25..27c75ed91cdc 100644 --- a/api/tests/unit/util/mappers/test_unit_mappers_dynamodb.py +++ b/api/tests/unit/util/mappers/test_unit_mappers_dynamodb.py @@ -76,6 +76,7 @@ def test_map_environment_to_environment_document__call_expected( "segment_config": None, "updated_at": expected_updated_at, "use_identity_composite_key_for_hashing": True, + "use_identity_overrides_in_local_eval": True, "webhook_config": None, } @@ -130,6 +131,7 @@ def test_map_identity_to_identity_document__call_expected( "identity_features": [], "identity_traits": [{"trait_key": "key1", "trait_value": "value1"}], "identity_uuid": mocker.ANY, + "dashboard_alias": None, } assert uuid.UUID(result["identity_uuid"]) @@ -195,6 +197,7 @@ def test_map_environment_to_environment_v2_document__call_expected( "segment_config": None, "updated_at": expected_updated_at, "use_identity_composite_key_for_hashing": True, + "use_identity_overrides_in_local_eval": True, "webhook_config": None, } diff --git a/api/tests/unit/util/mappers/test_unit_mappers_engine.py b/api/tests/unit/util/mappers/test_unit_mappers_engine.py index 6e08270cbbf5..6f2f4d15ba97 100644 --- a/api/tests/unit/util/mappers/test_unit_mappers_engine.py +++ b/api/tests/unit/util/mappers/test_unit_mappers_engine.py @@ -386,6 +386,7 @@ def test_map_environment_to_engine__return_expected( allow_client_traits=environment.allow_client_traits, updated_at=environment.updated_at, use_identity_composite_key_for_hashing=environment.use_identity_composite_key_for_hashing, + use_identity_overrides_in_local_eval=environment.use_identity_overrides_in_local_eval, hide_sensitive_data=environment.hide_sensitive_data, hide_disabled_flags=environment.hide_disabled_flags, amplitude_config=None, diff --git a/api/tests/unit/util/mappers/test_unit_mappers_sdk.py b/api/tests/unit/util/mappers/test_unit_mappers_sdk.py index 7363f1e840db..ce849a4485cc 100644 --- a/api/tests/unit/util/mappers/test_unit_mappers_sdk.py +++ b/api/tests/unit/util/mappers/test_unit_mappers_sdk.py @@ -65,6 +65,7 @@ def test_map_environment_to_sdk_document__return_expected( "django_id": identity.pk, "environment_api_key": expected_api_key, "identifier": identity.identifier, + "dashboard_alias": None, "identity_features": [ { "django_id": identity_featurestate.pk, @@ -105,4 +106,73 @@ def test_map_environment_to_sdk_document__return_expected( }, "updated_at": environment.updated_at, "use_identity_composite_key_for_hashing": True, + "use_identity_overrides_in_local_eval": True, + } + + +def test_map_environment_to_sdk_document__for_environment_with_identity_overrides_in_local_eval_disabled( + mocker: "MockerFixture", + environment: "Environment", + feature_state: "FeatureState", + identity: Identity, + identity_featurestate: "FeatureState", + identity_without_overrides: Identity, +) -> None: + # Given + expected_overridden_value = "some-overridden-value" + expected_api_key = environment.api_key + + identity_featurestate.feature_state_value.string_value = expected_overridden_value + identity_featurestate.feature_state_value.save() + identity_featurestate.enabled = True + identity_featurestate.save() + + environment.use_identity_overrides_in_local_eval = False + environment.save() + + # When + result = map_environment_to_sdk_document(environment) + + # Then + assert result == { + "allow_client_traits": True, + "api_key": expected_api_key, + "feature_states": [ + { + "django_id": feature_state.pk, + "enabled": False, + "feature": { + "id": feature_state.feature.pk, + "name": "Test Feature1", + "type": "STANDARD", + }, + "feature_segment": None, + "feature_state_value": None, + "featurestate_uuid": feature_state.uuid, + "multivariate_feature_state_values": [], + } + ], + "identity_overrides": [], + "hide_disabled_flags": None, + "hide_sensitive_data": False, + "id": environment.pk, + "name": "Test Environment", + "project": { + "enable_realtime_updates": False, + "hide_disabled_flags": False, + "id": environment.project.pk, + "name": "Test Project", + "organisation": { + "feature_analytics": False, + "id": environment.project.organisation.pk, + "name": "Test Org", + "persist_trait_data": True, + "stop_serving_flags": False, + }, + "segments": [], + "server_key_only_feature_ids": [], + }, + "updated_at": environment.updated_at, + "use_identity_composite_key_for_hashing": True, + "use_identity_overrides_in_local_eval": False, } diff --git a/api/util/mappers/engine.py b/api/util/mappers/engine.py index 90150206ed17..9e111d5748d0 100644 --- a/api/util/mappers/engine.py +++ b/api/util/mappers/engine.py @@ -326,6 +326,7 @@ def map_environment_to_engine( use_identity_composite_key_for_hashing=environment.use_identity_composite_key_for_hashing, hide_sensitive_data=environment.hide_sensitive_data, hide_disabled_flags=environment.hide_disabled_flags, + use_identity_overrides_in_local_eval=environment.use_identity_overrides_in_local_eval, # # Relationships: project=project_model, diff --git a/api/util/mappers/sdk.py b/api/util/mappers/sdk.py index bc94def7ae8f..2bc9c4a0ff61 100644 --- a/api/util/mappers/sdk.py +++ b/api/util/mappers/sdk.py @@ -29,11 +29,12 @@ def map_environment_to_sdk_document(environment: "Environment") -> SDKDocument: """ # Read relationships. identities_with_overrides = {} - for feature_state in environment.feature_states.all(): - if (identity_id := feature_state.identity_id) and ( - identity_id not in identities_with_overrides - ): - identities_with_overrides[identity_id] = feature_state.identity + if environment.use_identity_overrides_in_local_eval: + for feature_state in environment.feature_states.all(): + if (identity_id := feature_state.identity_id) and ( + identity_id not in identities_with_overrides + ): + identities_with_overrides[identity_id] = feature_state.identity # Get the engine data. engine_environment = map_environment_to_engine(environment, with_integrations=False)