From 45269d2342c6f1ce69a20616fd8e3a22653fb432 Mon Sep 17 00:00:00 2001 From: ToryMic Date: Tue, 28 Apr 2026 17:09:02 +0100 Subject: [PATCH] feat: add success and failure counts to WebhookSubscription (#371) --- django-backend/soroscan/ingest/admin.py | 6 ++-- ...hooksubscription_success_failure_counts.py | 28 +++++++++++++++++++ django-backend/soroscan/ingest/models.py | 4 ++- django-backend/soroscan/ingest/serializers.py | 12 +++++++- django-backend/soroscan/ingest/tasks.py | 2 ++ 5 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 django-backend/soroscan/ingest/migrations/0039_webhooksubscription_success_failure_counts.py diff --git a/django-backend/soroscan/ingest/admin.py b/django-backend/soroscan/ingest/admin.py index 7f323bf5..b3d0a84f 100644 --- a/django-backend/soroscan/ingest/admin.py +++ b/django-backend/soroscan/ingest/admin.py @@ -506,11 +506,13 @@ class WebhookSubscriptionAdmin(AdminAuditMixin, admin.ModelAdmin): "is_active_display", "timeout_seconds", "failure_count", + "success_count", + "total_failure_count", "last_delivery_status", ] list_filter = ["is_active", "status", "contract", "created_at", "retry_backoff_strategy"] search_fields = ["target_url", "contract__name", "event_type"] - readonly_fields = ["secret", "created_at", "last_triggered", "failure_count", "status"] + readonly_fields = ["secret", "created_at", "last_triggered", "failure_count", "success_count", "total_failure_count", "status"] fieldsets = ( (None, { "fields": ("contract", "target_url", "event_type", "is_active"), @@ -532,7 +534,7 @@ class WebhookSubscriptionAdmin(AdminAuditMixin, admin.ModelAdmin): "Exponential: base * 2^attempt | Linear: base * attempt | Fixed: base", }), ("Status", { - "fields": ("status", "failure_count", "last_triggered"), + "fields": ("status", "failure_count", "success_count", "total_failure_count", "last_triggered"), "classes": ("collapse",), }), ("Secret", { diff --git a/django-backend/soroscan/ingest/migrations/0039_webhooksubscription_success_failure_counts.py b/django-backend/soroscan/ingest/migrations/0039_webhooksubscription_success_failure_counts.py new file mode 100644 index 00000000..783ec174 --- /dev/null +++ b/django-backend/soroscan/ingest/migrations/0039_webhooksubscription_success_failure_counts.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.10 on 2026-04-28 15:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ingest', '0038_dependencyimpactassessment_organizationbudget_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='webhooksubscription', + name='success_count', + field=models.PositiveIntegerField(default=0, help_text='Total successful delivery attempts'), + ), + migrations.AddField( + model_name='webhooksubscription', + name='total_failure_count', + field=models.PositiveIntegerField(default=0, help_text='Total failed delivery attempts'), + ), + migrations.AlterField( + model_name='webhooksubscription', + name='failure_count', + field=models.PositiveIntegerField(default=0, help_text='Consecutive failures since last success'), + ), + ] diff --git a/django-backend/soroscan/ingest/models.py b/django-backend/soroscan/ingest/models.py index ee7c9125..a1c574eb 100644 --- a/django-backend/soroscan/ingest/models.py +++ b/django-backend/soroscan/ingest/models.py @@ -733,7 +733,9 @@ class WebhookSubscription(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) last_triggered = models.DateTimeField(null=True, blank=True) - failure_count = models.PositiveIntegerField(default=0) + failure_count = models.PositiveIntegerField(default=0, help_text="Consecutive failures since last success") + success_count = models.PositiveIntegerField(default=0, help_text="Total successful delivery attempts") + total_failure_count = models.PositiveIntegerField(default=0, help_text="Total failed delivery attempts") timeout_seconds = models.IntegerField( default=10, validators=[MinValueValidator(1), MaxValueValidator(60)], diff --git a/django-backend/soroscan/ingest/serializers.py b/django-backend/soroscan/ingest/serializers.py index 6c9fa42b..9f2999ad 100644 --- a/django-backend/soroscan/ingest/serializers.py +++ b/django-backend/soroscan/ingest/serializers.py @@ -286,8 +286,18 @@ class Meta: "created_at", "last_triggered", "failure_count", + "success_count", + "total_failure_count", + ] + read_only_fields = [ + "id", + "contract_id", + "created_at", + "last_triggered", + "failure_count", + "success_count", + "total_failure_count", ] - read_only_fields = ["id", "contract_id", "created_at", "last_triggered", "failure_count"] extra_kwargs = { "secret": {"write_only": True}, } diff --git a/django-backend/soroscan/ingest/tasks.py b/django-backend/soroscan/ingest/tasks.py index 4a4d25e3..6dc7b428 100644 --- a/django-backend/soroscan/ingest/tasks.py +++ b/django-backend/soroscan/ingest/tasks.py @@ -958,6 +958,7 @@ def dispatch_webhook(self, subscription_id: int, event_id: int) -> bool: if success: WebhookSubscription.objects.filter(pk=webhook.pk).update( failure_count=0, + success_count=F("success_count") + 1, last_triggered=timezone.now(), ) logger.info( @@ -1365,6 +1366,7 @@ def _on_delivery_failure( """ WebhookSubscription.objects.filter(pk=webhook.pk).update( failure_count=F("failure_count") + 1, + total_failure_count=F("total_failure_count") + 1, ) webhook.refresh_from_db(fields=["failure_count", "status", "is_active", "escalation_policy"])