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..7db9f501898e --- /dev/null +++ b/api/tests/integration/features/experiments/test_update_flag_endpoint.py @@ -0,0 +1,765 @@ +"""https://docs.flagsmith.com/managing-flags/updating-flags""" + +from collections.abc import Callable +from typing import Any + +import pytest +from rest_framework.test import APIClient + +from environments.models import Environment +from features.models import FeatureState +from features.multivariate.models import MultivariateFeatureOption +from features.versioning.tasks import enable_v2_versioning + + +@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"]) + + +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": 1, + "enabled": True, + "value": {"type": "string", "value": "enterprise"}, + }, + { + "segment_id": segment_2, + "priority": 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, + ) + 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.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.enabled is True + assert premium_override.get_feature_state_value() == "premium" + + +def test_update_flag__environment_default_variants__creates_variants( + 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": "string", "value": "default_gateway"}, + "variants": [ + { + "key": "new_gateway_a", + "weight": 0.1, + "value": {"type": "string", "value": "sharp_payments"}, + }, + { + "key": "new_gateway_b", + "weight": 0.1, + "value": {"type": "string", "value": "e_z_pay"}, + }, + ], + }, + }, + format="json", + ) + + # Then + assert response.status_code == 204 + assert dict( + MultivariateFeatureOption.objects.filter(feature_id=feature).values_list( + "key", "string_value" + ) + ) == {"new_gateway_a": "sharp_payments", "new_gateway_b": "e_z_pay"} + 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" + ) + ) == {"new_gateway_a": 10, "new_gateway_b": 10} + + +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 + 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": "default_gateway"}, + "variants": [ + { + "key": "new_gateway_a", + "weight": 0.1, + "value": {"type": "string", "value": "sharp_payments"}, + }, + { + "key": "new_gateway_b", + "weight": 0.1, + "value": {"type": "string", "value": "e_z_pay"}, + }, + ], + }, + }, + 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, + "enabled": True, + "value": {"type": "string", "value": "enterprise_gateway"}, + "variants": [ + {"key": "new_gateway_a", "weight": 0.25}, + {"key": "new_gateway_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" + ) + ) == {"new_gateway_a": 25, "new_gateway_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" + ) + ) == {"new_gateway_a": 10, "new_gateway_b": 10} + + +def test_update_flag__segment_override_variant_omitted__keeps_weight_for_segment( + 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}, + "environment_default": { + "enabled": True, + "value": {"type": "string", "value": "default"}, + "variants": [ + { + "key": "variant_a", + "weight": 0.1, + "value": {"type": "string", "value": "a"}, + }, + { + "key": "variant_b", + "weight": 0.2, + "value": {"type": "string", "value": "b"}, + }, + ], + }, + }, + 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, + "enabled": True, + "value": {"type": "string", "value": "override"}, + "variants": [ + {"key": "variant_a", "weight": 0.3}, + ], + }, + ], + }, + 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 dict( + override.multivariate_feature_state_values.values_list( + "multivariate_feature_option__key", "percentage_allocation" + ) + ) == {"variant_a": 30, "variant_b": 20} + + +def test_update_flag__environment_default_variant_omitted__deletes_variant( + 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}, + "environment_default": { + "enabled": True, + "value": {"type": "string", "value": "default"}, + "variants": [ + { + "key": "variant_kept", + "weight": 0.1, + "value": {"type": "string", "value": "kept"}, + }, + { + "key": "variant_deleted", + "weight": 0.2, + "value": {"type": "string", "value": "deleted"}, + }, + ], + }, + "segment_overrides": [ + { + "segment_id": segment, + "enabled": True, + "value": {"type": "string", "value": "override"}, + "variants": [ + {"key": "variant_kept", "weight": 0.3}, + {"key": "variant_deleted", "weight": 0.4}, + ], + }, + ], + }, + 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": { + "variants": [ + {"key": "variant_kept", "weight": 0.5}, + ], + }, + }, + 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 dict( + environment_default.multivariate_feature_state_values.values_list( + "multivariate_feature_option__key", "percentage_allocation" + ) + ) == {"variant_kept": 50} + 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_kept": 30} + + +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( + "update", + [ + pytest.param( + lambda segment: { + "environment_default": { + "variants": [ + {"key": "variant_a", "weight": 0.6}, + {"key": "variant_b", "weight": 0.5}, + ], + }, + }, + id="environment-default", + ), + pytest.param( + lambda segment: { + "segment_overrides": [ + { + "segment_id": segment, + "variants": [ + {"key": "variant_a", "weight": 0.6}, + {"key": "variant_b", "weight": 0.5}, + ], + }, + ], + }, + id="segment-override", + ), + ], +) +def test_update_flag__variant_weights_exceed_one__responds_400( + admin_client: APIClient, + environment_api_key: str, + feature: int, + segment: int, + versioned_environment: Environment, + update: Callable[[int], dict[str, Any]], +) -> None: + # Given + setup_response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "environment_default": { + "variants": [ + { + "key": "variant_a", + "weight": 0.1, + "value": {"type": "string", "value": "a"}, + }, + { + "key": "variant_b", + "weight": 0.1, + "value": {"type": "string", "value": "b"}, + }, + ], + }, + }, + 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}, + **update(segment), + }, + 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": 10} + + +@pytest.mark.parametrize( + "variant", + [ + pytest.param( + { + "key": "variant_a", + "weight": 0.5, + "value": {"type": "string", "value": "changed"}, + }, + id="value-provided", + ), + pytest.param( + {"key": "unknown_variant", "weight": 0.5}, + id="unknown-key", + ), + ], +) +def test_update_flag__segment_override_variant_not_reweighting__responds_400( + admin_client: APIClient, + environment_api_key: str, + feature: int, + segment: int, + variant: dict[str, Any], +) -> None: + # Given + setup_response = admin_client.post( + f"/api/experiments/environments/{environment_api_key}/update-flag/", + { + "feature": {"id": feature}, + "environment_default": { + "variants": [ + { + "key": "variant_a", + "weight": 0.1, + "value": {"type": "string", "value": "a"}, + }, + ], + }, + }, + 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, + "variants": [variant], + }, + ], + }, + format="json", + ) + + # Then + assert response.status_code == 400 + assert "variants" in str(response.json()) + + +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/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..885ba41efc8d --- /dev/null +++ b/docs/docs/managing-flags/updating-flags.md @@ -0,0 +1,211 @@ +--- +title: Updating Flags (Experimental) +sidebar_label: Updating Flags (Experimental) +sidebar_position: 6 +--- + +The `/api/experiments/environments/{environment_key}/update-flag/` endpoint lets you update feature flags, segment +overrides, and variant allocations via the Admin API. + +A successful response is always **204 No Content**. The request body is a JSON object with the following fields: + +- `feature` (required) — the feature to update, identified by `name` or `id`. +- `environment_default` (optional) — the default state of the feature in the environment. +- `segment_overrides` (optional) — a list of segment overrides for the feature. + +Any attribute omitted in the payload will be left unchanged. + +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"}` | + +Learn more in the +[API specification](https://api.flagsmith.com/api/v1/docs/#/experimental/api_experiments_environments_update_flag). + +:::caution + +**This endpoint is experimental and may change without notice.** It cannot be used when +[change requests](/administration-and-security/governance-and-compliance/change-requests) are enabled. + +::: + +## Examples + +### Toggle a flag on or off + +The simplest case — flip a feature flag in an environment. + +```bash +curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag/' \ + -H 'Authorization: Api-Key ' \ + -H 'Content-Type: application/json' \ + -d '{ + "feature": {"name": "maintenance_mode"}, + "environment_default": {"enabled": true} + }' +``` + +### Update a feature value + +Change a feature's value — for example, setting a rate limit. + +```bash +curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag/' \ + -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. + +```bash +curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag/' \ + -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 in segment overrides is optional. The lowest number has the highest priority. Omit it to add at the +lowest priority. + +### Configure multiple segment overrides + +Set different values per segment — for example, pricing tiers. + +```bash +curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag/' \ + -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"} + } + ] + }' +``` + +### Configure A/B/n experiments (variants) + +Set up features with weighted variants and customise weights per segment. + +The `variants` list in `environment_default` defines the available variants for the feature, and their default weights +in the environment. Each variant is identified by a `key` (slug format). Omitting a variant from the list deletes it in +the environment, and in any segment overrides. + +A `weight` is a fraction between 0 and 1. Any weight not allocated to variants serves the flag's default `value`. + +The `variants` list in `segment_overrides` can only re-weight existing variants. Variants omitted from it keep their +current weights for that segment. + +```bash +curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag/' \ + -H 'Authorization: Api-Key ' \ + -H 'Content-Type: application/json' \ + -d '{ + "feature": {"name": "new_payment_gateway_experiment"}, + "environment_default": { + "enabled": true, + "value": {"type": "string", "value": "default_gateway"}, + "variants": [ + { + "key": "new_gateway_a", + "weight": 0.1, + "value": {"type": "string", "value": "sharp_payments"} + }, + { + "key": "new_gateway_b", + "weight": 0.1, + "value": {"type": "string", "value": "e_z_pay"} + } + ] + } + }' +``` + +Within the same request as above, or separately, you can also set different weights for a segment: + +```bash +curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag/' \ + -H 'Authorization: Api-Key ' \ + -H 'Content-Type: application/json' \ + -d '{ + "feature": {"name": "new_payment_gateway_experiment"}, + "segment_overrides": [ + { + "segment_id": 101, + "enabled": true, + "value": {"type": "string", "value": "enterprise_gateway"}, + "variants": [ + { + "key": "new_gateway_a", + "weight": 0.25 + }, + { + "key": "new_gateway_b", + "weight": 0.25 + } + ] + } + ] + }' +``` + +### Remove a segment override + +A special `delete` attribute can be used to remove a segment override from a feature. It cannot be combined with other +attributes besides `segment_id`. + +```bash +curl -X POST 'https://api.flagsmith.com/api/experiments/environments/{environment_key}/update-flag/' \ + -H 'Authorization: Api-Key ' \ + -H 'Content-Type: application/json' \ + -d '{ + "feature": {"name": "new_checkout"}, + "segment_overrides": [ + { + "segment_id": 456, + "delete": true + } + ] + }' +```