Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ public API, a minor version adds to it, and a patch version fixes it.
- `feature_flags.targeting_context()`, `feature_flags.lint_rule()` and `feature_flags.test_rule()`:
the facts a targeting rule can read, the check the API runs before accepting one, and a dry run
of a rule against a customer or an instance.
- `feature_flags.update(..., slug=...)` renames a flag.

### Fixed

- `feature_flags.update` sends the flag's slug. Without it, Kaiten blanked the slug and the flag
could no longer be reached.

## [1.0.0] - 2026-10-01

Expand Down
8 changes: 8 additions & 0 deletions src/kaitencloud/_async/resources/feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,16 @@ async def update(
description: str | None = None,
metadata: Mapping[str, Any] | None = None,
event_name: str = "",
slug: str | None = None,
) -> None:
"""Replace a feature flag's definition.

A full replacement: the variants, rules and metadata sent become the flag's whole
definition. See :meth:`create` for what each field means.

Args:
slug: A new slug, to rename the flag; applications evaluating it by its old key
stop finding it. Omitted, the flag keeps ``feature_flag_slug``.
"""
body = _flag(
name=name,
Expand All @@ -120,6 +125,9 @@ async def update(
metadata=metadata,
event_name=event_name,
)
# The slug is always sent. Kaiten writes the body's slug as the flag's own, so a
# body without one would blank it rather than keep it.
body["slug"] = slug or feature_flag_slug
await self._api.send_empty("PUT", api_path("feature-flags", feature_flag_slug), body=body)

async def targeting_context(self) -> TargetingContext:
Expand Down
8 changes: 8 additions & 0 deletions src/kaitencloud/_sync/resources/feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,16 @@ def update(
description: str | None = None,
metadata: Mapping[str, Any] | None = None,
event_name: str = "",
slug: str | None = None,
) -> None:
"""Replace a feature flag's definition.

A full replacement: the variants, rules and metadata sent become the flag's whole
definition. See :meth:`create` for what each field means.

Args:
slug: A new slug, to rename the flag; applications evaluating it by its old key
stop finding it. Omitted, the flag keeps ``feature_flag_slug``.
"""
body = _flag(
name=name,
Expand All @@ -122,6 +127,9 @@ def update(
metadata=metadata,
event_name=event_name,
)
# The slug is always sent. Kaiten writes the body's slug as the flag's own, so a
# body without one would blank it rather than keep it.
body["slug"] = slug or feature_flag_slug
self._api.send_empty("PUT", api_path("feature-flags", feature_flag_slug), body=body)

def targeting_context(self) -> TargetingContext:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ class Case:
variants=VARIANTS,
default_variant=targeting.fixed("off"),
),
forbidden=frozenset({"slug"}),
required=frozenset({"slug"}),
),
Case("delete-feature-flag", lambda c: c.feature_flags.delete("new-checkout")),
Case("get-openfeature-manifest", lambda c: c.feature_flags.manifest()),
Expand Down
22 changes: 22 additions & 0 deletions tests/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,31 @@ def test_a_feature_flag_needs_only_the_essentials(make_client: Callable[..., Any
"description": None,
"metadata": {},
"event_name": "",
"slug": "new-checkout",
}


def test_a_feature_flag_update_keeps_its_slug_unless_renamed(
make_client: Callable[..., Any],
) -> None:
# Kaiten writes the body's slug as the flag's own: sending none blanked it.
client, recorder = make_client(respond(204))
essentials = {
"name": "New checkout",
"type": "boolean",
"variants": [{"name": "on", "value": True}],
"default_variant": "on",
}

client.feature_flags.update("new-checkout", **essentials)
client.feature_flags.update("new-checkout", **essentials, slug="checkout-v2")

first, second = recorder
assert body(first)["slug"] == "new-checkout"
assert body(second)["slug"] == "checkout-v2"
assert second.url.path.endswith("/feature-flags/new-checkout")


def test_usage_defaults_to_append(make_client: Callable[..., Any]) -> None:
client, recorder = make_client(respond(200, USAGE))
client.instances.report_usage("acme-production", "seats", 3)
Expand Down
Loading