diff --git a/bugsink/tests.py b/bugsink/tests.py index 5f725c0..de21b07 100644 --- a/bugsink/tests.py +++ b/bugsink/tests.py @@ -5,6 +5,7 @@ from django.test import TestCase as DjangoTestCase from projects.models import Project from issues.models import Issue +from issues.factories import denormalized_issue_fields from events.models import Event from events.factories import create_event @@ -180,6 +181,7 @@ class PCRegistryTestCase(DjangoTestCase): project=project, is_muted=True, unmute_on_volume_based_conditions='[{"period": "day", "nr_of_periods": 1, "volume": 100}]', + **denormalized_issue_fields(), ) create_event(project, issue) diff --git a/ingest/views.py b/ingest/views.py index dd01788..d91675e 100644 --- a/ingest/views.py +++ b/ingest/views.py @@ -106,6 +106,11 @@ class BaseIngestAPIView(APIView): issue, issue_created = Issue.objects.get_or_create( project=ingested_event.project, hash=hash_, + defaults={ + "first_seen": ingested_event.timestamp, + "last_seen": ingested_event.timestamp, + "event_count": 1, + }, ) event, event_created = Event.from_ingested(ingested_event, issue, event_data) @@ -121,11 +126,17 @@ class BaseIngestAPIView(APIView): if ingested_event.project.alert_on_new_issue: send_new_issue_alert.delay(issue.id) - elif issue_is_regression(issue, event.release): # new issues cannot be regressions by definition, hence 'else' - if ingested_event.project.alert_on_regression: - send_regression_alert.delay(issue.id) + else: + # new issues cannot be regressions by definition, hence this is in the 'else' branch + if issue_is_regression(issue, event.release): + if ingested_event.project.alert_on_regression: + send_regression_alert.delay(issue.id) - IssueStateManager.reopen(issue) + IssueStateManager.reopen(issue) + + # update the denormalized fields + issue.last_seen = ingested_event.timestamp + issue.event_count += 1 if issue.id not in get_pc_registry().by_issue: pc_registry.by_issue[issue.id] = PeriodCounter() diff --git a/issues/factories.py b/issues/factories.py index b6bc014..7fa93a2 100644 --- a/issues/factories.py +++ b/issues/factories.py @@ -19,6 +19,7 @@ def get_or_create_issue(project=None, event_data=None): issue, issue_created = Issue.objects.get_or_create( project=project, hash=hash_, + defaults=denormalized_issue_fields(), ) return issue, issue_created @@ -31,3 +32,12 @@ def create_event_data(): "timestamp": timezone.now().isoformat(), "platform": "python", } + + +def denormalized_issue_fields(): + """return a dict of fields that are expected to be denormalized on Issue; for testing purposes""" + return { + "first_seen": timezone.now(), + "last_seen": timezone.now(), + "event_count": 1, + } diff --git a/issues/migrations/0008_issue_event_count_issue_first_seen_issue_last_seen.py b/issues/migrations/0008_issue_event_count_issue_first_seen_issue_last_seen.py new file mode 100644 index 0000000..614c26e --- /dev/null +++ b/issues/migrations/0008_issue_event_count_issue_first_seen_issue_last_seen.py @@ -0,0 +1,30 @@ +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('issues', '0007_remove_issue_events'), + ] + + operations = [ + migrations.AddField( + model_name='issue', + name='event_count', + field=models.IntegerField(default=1), + preserve_default=False, + ), + migrations.AddField( + model_name='issue', + name='first_seen', + field=models.DateTimeField(default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='issue', + name='last_seen', + field=models.DateTimeField(default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/issues/migrations/0009_issue_issues_issu_first_s_9fb0f9_idx_and_more.py b/issues/migrations/0009_issue_issues_issu_first_s_9fb0f9_idx_and_more.py new file mode 100644 index 0000000..7456847 --- /dev/null +++ b/issues/migrations/0009_issue_issues_issu_first_s_9fb0f9_idx_and_more.py @@ -0,0 +1,19 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('issues', '0008_issue_event_count_issue_first_seen_issue_last_seen'), + ] + + operations = [ + migrations.AddIndex( + model_name='issue', + index=models.Index(fields=['first_seen'], name='issues_issu_first_s_9fb0f9_idx'), + ), + migrations.AddIndex( + model_name='issue', + index=models.Index(fields=['last_seen'], name='issues_issu_last_se_400a05_idx'), + ), + ] diff --git a/issues/models.py b/issues/models.py index 10db238..7b1045b 100644 --- a/issues/models.py +++ b/issues/models.py @@ -15,6 +15,11 @@ class Issue(models.Model): "projects.Project", blank=False, null=True, on_delete=models.SET_NULL) # SET_NULL: cleanup 'later' hash = models.CharField(max_length=32, blank=False, null=False) + # denormalized fields: + last_seen = models.DateTimeField(blank=False, null=False) + first_seen = models.DateTimeField(blank=False, null=False) + event_count = models.IntegerField(blank=False, null=False) + # fields related to resolution: # what does this mean for the release-based use cases? it means what you filter on. # it also simply means: it was "marked as resolved" after the last regression (if any) @@ -23,6 +28,7 @@ class Issue(models.Model): fixed_at = models.TextField(blank=False, null=False, default='[]') events_at = models.TextField(blank=False, null=False, default='[]') + # fields related to muting: is_muted = models.BooleanField(default=False) unmute_on_volume_based_conditions = models.TextField(blank=False, null=False, default="[]") # json string @@ -74,6 +80,12 @@ class Issue(models.Model): def occurs_in_last_release(self): return False # TODO actually implement (and then: implement in a performant manner) + class Meta: + indexes = [ + models.Index(fields=["first_seen"]), + models.Index(fields=["last_seen"]), + ] + class IssueStateManager(object): """basically: a namespace; with static methods that combine field-setting in a single place""" diff --git a/issues/templates/issues/issue_list.html b/issues/templates/issues/issue_list.html index fea13c5..7616431 100644 --- a/issues/templates/issues/issue_list.html +++ b/issues/templates/issues/issue_list.html @@ -50,11 +50,11 @@ https://flowbite.com/docs/forms/floating-label/