Просмотр исходного кода

feat(workflow): GroupHistory model (#28691)

This model will store various states of history for groups. It differs from GroupResolution because it stores other status', and is intended to be easier to quickly query than Activity.
Chris Fuller 3 лет назад
Родитель
Сommit
1e0611f1b5

+ 1 - 1
migrations_lockfile.txt

@@ -6,5 +6,5 @@ To resolve this, rebase against latest master and regenerate your migration. Thi
 will then be regenerated, and you should be able to merge without conflicts.
 
 nodestore: 0002_nodestore_no_dictfield
-sentry: 0233_recreate_subscriptions_in_snuba
+sentry: 0234_grouphistory
 social_auth: 0001_initial

+ 1 - 0
src/sentry/deletions/__init__.py

@@ -109,6 +109,7 @@ def load_defaults():
     default_manager.register(models.GroupEmailThread, BulkModelDeletionTask)
     default_manager.register(models.GroupEnvironment, BulkModelDeletionTask)
     default_manager.register(models.GroupHash, BulkModelDeletionTask)
+    default_manager.register(models.GroupHistory, BulkModelDeletionTask)
     default_manager.register(models.GroupLink, BulkModelDeletionTask)
     default_manager.register(models.GroupMeta, BulkModelDeletionTask)
     default_manager.register(models.GroupRedirect, BulkModelDeletionTask)

+ 1 - 0
src/sentry/deletions/defaults/group.py

@@ -13,6 +13,7 @@ DIRECT_GROUP_RELATED_MODELS = (
     models.GroupAssignee,
     models.GroupCommitResolution,
     models.GroupLink,
+    models.GroupHistory,
     models.GroupBookmark,
     models.GroupMeta,
     models.GroupEnvironment,

+ 102 - 0
src/sentry/migrations/0234_grouphistory.py

@@ -0,0 +1,102 @@
+# Generated by Django 2.2.24 on 2021-09-28 23:14
+
+import django.db.models.deletion
+import django.utils.timezone
+from django.db import migrations, models
+
+import sentry.db.models.fields.bounded
+import sentry.db.models.fields.foreignkey
+
+
+class Migration(migrations.Migration):
+    # This flag is used to mark that a migration shouldn't be automatically run in
+    # production. We set this to True for operations that we think are risky and want
+    # someone from ops to run manually and monitor.
+    # General advice is that if in doubt, mark your migration as `is_dangerous`.
+    # Some things you should always mark as dangerous:
+    # - Large data migrations. Typically we want these to be run manually by ops so that
+    #   they can be monitored. Since data migrations will now hold a transaction open
+    #   this is even more important.
+    # - Adding columns to highly active tables, even ones that are NULL.
+    is_dangerous = False
+
+    # This flag is used to decide whether to run this migration in a transaction or not.
+    # By default we prefer to run in a transaction, but for migrations where you want
+    # to `CREATE INDEX CONCURRENTLY` this needs to be set to False. Typically you'll
+    # want to create an index concurrently when adding one to an existing table.
+    # You'll also usually want to set this to `False` if you're writing a data
+    # migration, since we don't want the entire migration to run in one long-running
+    # transaction.
+    atomic = True
+
+    dependencies = [
+        ("sentry", "0233_recreate_subscriptions_in_snuba"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="GroupHistory",
+            fields=[
+                (
+                    "id",
+                    sentry.db.models.fields.bounded.BoundedBigAutoField(
+                        primary_key=True, serialize=False
+                    ),
+                ),
+                ("status", sentry.db.models.fields.bounded.BoundedPositiveIntegerField(default=0)),
+                ("prev_history_date", models.DateTimeField(null=True)),
+                ("date_added", models.DateTimeField(default=django.utils.timezone.now)),
+                (
+                    "actor",
+                    sentry.db.models.fields.foreignkey.FlexibleForeignKey(
+                        null=True, on_delete=django.db.models.deletion.CASCADE, to="sentry.Actor"
+                    ),
+                ),
+                (
+                    "group",
+                    sentry.db.models.fields.foreignkey.FlexibleForeignKey(
+                        db_constraint=False,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="sentry.Group",
+                    ),
+                ),
+                (
+                    "organization",
+                    sentry.db.models.fields.foreignkey.FlexibleForeignKey(
+                        db_constraint=False,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="sentry.Organization",
+                    ),
+                ),
+                (
+                    "prev_history",
+                    sentry.db.models.fields.foreignkey.FlexibleForeignKey(
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="sentry.GroupHistory",
+                    ),
+                ),
+                (
+                    "project",
+                    sentry.db.models.fields.foreignkey.FlexibleForeignKey(
+                        db_constraint=False,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="sentry.Project",
+                    ),
+                ),
+                (
+                    "release",
+                    sentry.db.models.fields.foreignkey.FlexibleForeignKey(
+                        db_constraint=False,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="sentry.Release",
+                    ),
+                ),
+            ],
+            options={
+                "db_table": "sentry_grouphistory",
+                "index_together": {("project", "status", "release")},
+            },
+        ),
+    ]

+ 1 - 0
src/sentry/models/__init__.py

@@ -44,6 +44,7 @@ from .groupcommitresolution import *  # NOQA
 from .groupemailthread import *  # NOQA
 from .groupenvironment import *  # NOQA
 from .grouphash import *  # NOQA
+from .grouphistory import *  # NOQA
 from .groupinbox import *  # NOQA
 from .grouplink import *  # NOQA
 from .groupmeta import *  # NOQA

+ 68 - 0
src/sentry/models/grouphistory.py

@@ -0,0 +1,68 @@
+from django.db import models
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+
+from sentry.db.models import BoundedPositiveIntegerField, FlexibleForeignKey, Model, sane_repr
+
+
+class GroupHistoryStatus:
+    UNRESOLVED = 0
+    RESOLVED = 1
+    AUTO_RESOLVED = 2
+    IGNORED = 3
+    UNIGNORED = 4
+    ASSIGNED = 5
+    UNASSIGNED = 6
+    REGRESSED = 7
+    DELETED = 8
+    DELETED_AND_DISCADED = 9
+    REVIEWED = 10
+
+
+class GroupHistory(Model):
+    """
+    This model is used to track certain status changes for groups,
+    and is designed to power a few types of queries:
+    - `resolved_in:release` syntax - we can query for entries with status=REGRESSION and matching release
+    - Time to Resolution and Age of Unresolved Issues-style queries
+    - Issue Actvity/Status over time breakdown (i.e. for each of the last 14 days, how many new, resolved, regressed, unignored, etc. issues were there?)
+    """
+
+    __include_in_export__ = False
+
+    organization = FlexibleForeignKey("sentry.Organization", db_constraint=False)
+    group = FlexibleForeignKey("sentry.Group", db_constraint=False)
+    project = FlexibleForeignKey("sentry.Project", db_constraint=False)
+    release = FlexibleForeignKey("sentry.Release", null=True, db_constraint=False)
+    actor = FlexibleForeignKey("sentry.Actor", null=True)
+
+    status = BoundedPositiveIntegerField(
+        default=0,
+        choices=(
+            (GroupHistoryStatus.UNRESOLVED, _("Unresolved")),
+            (GroupHistoryStatus.RESOLVED, _("Resolved")),
+            (GroupHistoryStatus.AUTO_RESOLVED, _("Automatically Resolved")),
+            (GroupHistoryStatus.IGNORED, _("Ignored")),
+            (GroupHistoryStatus.UNIGNORED, _("Unignored")),
+            (GroupHistoryStatus.REGRESSED, _("Regressed")),
+            (GroupHistoryStatus.ASSIGNED, _("Assigned")),
+            (GroupHistoryStatus.UNASSIGNED, _("Unassigned")),
+            (GroupHistoryStatus.DELETED, _("Deleted")),
+            (GroupHistoryStatus.DELETED_AND_DISCADED, _("Deleted and Discarded")),
+            (GroupHistoryStatus.REVIEWED, _("Reviewed")),
+        ),
+    )
+    prev_history = FlexibleForeignKey(
+        "sentry.GroupHistory", null=True
+    )  # This field has no immediate use, but might be useful.
+    prev_history_date = models.DateTimeField(
+        null=True
+    )  # This field is used to simplify query calculations.
+    date_added = models.DateTimeField(default=timezone.now)
+
+    class Meta:
+        db_table = "sentry_grouphistory"
+        app_label = "sentry"
+        index_together = (("project", "status", "release"),)
+
+    __repr__ = sane_repr("group_id", "release_id")