diff --git a/api/tests/integration/environments/identities/test_integration_identities.py b/api/tests/integration/environments/identities/test_integration_identities.py index 58e865ba3dbb..5be2e646b900 100644 --- a/api/tests/integration/environments/identities/test_integration_identities.py +++ b/api/tests/integration/environments/identities/test_integration_identities.py @@ -73,14 +73,14 @@ def test_get_feature_states_for_identity__mv_percentage_allocation__returns_corr create_mv_option_with_api( admin_client, project, - multivariate_feature_id, # type: ignore[arg-type] + multivariate_feature_id, variant_1_percentage_allocation, variant_1_value, ) variant_2_mvfo_id = create_mv_option_with_api( admin_client, project, - multivariate_feature_id, # type: ignore[arg-type] + multivariate_feature_id, variant_2_percentage_allocation, variant_2_value, ) @@ -193,7 +193,7 @@ def test_get_feature_states_for_identity__mv_allocation__returns_variant( # typ create_mv_option_with_api( admin_client, project, - multivariate_feature_id, # type: ignore[arg-type] + multivariate_feature_id, variant_1_percentage_allocation, variant_1_value, key="variant-1", @@ -201,7 +201,7 @@ def test_get_feature_states_for_identity__mv_allocation__returns_variant( # typ create_mv_option_with_api( admin_client, project, - multivariate_feature_id, # type: ignore[arg-type] + multivariate_feature_id, variant_2_percentage_allocation, variant_2_value, key="variant-2", @@ -242,7 +242,7 @@ def test_get_flags__multivariate_feature__response_excludes_variant( # type: ig create_mv_option_with_api( admin_client, project, - multivariate_feature_id, # type: ignore[arg-type] + multivariate_feature_id, 100, variant_1_value, key="variant-1", @@ -279,14 +279,14 @@ def test_get_feature_states_for_identity__multiple_mv_features__single_mv_query( create_mv_option_with_api( admin_client, project, - feature_id, # type: ignore[arg-type] + feature_id, variant_1_percentage_allocation, variant_1_value, ) create_mv_option_with_api( admin_client, project, - feature_id, # type: ignore[arg-type] + feature_id, variant_2_percentage_allocation, variant_2_value, ) @@ -314,14 +314,14 @@ def test_get_feature_states_for_identity__multiple_mv_features__single_mv_query( create_mv_option_with_api( admin_client, project, - feature_id, # type: ignore[arg-type] + feature_id, variant_1_percentage_allocation, variant_1_value, ) create_mv_option_with_api( admin_client, project, - feature_id, # type: ignore[arg-type] + feature_id, variant_2_percentage_allocation, variant_2_value, ) diff --git a/api/tests/integration/features/experiments/__init__.py b/api/tests/integration/features/experiments/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/api/tests/integration/features/experiments/test_update_flag_endpoint.py b/api/tests/integration/features/experiments/test_update_flag_endpoint.py new file mode 100644 index 000000000000..27e860338095 --- /dev/null +++ b/api/tests/integration/features/experiments/test_update_flag_endpoint.py @@ -0,0 +1,640 @@ +"""https://docs.flagsmith.com/managing-flags/updating-flags""" + +import pytest +from rest_framework.test import APIClient + +from environments.models import Environment +from features.models import FeatureState +from features.versioning.tasks import enable_v2_versioning +from tests.integration.helpers import create_mv_option_with_api + + +@pytest.fixture(params=["feature_versioning_v1", "feature_versioning_v2"], autouse=True) +def versioned_environment( + request: pytest.FixtureRequest, + environment: int, +) -> Environment: + if request.param == "feature_versioning_v2": + enable_v2_versioning(environment_id=environment) + return Environment.objects.get(id=environment) # type: ignore[no-any-return] + + +@pytest.fixture() +def segment_2( + admin_client: APIClient, + project: int, +) -> int: + response = admin_client.post( + f"/api/v1/projects/{project}/segments/", + { + "name": "Test Segment 2", + "project": project, + "rules": [{"type": "ALL", "rules": [], "conditions": []}], + }, + format="json", + ) + return int(response.json()["id"]) + + +@pytest.fixture() +def feature_variants( + admin_client: APIClient, + project: int, + feature: int, +) -> None: + for key, value, default_percentage_allocation in [ + ("variant_a", "a", 10), + ("variant_b", "b", 20), + ]: + create_mv_option_with_api( + admin_client, + project, + feature, + default_percentage_allocation, + value, + key=key, + ) + + +def test_update_flag__environment_default_enabled__toggles_flag( + admin_client: APIClient, + environment_api_key: str, + feature: int, + feature_name: str, + versioned_environment: Environment, +) -> None: + # Given + environment_default = FeatureState.objects.get_live_feature_states( + environment=versioned_environment, + feature_id=feature, + feature_segment=None, + ).get() + assert environment_default.enabled is False + + # When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"name": feature_name}, + "environment_default": {"enabled": True}, + }, + format="json", + ) + + # Then + assert response.status_code == 204 + environment_default = FeatureState.objects.get_live_feature_states( + environment=versioned_environment, + feature_id=feature, + feature_segment=None, + ).get() + assert environment_default.enabled is True + + +def test_update_flag__environment_default_value__updates_value( + admin_client: APIClient, + environment_api_key: str, + feature: int, + versioned_environment: Environment, +) -> None: + # Given / When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "environment_default": { + "enabled": True, + "value": {"type": "integer", "value": "1000"}, + }, + }, + format="json", + ) + + # Then + assert response.status_code == 204 + environment_default = FeatureState.objects.get_live_feature_states( + environment=versioned_environment, + feature_id=feature, + feature_segment=None, + ).get() + assert environment_default.enabled is True + assert environment_default.get_feature_state_value() == 1000 + + +@pytest.mark.parametrize( + "update", + [ + pytest.param({"value": {"type": "string", "value": "control"}}, id="enabled"), + pytest.param({"enabled": True}, id="value"), + pytest.param({}, id="enabled-and-value"), + ], +) +def test_update_flag__environment_default_attribute_omitted__left_unchanged( + admin_client: APIClient, + environment_api_key: str, + feature: int, + versioned_environment: Environment, + update: dict[str, object], +) -> None: + # Given + setup_response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "environment_default": { + "enabled": True, + "value": {"type": "string", "value": "control"}, + }, + }, + format="json", + ) + assert setup_response.status_code == 204 + + # When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "environment_default": update, + }, + format="json", + ) + + # Then + assert response.status_code == 204 + environment_default = FeatureState.objects.get_live_feature_states( + environment=versioned_environment, + feature_id=feature, + feature_segment=None, + ).get() + assert environment_default.enabled is True + assert environment_default.get_feature_state_value() == "control" + + +def test_update_flag__segment_overrides__creates_overrides( + admin_client: APIClient, + environment_api_key: str, + feature: int, + segment: int, + segment_2: int, + versioned_environment: Environment, +) -> None: + # Given / When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "environment_default": { + "enabled": False, + "value": {"type": "string", "value": "standard"}, + }, + "segment_overrides": [ + { + "segment_id": segment, + "priority": 10, + "enabled": True, + "value": {"type": "string", "value": "enterprise"}, + }, + { + "segment_id": segment_2, + "priority": 20, + "enabled": True, + "value": {"type": "string", "value": "premium"}, + }, + ], + }, + format="json", + ) + + # Then + assert response.status_code == 204 + live_feature_states = FeatureState.objects.get_live_feature_states( + environment=versioned_environment, + feature_id=feature, + ) + environment_default = live_feature_states.get(feature_segment=None) + assert environment_default.enabled is False + assert environment_default.get_feature_state_value() == "standard" + enterprise_override = live_feature_states.get(feature_segment__segment_id=segment) + assert enterprise_override.priority == 10 + assert enterprise_override.enabled is True + assert enterprise_override.get_feature_state_value() == "enterprise" + premium_override = live_feature_states.get(feature_segment__segment_id=segment_2) + assert premium_override.priority == 20 + assert premium_override.enabled is True + assert premium_override.get_feature_state_value() == "premium" + + +def test_update_flag__segment_override_priority_omitted__sets_priority_from_list_position( + admin_client: APIClient, + environment_api_key: str, + feature: int, + segment: int, + segment_2: int, + versioned_environment: Environment, +) -> None: + # Given / When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "segment_overrides": [ + { + "segment_id": segment, + "priority": 10, + "enabled": True, + "value": {"type": "string", "value": "enterprise"}, + }, + { + "segment_id": segment_2, + "enabled": True, + "value": {"type": "string", "value": "premium"}, + }, + ], + }, + format="json", + ) + + # Then + assert response.status_code == 204 + live_feature_states = FeatureState.objects.get_live_feature_states( + environment=versioned_environment, + feature_id=feature, + ) + assert live_feature_states.get(feature_segment__segment_id=segment).priority == 10 + assert live_feature_states.get(feature_segment__segment_id=segment_2).priority == 1 + + +@pytest.mark.parametrize( + "update", + [ + pytest.param( + {"priority": 10, "value": {"type": "string", "value": "enterprise"}}, + id="enabled", + ), + pytest.param({"priority": 10, "enabled": True}, id="value"), + pytest.param( + {"enabled": True, "value": {"type": "string", "value": "enterprise"}}, + id="priority", + ), + pytest.param({}, id="enabled-and-value-and-priority"), + ], +) +def test_update_flag__segment_override_attribute_omitted__left_unchanged( + admin_client: APIClient, + environment_api_key: str, + feature: int, + segment: int, + versioned_environment: Environment, + update: dict[str, object], +) -> None: + # Given + setup_response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "segment_overrides": [ + { + "segment_id": segment, + "priority": 10, + "enabled": True, + "value": {"type": "string", "value": "enterprise"}, + }, + ], + }, + format="json", + ) + assert setup_response.status_code == 204 + + # When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "segment_overrides": [{"segment_id": segment, **update}], + }, + format="json", + ) + + # Then + assert response.status_code == 204 + override = FeatureState.objects.get_live_feature_states( + environment=versioned_environment, + feature_id=feature, + ).get(feature_segment__segment_id=segment) + assert override.priority == 10 + assert override.enabled is True + assert override.get_feature_state_value() == "enterprise" + + +@pytest.mark.parametrize( + "variants, expected_allocations", + [ + pytest.param( + [ + {"key": "variant_a", "weight": 0.25}, + {"key": "variant_b", "weight": 0.25}, + ], + {"variant_a": 25, "variant_b": 25}, + id="fractional", + ), + pytest.param( + [ + {"key": "variant_a", "weight": 0.5}, + {"key": "variant_b", "weight": 0}, + ], + {"variant_a": 50, "variant_b": 0}, + id="zero-weight", + ), + ], +) +@pytest.mark.usefixtures("feature_variants") +def test_update_flag__environment_default_variants__reweights_variants( + admin_client: APIClient, + environment_api_key: str, + feature: int, + versioned_environment: Environment, + variants: list[dict[str, object]], + expected_allocations: dict[str, float], +) -> None: + # Given / When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "environment_default": {"variants": variants}, + }, + format="json", + ) + + # Then + assert response.status_code == 204 + environment_default = FeatureState.objects.get_live_feature_states( + environment=versioned_environment, + feature_id=feature, + feature_segment=None, + ).get() + assert ( + dict( + environment_default.multivariate_feature_state_values.values_list( + "multivariate_feature_option__key", "percentage_allocation" + ) + ) + == expected_allocations + ) + + +@pytest.mark.usefixtures("feature_variants") +def test_update_flag__segment_override_variants__reweights_for_segment_only( + admin_client: APIClient, + environment_api_key: str, + feature: int, + segment: int, + versioned_environment: Environment, +) -> None: + # Given / When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "segment_overrides": [ + { + "segment_id": segment, + "enabled": True, + "variants": [ + {"key": "variant_a", "weight": 0.25}, + {"key": "variant_b", "weight": 0.25}, + ], + }, + ], + }, + format="json", + ) + + # Then + assert response.status_code == 204 + live_feature_states = FeatureState.objects.get_live_feature_states( + environment=versioned_environment, + feature_id=feature, + ) + override = live_feature_states.get(feature_segment__segment_id=segment) + assert dict( + override.multivariate_feature_state_values.values_list( + "multivariate_feature_option__key", "percentage_allocation" + ) + ) == {"variant_a": 25, "variant_b": 25} + environment_default = live_feature_states.get(feature_segment=None) + assert dict( + environment_default.multivariate_feature_state_values.values_list( + "multivariate_feature_option__key", "percentage_allocation" + ) + ) == {"variant_a": 10, "variant_b": 20} + + +def test_update_flag__segment_override_delete__removes_override( + admin_client: APIClient, + environment_api_key: str, + feature: int, + segment: int, + versioned_environment: Environment, +) -> None: + # Given + setup_response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "segment_overrides": [ + { + "segment_id": segment, + "enabled": True, + "value": {"type": "string", "value": "override"}, + }, + ], + }, + format="json", + ) + assert setup_response.status_code == 204 + + # When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "segment_overrides": [ + {"segment_id": segment, "delete": True}, + ], + }, + format="json", + ) + + # Then + assert response.status_code == 204 + assert ( + not FeatureState.objects.get_live_feature_states( + environment=versioned_environment, + feature_id=feature, + ) + .filter(feature_segment__segment_id=segment) + .exists() + ) + + +def test_update_flag__segment_override_delete_with_other_attributes__responds_400( + admin_client: APIClient, + environment_api_key: str, + feature: int, + segment: int, +) -> None: + # Given / When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "segment_overrides": [ + {"segment_id": segment, "delete": True, "enabled": True}, + ], + }, + format="json", + ) + + # Then + assert response.status_code == 400 + assert "delete" in str(response.json()) + + +@pytest.mark.parametrize("context", ["environment_default", "segment_overrides"]) +@pytest.mark.parametrize( + "variants", + [ + pytest.param( + [ + {"key": "variant_a", "weight": 0.6}, + {"key": "variant_b", "weight": 0.5}, + ], + id="weights-exceed-one", + ), + pytest.param( + [{"key": "variant_a", "weight": 0.5}], + id="variant-omitted", + ), + pytest.param( + [ + {"key": "variant_a", "weight": 0.1}, + {"key": "variant_b", "weight": 0.2}, + {"key": "unknown_variant", "weight": 0.5}, + ], + id="unknown-key", + ), + ], +) +@pytest.mark.usefixtures("feature_variants") +def test_update_flag__invalid_variants__responds_400( + admin_client: APIClient, + environment_api_key: str, + feature: int, + segment: int, + versioned_environment: Environment, + context: str, + variants: list[dict[str, object]], +) -> None: + # Given + update = { + "environment_default": {"variants": variants}, + "segment_overrides": [{"segment_id": segment, "variants": variants}], + }[context] + + # When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + context: update, + }, + format="json", + ) + + # Then + assert response.status_code == 400 + assert "variants" in str(response.json()) + environment_default = FeatureState.objects.get_live_feature_states( + environment=versioned_environment, + feature_id=feature, + feature_segment=None, + ).get() + assert dict( + environment_default.multivariate_feature_state_values.values_list( + "multivariate_feature_option__key", "percentage_allocation" + ) + ) == {"variant_a": 10, "variant_b": 20} + + +def test_update_flag__unknown_feature__responds_400( + admin_client: APIClient, + environment_api_key: str, + versioned_environment: Environment, +) -> None: + # Given / When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"name": "unknown_feature"}, + "environment_default": {"enabled": True}, + }, + format="json", + ) + + # Then + assert response.status_code == 400 + assert "feature" in str(response.json()).lower() + + +def test_update_flag__unknown_segment__responds_400( + admin_client: APIClient, + environment_api_key: str, + feature: int, +) -> None: + # Given / When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "segment_overrides": [ + {"segment_id": 999999, "enabled": True}, + ], + }, + format="json", + ) + + # Then + assert response.status_code == 400 + assert "segment" in str(response.json()).lower() + + +def test_update_flag__change_requests_enabled__responds_400( + admin_client: APIClient, + environment_api_key: str, + feature: int, + versioned_environment: Environment, +) -> None: + # Given + versioned_environment.minimum_change_request_approvals = 0 + versioned_environment.save() + + # When + response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "environment_default": {"enabled": True}, + }, + format="json", + ) + + # Then + assert response.status_code == 400 + environment_default = FeatureState.objects.get_live_feature_states( + environment=versioned_environment, + feature_id=feature, + feature_segment=None, + ).get() + assert environment_default.enabled is False diff --git a/api/tests/integration/helpers.py b/api/tests/integration/helpers.py index cd98edd267bc..0145d252c513 100644 --- a/api/tests/integration/helpers.py +++ b/api/tests/integration/helpers.py @@ -45,7 +45,7 @@ def create_feature_with_api( def create_mv_option_with_api( client: APIClient, project_id: int, - feature_id: str, + feature_id: int, default_percentage_allocation: float, value: str, key: str | None = None, diff --git a/docs/docs/integrating-with-flagsmith/flagsmith-api-overview/admin-api/updating-flags.md b/docs/docs/integrating-with-flagsmith/flagsmith-api-overview/admin-api/updating-flags.md deleted file mode 100644 index 6a8ca75fbfb1..000000000000 --- a/docs/docs/integrating-with-flagsmith/flagsmith-api-overview/admin-api/updating-flags.md +++ /dev/null @@ -1,247 +0,0 @@ ---- -title: Updating Flags (Experimental) -sidebar_label: Updating Flags (Experimental) -sidebar_position: 3 ---- - -These experimental endpoints let you update feature flag values and segment overrides via the Admin API. They're -purpose-built for automation and CI/CD — minimal payloads, no need to look up internal IDs, and they work the same -regardless of whether your environment has Feature Versioning enabled. - -:::caution - -These endpoints are experimental and may change without notice. They do not support multivariate values and cannot be -used when [change requests](/administration-and-security/governance-and-compliance/change-requests) are enabled. - -::: - -We're evaluating two approaches for updating flags — **Option A** (one change per request) and **Option B** (everything -in one request). Each scenario below shows both. Try them and -[let us know which works better for you](https://github.com/Flagsmith/flagsmith/issues/6233). - -**Common details:** - -- Identify features by `name` or `id` (pick one, not both). -- All endpoints return **204 No Content** on success. -- Values are passed as a `value` object with `type` and `value` (always a string): - -| Type | Example | -| --------- | ------------------------------------------------ | -| `string` | `{"type": "string", "value": "hello"}` | -| `integer` | `{"type": "integer", "value": "42"}` | -| `boolean` | `{"type": "boolean", "value": "true"}` | - ---- - -## Toggle a flag on or off - -The simplest case — flip a feature flag in an environment. - -**Option A** — [`POST /api/experiments/environments/{environment_key}/update-flag-v1/`](https://api.flagsmith.com/api/v1/docs/#/experimental/api_experiments_environments_update_flag_v1_create) - -```bash -curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag-v1/' \ - -H 'Authorization: Api-Key ' \ - -H 'Content-Type: application/json' \ - -d '{ - "feature": {"name": "maintenance_mode"}, - "enabled": true, - "value": {"type": "boolean", "value": "true"} - }' -``` - -**Option B** — [`POST /api/experiments/environments/{environment_key}/update-flag-v2/`](https://api.flagsmith.com/api/v1/docs/#/experimental/api_experiments_environments_update_flag_v2_create) - -```bash -curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag-v2/' \ - -H 'Authorization: Api-Key ' \ - -H 'Content-Type: application/json' \ - -d '{ - "feature": {"name": "maintenance_mode"}, - "environment_default": { - "enabled": true, - "value": {"type": "boolean", "value": "true"} - } - }' -``` - ---- - -## Update a feature value - -Change a feature's value — for example, setting a rate limit. - -**Option A** - -```bash -curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag-v1/' \ - -H 'Authorization: Api-Key ' \ - -H 'Content-Type: application/json' \ - -d '{ - "feature": {"name": "api_rate_limit"}, - "enabled": true, - "value": {"type": "integer", "value": "1000"} - }' -``` - -**Option B** - -```bash -curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag-v2/' \ - -H 'Authorization: Api-Key ' \ - -H 'Content-Type: application/json' \ - -d '{ - "feature": {"name": "api_rate_limit"}, - "environment_default": { - "enabled": true, - "value": {"type": "integer", "value": "1000"} - } - }' -``` - ---- - -## Roll out a feature to a segment - -Enable a feature for a specific segment (e.g. beta users) while keeping it off for everyone else. - -**Option A** - -```bash -curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag-v1/' \ - -H 'Authorization: Api-Key ' \ - -H 'Content-Type: application/json' \ - -d '{ - "feature": {"name": "new_checkout"}, - "segment": {"id": 456}, - "enabled": true, - "value": {"type": "boolean", "value": "true"} - }' -``` - -**Option B** — single request: - -```bash -curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag-v2/' \ - -H 'Authorization: Api-Key ' \ - -H 'Content-Type: application/json' \ - -d '{ - "feature": {"name": "new_checkout"}, - "environment_default": { - "enabled": false, - "value": {"type": "boolean", "value": "false"} - }, - "segment_overrides": [ - { - "segment_id": 456, - "enabled": true, - "value": {"type": "boolean", "value": "true"} - } - ] - }' -``` - -The `priority` field on segment overrides is optional. Omit it to add at the lowest priority. Priority `1` is highest. - ---- - -## Configure multiple segment overrides - -Set different values per segment — for example, pricing tiers. - -**Option A** — one request per segment override plus one for the default: - -```bash -# Default -curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag-v1/' \ - -H 'Authorization: Api-Key ' \ - -H 'Content-Type: application/json' \ - -d '{ - "feature": {"name": "pricing_tier"}, - "enabled": true, - "value": {"type": "string", "value": "standard"} - }' - -# Enterprise segment (highest priority) -curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag-v1/' \ - -H 'Authorization: Api-Key ' \ - -H 'Content-Type: application/json' \ - -d '{ - "feature": {"name": "pricing_tier"}, - "segment": {"id": 101, "priority": 1}, - "enabled": true, - "value": {"type": "string", "value": "enterprise"} - }' - -# Premium segment -curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag-v1/' \ - -H 'Authorization: Api-Key ' \ - -H 'Content-Type: application/json' \ - -d '{ - "feature": {"name": "pricing_tier"}, - "segment": {"id": 202, "priority": 2}, - "enabled": true, - "value": {"type": "string", "value": "premium"} - }' -``` - -**Option B** — single request: - -```bash -curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag-v2/' \ - -H 'Authorization: Api-Key ' \ - -H 'Content-Type: application/json' \ - -d '{ - "feature": {"name": "pricing_tier"}, - "environment_default": { - "enabled": true, - "value": {"type": "string", "value": "standard"} - }, - "segment_overrides": [ - { - "segment_id": 101, - "priority": 1, - "enabled": true, - "value": {"type": "string", "value": "enterprise"} - }, - { - "segment_id": 202, - "priority": 2, - "enabled": true, - "value": {"type": "string", "value": "premium"} - } - ] - }' -``` - ---- - -## Remove a segment override - -A separate endpoint for removing a segment override from a feature: - -[`POST /api/experiments/environments/{environment_key}/delete-segment-override/`](https://api.flagsmith.com/api/v1/docs/#/experimental/api_experiments_environments_delete_segment_override_create) - -```bash -curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/delete-segment-override/' \ - -H 'Authorization: Api-Key ' \ - -H 'Content-Type: application/json' \ - -d '{ - "feature": {"name": "pricing_tier"}, - "segment": {"id": 202} - }' -``` - ---- - -## Quick reference - -| Aspect | Details | -| -------------------- | ---------------------------------------------------------------------------- | -| Feature ID | `name` or `id` — use one, not both | -| Value types | `string`, `integer`, `boolean` | -| Segment priority | Optional — omit to add at lowest priority; `1` is highest | -| Feature Versioning | Works the same whether enabled or not | -| Success response | `204 No Content` | -| Limitations | No multivariate support; incompatible with change requests | -| Full API schema | [Swagger Explorer](https://api.flagsmith.com/api/v1/docs/) | diff --git a/docs/docs/managing-flags/feature-versioning.md b/docs/docs/managing-flags/feature-versioning.md index 1f842e3d36d5..2946a51447c4 100644 --- a/docs/docs/managing-flags/feature-versioning.md +++ b/docs/docs/managing-flags/feature-versioning.md @@ -27,7 +27,7 @@ Enabling Feature Versioning v2 on an environment is irreversible. To produce a new published version on a v2 environment, use one of: -- **The experimental [update-flag endpoints](/integrating-with-flagsmith/flagsmith-api-overview/admin-api/updating-flags)** (`update-flag-v1`, `update-flag-v2`, `delete-segment-override`). These accept the same payloads as on v1 environments and publish a new version per call on v2 environments. +- **The experimental [update-flag endpoints](/managing-flags/updating-flags)** (`update-flag-v1`, `update-flag-v2`, `delete-segment-override`). These accept the same payloads as on v1 environments and publish a new version per call on v2 environments. - **The new versioning endpoint family**: - `GET /environments/{env}/features/{feature}/versions/` — list versions for a feature. - `POST /environments/{env}/features/{feature}/versions/` — create a draft version. diff --git a/docs/docs/managing-flags/updating-flags.md b/docs/docs/managing-flags/updating-flags.md new file mode 100644 index 000000000000..cc83d8a8dc1f --- /dev/null +++ b/docs/docs/managing-flags/updating-flags.md @@ -0,0 +1,177 @@ +--- +title: 'Experimental: Updating Flags' +sidebar_label: 'Experimental: Updating Flags' +--- + +We're experimenting with a set of new endpoints for updating feature flags. They should provide better ergonomics for +the most common use cases, while keeping operations agnostic to +[Feature Versioning](/managing-flags/feature-versioning). We plan to dogfood them in our own dashboard and CLI, and +eventually make them canonical. + +:::caution + +**These endpoints are experimental and may change without notice.** Note these limitations: + +- They cannot be used when [change requests](/administration-and-security/governance-and-compliance/change-requests) are + enabled. +- They do not support identity overrides. + +These may be lifted in the future. + +::: + +Learn more in the [API specification](link TODO). + +## Updating a flag + +We support both `PATCH` and `PUT` methods for updating a flag. Both accept optional `environment_default` and +`segment_overrides` properties. Attributes omitted from a `PATCH` payload are left unchanged, while `PUT` replaces +each property it receives in full — use it with caution. + +Values are passed as a `value` object with a `type` and a `value` string: + +| Type | Example | +| --------- | -------------------------------------- | +| `string` | `{"type": "string", "value": "hello"}` | +| `integer` | `{"type": "integer", "value": "42"}` | +| `boolean` | `{"type": "boolean", "value": "true"}` | + +### Toggle a flag on or off + +The simplest case — flip a feature flag in an environment: + +```bash +curl -X PATCH 'https://api.flagsmith.com/api/__future__/environments/{environment_key}/features/{feature_id}/' \ + -H 'Authorization: Api-Key {api_key}' \ + -H 'Content-Type: application/json' \ + -d '{ + "environment_default": {"enabled": true} + }' +``` + +### Update a feature value + +Change a feature's default value in an environment: + +```bash +curl -X PATCH 'https://api.flagsmith.com/api/__future__/environments/{environment_key}/features/{feature_id}/' \ + -H 'Authorization: Api-Key {api_key}' \ + -H 'Content-Type: application/json' \ + -d '{ + "environment_default": { + "value": {"type": "integer", "value": "1000"} + } + }' +``` + +### Roll out a feature to a segment + +Enable a flag for one or more segments, while keeping it off for everyone else: + +```bash +curl -X PATCH 'https://api.flagsmith.com/api/__future__/environments/{environment_key}/features/{feature_id}/' \ + -H 'Authorization: Api-Key {api_key}' \ + -H 'Content-Type: application/json' \ + -d '{ + "environment_default": { + "enabled": false + }, + "segment_overrides": [ + { + "segment": {"id": 101}, + "enabled": true, + "priority": 10 + }, + { + "segment": {"id": 202}, + "enabled": true, + "priority": 20 + } + ] + }' +``` + +Segments can also override the feature's value for the environment: + +```bash +curl -X PATCH 'https://api.flagsmith.com/api/__future__/environments/{environment_key}/features/{feature_id}/' \ + -H 'Authorization: Api-Key {api_key}' \ + -H 'Content-Type: application/json' \ + -d '{ + "segment_overrides": [ + { + "segment": {"id": 101}, + "value": {"type": "string", "value": "enterprise"} + } + ] + }' +``` + +Overrides listed in a `PATCH` payload are added or updated by segment; overrides not listed are left unchanged. When +adding a new segment override, if `priority` is omitted, it defaults to the override's position in the +`segment_overrides` list. The lowest number has the highest priority. + +### Remove a segment override + +To remove a segment override, `PUT` the full list of overrides without it. `PUT` replaces the whole set, deleting any +override not listed: + +```bash +curl -X PUT 'https://api.flagsmith.com/api/__future__/environments/{environment_key}/features/{feature_id}/' \ + -H 'Authorization: Api-Key {api_key}' \ + -H 'Content-Type: application/json' \ + -d '{ + "segment_overrides": [ + { + "segment": {"id": 101}, + "priority": 10, + "value": {"type": "string", "value": "enterprise"} + } + ] + }' +``` + +### Re-weight variants (A/B/n) + +On previously configured multivariate features (e.g. experiments), the weight of each variant can be adjusted in the +environment and per segment with the `variants` property. + +A `weight` is a percentage between 0 and 100. Any weight not allocated to variants serves the flag's default `value`. + +Re-weight the variants for a feature in the environment: + +```bash +curl -X PATCH 'https://api.flagsmith.com/api/__future__/environments/{environment_key}/features/{feature_id}/' \ + -H 'Authorization: Api-Key {api_key}' \ + -H 'Content-Type: application/json' \ + -d '{ + "environment_default": { + "variants": [ + {"key": "variant_a", "weight": 10}, + {"key": "variant_b", "weight": 10.5} + ] + } + }' +``` + +Within the same request as above, or separately, you can also set different weights for a segment: + +```bash +curl -X PATCH 'https://api.flagsmith.com/api/__future__/environments/{environment_key}/features/{feature_id}/' \ + -H 'Authorization: Api-Key {api_key}' \ + -H 'Content-Type: application/json' \ + -d '{ + "segment_overrides": [ + { + "segment": {"id": 101}, + "variants": [ + {"key": "variant_a", "weight": 25}, + {"key": "variant_b", "weight": 25} + ] + } + ] + }' +``` + +In both `environment_default` and `segment_overrides`, the `variants` list **must** include all variants for the +feature, even if their weight is zero.