-
Notifications
You must be signed in to change notification settings - Fork 413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Backend support for feature health #5023
Merged
Merged
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
aa5e08f
feat: Backend support for feature health
khvn26 7d43575
feat: Add `/api/v1/projects/<id>/feature-health/events`
khvn26 9d4ea14
feat: Accommodate for multiple providers when tagging features
khvn26 979651a
typing fix
khvn26 3b9996f
check provider type against value
khvn26 8c9b78b
tag creation fix
khvn26 88c49f9
distinct fix
khvn26 1c471e6
improve naming
khvn26 16a6490
fix migration, admin display
khvn26 a2f9a10
fixes, coverage
khvn26 0f98d01
improved webhook semantics, coverage
khvn26 4b8abee
improve coverage
khvn26 3fee836
fix migrations
khvn26 c2d4923
fix admin webhook test
khvn26 3a96d16
clarify permissions
khvn26 bfe1e3e
complete coverage
khvn26 cde95c5
get latest events per provider per feature per environment
khvn26 1c0d4b7
nullable created_by
khvn26 8c0f0d7
improve structure
khvn26 7212bf1
accommodate for api keys
khvn26 ca8f625
add destroy
khvn26 9f92c17
fix test
khvn26 8515376
clarify exception
khvn26 75d0ef3
add constant
khvn26 f63d084
handle `KeyError`
khvn26 f6cbdfa
whoops
khvn26 03da785
use reverse
khvn26 d7557f4
fixture fix
khvn26 1486737
fix the fixture
khvn26 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import typing | ||
|
||
from django.contrib import admin | ||
from django.http import HttpRequest | ||
|
||
from features.feature_health.models import FeatureHealthProvider | ||
from features.feature_health.services import get_webhook_path_from_provider | ||
|
||
|
||
@admin.register(FeatureHealthProvider) | ||
class FeatureHealthProviderAdmin(admin.ModelAdmin): | ||
list_display = ( | ||
"project", | ||
"name", | ||
"created_by", | ||
"webhook_url", | ||
) | ||
|
||
def changelist_view( | ||
self, | ||
request: HttpRequest, | ||
*args: typing.Any, | ||
**kwargs: typing.Any, | ||
) -> None: | ||
self.request = request | ||
return super().changelist_view(request, *args, **kwargs) | ||
|
||
def webhook_url( | ||
self, | ||
instance: FeatureHealthProvider, | ||
) -> str: | ||
path = get_webhook_path_from_provider(instance) | ||
return self.request.build_absolute_uri(path) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from core.apps import BaseAppConfig | ||
|
||
|
||
class FeatureHealthConfig(BaseAppConfig): | ||
name = "features.feature_health" | ||
default = True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
FEATURE_HEALTH_PROVIDER_CREATED_MESSAGE = "Health provider %s set up for project %s." | ||
FEATURE_HEALTH_PROVIDER_DELETED_MESSAGE = "Health provider %s removed from project %s." | ||
|
||
FEATURE_HEALTH_EVENT_CREATED_MESSAGE = "Health status changed to %s for feature %s." | ||
FEATURE_HEALTH_EVENT_CREATED_FOR_ENVIRONMENT_MESSAGE = ( | ||
"Health status changed to %s for feature %s in environment %s." | ||
) | ||
FEATURE_HEALTH_EVENT_CREATED_PROVIDER_MESSAGE = "\n\nProvided by %s" | ||
FEATURE_HEALTH_EVENT_CREATED_REASON_MESSAGE = "\n\nReason:\n%s" | ||
|
||
UNHEALTHY_TAG_COLOUR = "#FFC0CB" | ||
|
||
FEATURE_HEALTH_WEBHOOK_PATH_PREFIX = "/api/v1/feature-health/" | ||
matthewelwell marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,272 @@ | ||
# Generated by Django 4.2.18 on 2025-01-27 14:15 | ||
|
||
from django.conf import settings | ||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
import django_lifecycle.mixins | ||
import simple_history.models | ||
import uuid | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
("features", "0065_make_feature_value_size_configurable"), | ||
("api_keys", "0003_masterapikey_is_admin"), | ||
("environments", "0037_add_uuid_field"), | ||
("projects", "0026_add_change_request_approval_limit_to_projects"), | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="HistoricalFeatureHealthProvider", | ||
fields=[ | ||
( | ||
"id", | ||
models.IntegerField( | ||
auto_created=True, blank=True, db_index=True, verbose_name="ID" | ||
), | ||
), | ||
( | ||
"name", | ||
models.CharField( | ||
choices=[("Sample", "Sample"), ("Grafana", "Grafana")], | ||
max_length=50, | ||
), | ||
), | ||
("history_id", models.AutoField(primary_key=True, serialize=False)), | ||
("history_date", models.DateTimeField()), | ||
("history_change_reason", models.CharField(max_length=100, null=True)), | ||
( | ||
"history_type", | ||
models.CharField( | ||
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], | ||
max_length=1, | ||
), | ||
), | ||
( | ||
"created_by", | ||
models.ForeignKey( | ||
blank=True, | ||
db_constraint=False, | ||
null=True, | ||
on_delete=django.db.models.deletion.DO_NOTHING, | ||
related_name="+", | ||
to=settings.AUTH_USER_MODEL, | ||
), | ||
), | ||
( | ||
"history_user", | ||
models.ForeignKey( | ||
null=True, | ||
on_delete=django.db.models.deletion.SET_NULL, | ||
related_name="+", | ||
to=settings.AUTH_USER_MODEL, | ||
), | ||
), | ||
( | ||
"master_api_key", | ||
models.ForeignKey( | ||
blank=True, | ||
null=True, | ||
on_delete=django.db.models.deletion.DO_NOTHING, | ||
to="api_keys.masterapikey", | ||
), | ||
), | ||
( | ||
"project", | ||
models.ForeignKey( | ||
blank=True, | ||
db_constraint=False, | ||
null=True, | ||
on_delete=django.db.models.deletion.DO_NOTHING, | ||
related_name="+", | ||
to="projects.project", | ||
), | ||
), | ||
], | ||
options={ | ||
"verbose_name": "historical feature health provider", | ||
"ordering": ("-history_date", "-history_id"), | ||
"get_latest_by": "history_date", | ||
}, | ||
bases=(simple_history.models.HistoricalChanges, models.Model), | ||
), | ||
migrations.CreateModel( | ||
name="HistoricalFeatureHealthEvent", | ||
fields=[ | ||
( | ||
"id", | ||
models.IntegerField( | ||
auto_created=True, blank=True, db_index=True, verbose_name="ID" | ||
), | ||
), | ||
("created_at", models.DateTimeField(blank=True, editable=False)), | ||
( | ||
"type", | ||
models.CharField( | ||
choices=[("UNHEALTHY", "Unhealthy"), ("HEALTHY", "Healthy")], | ||
max_length=50, | ||
), | ||
), | ||
( | ||
"provider_name", | ||
models.CharField(blank=True, max_length=255, null=True), | ||
), | ||
("reason", models.TextField(blank=True, null=True)), | ||
("history_id", models.AutoField(primary_key=True, serialize=False)), | ||
("history_date", models.DateTimeField()), | ||
("history_change_reason", models.CharField(max_length=100, null=True)), | ||
( | ||
"history_type", | ||
models.CharField( | ||
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], | ||
max_length=1, | ||
), | ||
), | ||
( | ||
"environment", | ||
models.ForeignKey( | ||
blank=True, | ||
db_constraint=False, | ||
null=True, | ||
on_delete=django.db.models.deletion.DO_NOTHING, | ||
related_name="+", | ||
to="environments.environment", | ||
), | ||
), | ||
( | ||
"feature", | ||
models.ForeignKey( | ||
blank=True, | ||
db_constraint=False, | ||
null=True, | ||
on_delete=django.db.models.deletion.DO_NOTHING, | ||
related_name="+", | ||
to="features.feature", | ||
), | ||
), | ||
( | ||
"history_user", | ||
models.ForeignKey( | ||
null=True, | ||
on_delete=django.db.models.deletion.SET_NULL, | ||
related_name="+", | ||
to=settings.AUTH_USER_MODEL, | ||
), | ||
), | ||
( | ||
"master_api_key", | ||
models.ForeignKey( | ||
blank=True, | ||
null=True, | ||
on_delete=django.db.models.deletion.DO_NOTHING, | ||
to="api_keys.masterapikey", | ||
), | ||
), | ||
], | ||
options={ | ||
"verbose_name": "historical feature health event", | ||
"ordering": ("-history_date", "-history_id"), | ||
"get_latest_by": "history_date", | ||
}, | ||
bases=(simple_history.models.HistoricalChanges, models.Model), | ||
), | ||
migrations.CreateModel( | ||
name="FeatureHealthEvent", | ||
fields=[ | ||
( | ||
"id", | ||
models.AutoField( | ||
auto_created=True, | ||
primary_key=True, | ||
serialize=False, | ||
verbose_name="ID", | ||
), | ||
), | ||
( | ||
"uuid", | ||
models.UUIDField(default=uuid.uuid4, editable=False, unique=True), | ||
), | ||
("created_at", models.DateTimeField(auto_now_add=True)), | ||
( | ||
"type", | ||
models.CharField( | ||
choices=[("UNHEALTHY", "Unhealthy"), ("HEALTHY", "Healthy")], | ||
max_length=50, | ||
), | ||
), | ||
( | ||
"provider_name", | ||
models.CharField(blank=True, max_length=255, null=True), | ||
), | ||
("reason", models.TextField(blank=True, null=True)), | ||
( | ||
"environment", | ||
models.ForeignKey( | ||
null=True, | ||
on_delete=django.db.models.deletion.CASCADE, | ||
related_name="feature_health_events", | ||
to="environments.environment", | ||
), | ||
), | ||
( | ||
"feature", | ||
models.ForeignKey( | ||
on_delete=django.db.models.deletion.CASCADE, | ||
related_name="feature_health_events", | ||
to="features.feature", | ||
), | ||
), | ||
], | ||
options={ | ||
"abstract": False, | ||
}, | ||
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model), | ||
), | ||
migrations.CreateModel( | ||
name="FeatureHealthProvider", | ||
fields=[ | ||
( | ||
"id", | ||
models.AutoField( | ||
auto_created=True, | ||
primary_key=True, | ||
serialize=False, | ||
verbose_name="ID", | ||
), | ||
), | ||
( | ||
"uuid", | ||
models.UUIDField(default=uuid.uuid4, editable=False, unique=True), | ||
), | ||
( | ||
"name", | ||
models.CharField( | ||
choices=[("Sample", "Sample"), ("Grafana", "Grafana")], | ||
max_length=50, | ||
), | ||
), | ||
( | ||
"created_by", | ||
models.ForeignKey( | ||
on_delete=django.db.models.deletion.CASCADE, | ||
to=settings.AUTH_USER_MODEL, | ||
), | ||
), | ||
( | ||
"project", | ||
models.ForeignKey( | ||
on_delete=django.db.models.deletion.CASCADE, | ||
to="projects.project", | ||
), | ||
), | ||
], | ||
options={ | ||
"unique_together": {("name", "project")}, | ||
}, | ||
), | ||
] |
Empty file.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the path limited to 100 characters?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to prevent the business logic from parsing webhook hashes that are unreasonably long. Valid hashes are not expected to be longer than 100 characters.