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
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Generated by Django 4.2.26 on 2025-11-27 10:39
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor

from django.db import migrations, models


def migrate_sample_to_webhook_forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None:
FeatureHealthProvider = apps.get_model("feature_health", "FeatureHealthProvider")
FeatureHealthEvent = apps.get_model("feature_health", "FeatureHealthEvent")

FeatureHealthProvider.objects.filter(name="Sample").update(name="Webhook")
FeatureHealthEvent.objects.filter(provider_name="Sample").update(provider_name="Webhook")


def migrate_sample_to_webhook_reverse(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None:
FeatureHealthProvider = apps.get_model("feature_health", "FeatureHealthProvider")
FeatureHealthEvent = apps.get_model("feature_health", "FeatureHealthEvent")

FeatureHealthProvider.objects.filter(name="Webhook").update(name="Sample")
FeatureHealthEvent.objects.filter(provider_name="Webhook").update(provider_name="Sample")



class Migration(migrations.Migration):

dependencies = [
("feature_health", "0002_featurehealthevent_add_external_id_alter_created_at"),
]

operations = [
migrations.AlterField(
model_name='featurehealthprovider',
name='name',
field=models.CharField(
choices=[('Webhook', 'Webhook'), ('Grafana', 'Grafana')],
max_length=50
),
),
migrations.AlterField(
model_name='historicalfeaturehealthprovider',
name='name',
field=models.CharField(
choices=[('Webhook', 'Webhook'), ('Grafana', 'Grafana')],
max_length=50
),
),
migrations.RunPython(
migrate_sample_to_webhook_forward,
migrate_sample_to_webhook_reverse,
),
]
2 changes: 1 addition & 1 deletion api/features/feature_health/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@


class FeatureHealthProviderName(models.Choices):
SAMPLE = "Sample"
WEBHOOK = "Webhook"
GRAFANA = "Grafana"


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@
FeatureHealthEventType,
FeatureHealthProviderName,
)
from features.feature_health.providers.sample.types import (
SampleEvent,
SampleEventStatus,
from features.feature_health.providers.webhook.types import (
WebhookEvent,
WebhookEventStatus,
)
from features.feature_health.types import (
FeatureHealthEventData,
FeatureHealthProviderResponse,
)

_sample_event_type_adapter = TypeAdapter(SampleEvent)
_sample_event_type_adapter = TypeAdapter(WebhookEvent)


def map_sample_event_status_to_feature_health_event_type(
status: SampleEventStatus,
status: WebhookEventStatus,
) -> FeatureHealthEventType:
return (
FeatureHealthEventType.UNHEALTHY
Expand All @@ -29,7 +29,7 @@ def map_sample_event_status_to_feature_health_event_type(
def map_payload_to_provider_response(
payload: str,
) -> FeatureHealthProviderResponse:
event_data: SampleEvent = _sample_event_type_adapter.validate_json(payload)
event_data: WebhookEvent = _sample_event_type_adapter.validate_json(payload)

return FeatureHealthProviderResponse(
events=[
Expand All @@ -40,7 +40,7 @@ def map_payload_to_provider_response(
event_data["status"]
),
reason=event_data.get("reason"),
provider_name=FeatureHealthProviderName.SAMPLE.value,
provider_name=FeatureHealthProviderName.WEBHOOK.value,
),
],
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from features.feature_health.providers.sample.services import (
from features.feature_health.providers.webhook.services import (
get_provider_response,
)

Expand Down
46 changes: 46 additions & 0 deletions api/features/feature_health/providers/webhook/mappers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from pydantic.type_adapter import TypeAdapter

from features.feature_health.models import (
FeatureHealthEventType,
FeatureHealthProviderName,
)
from features.feature_health.providers.webhook.types import (
WebhookEvent,
WebhookEventStatus,
)
from features.feature_health.types import (
FeatureHealthEventData,
FeatureHealthProviderResponse,
)

_sample_event_type_adapter = TypeAdapter(WebhookEvent)


def map_sample_event_status_to_feature_health_event_type(
status: WebhookEventStatus,
) -> FeatureHealthEventType:
return (
FeatureHealthEventType.UNHEALTHY
if status == "unhealthy"
else FeatureHealthEventType.HEALTHY
)


def map_payload_to_provider_response(
payload: str,
) -> FeatureHealthProviderResponse:
event_data: WebhookEvent = _sample_event_type_adapter.validate_json(payload)

return FeatureHealthProviderResponse(
events=[
FeatureHealthEventData(
feature_name=event_data["feature"],
environment_name=event_data.get("environment"),
type=map_sample_event_status_to_feature_health_event_type(
event_data["status"]
),
reason=event_data.get("reason"),
provider_name=FeatureHealthProviderName.WEBHOOK.value,
),
],
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from features.feature_health.providers.sample.mappers import (
from features.feature_health.providers.webhook.mappers import (
map_payload_to_provider_response,
)
from features.feature_health.types import FeatureHealthProviderResponse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

from features.feature_health.types import FeatureHealthEventReason

SampleEventStatus: typing.TypeAlias = typing.Literal["healthy", "unhealthy"]
WebhookEventStatus: typing.TypeAlias = typing.Literal["healthy", "unhealthy"]


class SampleEvent(TypedDict):
class WebhookEvent(TypedDict):
environment: typing.NotRequired[str]
feature: str
status: SampleEventStatus
status: WebhookEventStatus
reason: typing.NotRequired[FeatureHealthEventReason]
4 changes: 2 additions & 2 deletions api/features/feature_health/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
FeatureHealthProvider,
FeatureHealthProviderName,
)
from features.feature_health.providers import grafana, sample
from features.feature_health.providers import grafana, webhook
from features.feature_health.types import FeatureHealthEventReason
from features.models import Feature
from projects.tags.models import Tag, TagType
Expand All @@ -36,7 +36,7 @@
typing.Callable[[str], "FeatureHealthProviderResponse"],
] = {
FeatureHealthProviderName.GRAFANA.value: grafana.get_provider_response,
FeatureHealthProviderName.SAMPLE.value: sample.get_provider_response,
FeatureHealthProviderName.WEBHOOK.value: webhook.get_provider_response,
}


Expand Down
8 changes: 4 additions & 4 deletions api/tests/integration/features/feature_health/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,25 @@ def _get_feature_health_provider_webhook_url(


@pytest.fixture
def sample_feature_health_provider_webhook_url(
def webhook_feature_health_provider_webhook_url(
project: int, admin_client_new: APIClient
) -> str:
return _get_feature_health_provider_webhook_url(
project=project,
api_client=admin_client_new,
name="Sample",
name="Webhook",
)


@pytest.fixture
def unhealthy_feature(
sample_feature_health_provider_webhook_url: str,
webhook_feature_health_provider_webhook_url: str,
feature_name: str,
feature: int,
api_client: APIClient,
) -> int:
api_client.post(
sample_feature_health_provider_webhook_url,
webhook_feature_health_provider_webhook_url,
data=json.dumps({"feature": feature_name, "status": "unhealthy"}),
content_type="application/json",
)
Expand Down
34 changes: 17 additions & 17 deletions api/tests/integration/features/feature_health/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def test_feature_health_providers__get__expected_response(
url = reverse("api-v1:projects:feature-health-providers-list", args=[project])
expected_feature_health_provider_data = admin_client_new.post(
url,
data={"name": "Sample"},
data={"name": "Webhook"},
).json()

# When
Expand All @@ -49,7 +49,7 @@ def test_feature_health_providers__get__expected_response(
# Then
assert expected_feature_health_provider_data == {
"created_by": expected_created_by,
"name": "Sample",
"name": "Webhook",
"project": project,
"webhook_url": mocker.ANY,
}
Expand All @@ -68,15 +68,15 @@ def test_feature_health_providers__delete__expected_response(
url = reverse("api-v1:projects:feature-health-providers-list", args=[project])
admin_client_new.post(
url,
data={"name": "Sample"},
data={"name": "Webhook"},
).json()

# When

response = admin_client_new.delete(
reverse(
"api-v1:projects:feature-health-providers-detail",
args=[project, "sample"],
args=[project, "Webhook"],
)
)

Expand Down Expand Up @@ -138,7 +138,7 @@ def test_feature_health_events__dismiss__expected_response(
"environment": None,
"feature": unhealthy_feature,
"id": mocker.ANY,
"provider_name": "Sample",
"provider_name": "Webhook",
"reason": {
"text_blocks": [
{"text": f"Manually dismissed by {expected_dismissed_by}"}
Expand Down Expand Up @@ -168,7 +168,7 @@ def test_webhook__sample_provider__post__expected_feature_health_event_created__
feature: int,
project: int,
feature_name: str,
sample_feature_health_provider_webhook_url: str,
webhook_feature_health_provider_webhook_url: str,
api_client: APIClient,
admin_client_new: APIClient,
mocker: MockerFixture,
Expand All @@ -186,7 +186,7 @@ def test_webhook__sample_provider__post__expected_feature_health_event_created__
"status": "unhealthy",
}
response = api_client.post(
sample_feature_health_provider_webhook_url,
webhook_feature_health_provider_webhook_url,
data=json.dumps(webhook_data),
content_type="application/json",
)
Expand All @@ -200,7 +200,7 @@ def test_webhook__sample_provider__post__expected_feature_health_event_created__
"created_at": "2023-01-19T09:09:47.325132Z",
"environment": None,
"feature": feature,
"provider_name": "Sample",
"provider_name": "Webhook",
"reason": None,
"type": "UNHEALTHY",
}
Expand Down Expand Up @@ -229,7 +229,7 @@ def test_webhook__sample_provider__post_with_environment_expected_feature_health
environment: int,
feature_name: str,
environment_name: str,
sample_feature_health_provider_webhook_url: str,
webhook_feature_health_provider_webhook_url: str,
api_client: APIClient,
admin_client_new: APIClient,
mocker: MockerFixture,
Expand All @@ -246,7 +246,7 @@ def test_webhook__sample_provider__post_with_environment_expected_feature_health
"status": "unhealthy",
}
response = api_client.post(
sample_feature_health_provider_webhook_url,
webhook_feature_health_provider_webhook_url,
data=json.dumps(webhook_data),
content_type="application/json",
)
Expand All @@ -260,7 +260,7 @@ def test_webhook__sample_provider__post_with_environment_expected_feature_health
"created_at": "2023-01-19T09:09:47.325132Z",
"environment": environment,
"feature": feature,
"provider_name": "Sample",
"provider_name": "Webhook",
"reason": None,
"type": "UNHEALTHY",
}
Expand All @@ -272,7 +272,7 @@ def test_webhook__unhealthy_feature__post__expected_feature_health_event_created
unhealthy_feature: int,
project: int,
feature_name: str,
sample_feature_health_provider_webhook_url: str,
webhook_feature_health_provider_webhook_url: str,
api_client: APIClient,
admin_client_new: APIClient,
mocker: MockerFixture,
Expand All @@ -291,7 +291,7 @@ def test_webhook__unhealthy_feature__post__expected_feature_health_event_created
}
with freeze_time(datetime.now() + timedelta(seconds=1)):
response = api_client.post(
sample_feature_health_provider_webhook_url,
webhook_feature_health_provider_webhook_url,
data=json.dumps(webhook_data),
content_type="application/json",
)
Expand All @@ -305,7 +305,7 @@ def test_webhook__unhealthy_feature__post__expected_feature_health_event_created
"created_at": "2023-01-19T09:09:48.325132Z",
"environment": None,
"feature": unhealthy_feature,
"provider_name": "Sample",
"provider_name": "Webhook",
"reason": None,
"type": "HEALTHY",
}
Expand All @@ -330,14 +330,14 @@ def test_webhook__unhealthy_feature__post__expected_feature_health_event_created
@pytest.mark.parametrize(
"body", ["invalid", json.dumps({"status": "unhealthy", "feature": "non_existent"})]
)
def test_webhook__sample_provider__post__invalid_payload__expected_response(
sample_feature_health_provider_webhook_url: str,
def test_webhook__webhook_provider__post__invalid_payload__expected_response(
webhook_feature_health_provider_webhook_url: str,
api_client: APIClient,
body: str,
) -> None:
# When
response = api_client.post(
sample_feature_health_provider_webhook_url,
webhook_feature_health_provider_webhook_url,
data=body,
content_type="application/json",
)
Expand Down
2 changes: 1 addition & 1 deletion api/tests/unit/features/feature_health/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ def feature_health_provider(
return FeatureHealthProvider.objects.create( # type: ignore[no-any-return]
created_by=staff_user,
project=project,
name="Sample",
name="Webhook",
)
2 changes: 1 addition & 1 deletion api/tests/unit/features/feature_health/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def test_feature_health_provider_admin__webhook_url__return_expected(
admin_instance = FeatureHealthProviderAdmin(
FeatureHealthProvider, mocker.MagicMock()
)
feature_health_provider = FeatureHealthProvider(name="Sample")
feature_health_provider = FeatureHealthProvider(name="Webhook")
admin_instance.changelist_view(feature_health_provider_admin_request)

# When
Expand Down
Loading
Loading