Browse Source

migration(issue-views): Add new `projects`, `environments`, `time_filter` columns to `GroupSearchView` (#83422)

This PR adds new columns to the GroupSearchView table to represent the
new filters we'd like to add to Issue Views: `projects`,
`is_my_projects`, `environments`, and `time_filters`. This will support
a new feature in Issue Views where users can save projects, environment,
and time filters to their views, rather than it being a global filter.

---------

Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
Michael Sun 1 month ago
parent
commit
801b41c67a

+ 1 - 1
migrations_lockfile.txt

@@ -15,7 +15,7 @@ remote_subscriptions: 0003_drop_remote_subscription
 
 replays: 0004_index_together
 
-sentry: 0820_snuba_query_non_none
+sentry: 0821_create_groupsearchview_page_filter_columns
 
 social_auth: 0002_default_auto_field
 

+ 4 - 0
src/sentry/backup/comparators.py

@@ -806,6 +806,10 @@ def get_default_comparators() -> dict[str, list[JSONScrubbingComparator]]:
                 DateUpdatedComparator("date_added", "date_updated"),
             ],
             "sentry.groupsearchview": [DateUpdatedComparator("date_updated")],
+            "sentry.groupsearchviewproject": [
+                DateUpdatedComparator("date_updated"),
+                DateUpdatedComparator("date_added"),
+            ],
             "sentry.incident": [UUID4Comparator("detection_uuid")],
             "sentry.incidentactivity": [UUID4Comparator("notification_uuid")],
             "sentry.incidenttrigger": [DateUpdatedComparator("date_modified")],

+ 84 - 0
src/sentry/migrations/0821_create_groupsearchview_page_filter_columns.py

@@ -0,0 +1,84 @@
+# Generated by Django 5.1.5 on 2025-01-15 21:39
+
+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
+import sentry.models.groupsearchview
+from sentry.new_migrations.migrations import CheckedMigration
+
+
+class Migration(CheckedMigration):
+    # This flag is used to mark that a migration shouldn't be automatically run in production.
+    # This should only be used for operations where it's safe to run the migration after your
+    # code has deployed. So this should not be used for most operations that alter the schema
+    # of a table.
+    # Here are some things that make sense to mark as post deployment:
+    # - Large data migrations. Typically we want these to be run manually so that they can be
+    #   monitored and not block the deploy for a long period of time while they run.
+    # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
+    #   run this outside deployments so that we don't block them. Note that while adding an index
+    #   is a schema change, it's completely safe to run the operation after the code has deployed.
+    # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment
+
+    is_post_deployment = False
+
+    dependencies = [
+        ("sentry", "0820_snuba_query_non_none"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="groupsearchview",
+            name="environments",
+            field=sentry.db.models.fields.array.ArrayField(default=list, null=True),
+        ),
+        migrations.AddField(
+            model_name="groupsearchview",
+            name="is_all_projects",
+            field=models.BooleanField(db_default=False),
+        ),
+        migrations.AddField(
+            model_name="groupsearchview",
+            name="time_filters",
+            field=models.JSONField(db_default={"period": "14d"}),
+        ),
+        migrations.CreateModel(
+            name="GroupSearchViewProject",
+            fields=[
+                (
+                    "id",
+                    sentry.db.models.fields.bounded.BoundedBigAutoField(
+                        primary_key=True, serialize=False
+                    ),
+                ),
+                ("date_updated", models.DateTimeField(auto_now=True)),
+                ("date_added", models.DateTimeField(auto_now_add=True)),
+                (
+                    "group_search_view",
+                    sentry.db.models.fields.foreignkey.FlexibleForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE, to="sentry.groupsearchview"
+                    ),
+                ),
+                (
+                    "project",
+                    sentry.db.models.fields.foreignkey.FlexibleForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE, to="sentry.project"
+                    ),
+                ),
+            ],
+            options={
+                "db_table": "sentry_groupsearchviewproject",
+                "unique_together": {("group_search_view", "project")},
+            },
+        ),
+        migrations.AddField(
+            model_name="groupsearchview",
+            name="projects",
+            field=models.ManyToManyField(
+                through="sentry.GroupSearchViewProject", to="sentry.project"
+            ),
+        ),
+    ]

+ 28 - 1
src/sentry/models/groupsearchview.py

@@ -2,12 +2,29 @@ from django.db import models
 from django.db.models import UniqueConstraint
 
 from sentry.backup.scopes import RelocationScope
+from sentry.constants import ENVIRONMENT_NAME_MAX_LENGTH
 from sentry.db.models import region_silo_model
-from sentry.db.models.base import DefaultFieldsModelExisting
+from sentry.db.models.base import DefaultFieldsModel, DefaultFieldsModelExisting
+from sentry.db.models.fields.array import ArrayField
 from sentry.db.models.fields.foreignkey import FlexibleForeignKey
 from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey
 from sentry.models.savedsearch import SortOptions
 
+DEFAULT_TIME_FILTER = {"period": "14d"}
+
+
+@region_silo_model
+class GroupSearchViewProject(DefaultFieldsModel):
+    __relocation_scope__ = RelocationScope.Organization
+
+    group_search_view = FlexibleForeignKey("sentry.GroupSearchView", on_delete=models.CASCADE)
+    project = FlexibleForeignKey("sentry.Project", on_delete=models.CASCADE)
+
+    class Meta:
+        app_label = "sentry"
+        db_table = "sentry_groupsearchviewproject"
+        unique_together = (("group_search_view", "project"),)
+
 
 @region_silo_model
 class GroupSearchView(DefaultFieldsModelExisting):
@@ -26,6 +43,16 @@ class GroupSearchView(DefaultFieldsModelExisting):
     )
     position = models.PositiveSmallIntegerField()
 
+    # Projects = [] maps to "My Projects" (This is so when a project is deleted, it correctly defaults to "My Projects")
+    projects = models.ManyToManyField("sentry.Project", through="sentry.GroupSearchViewProject")
+    # If is_all_projects is True, then override `projects` to be "All Projects"
+    is_all_projects = models.BooleanField(db_default=False)
+    # Environments = [] maps to "All Environments"
+    environments = ArrayField(
+        models.CharField(max_length=ENVIRONMENT_NAME_MAX_LENGTH), default=list
+    )
+    time_filters = models.JSONField(null=False, db_default=DEFAULT_TIME_FILTER)
+
     class Meta:
         app_label = "sentry"
         db_table = "sentry_groupsearchview"

+ 7 - 2
src/sentry/testutils/helpers/backups.py

@@ -73,7 +73,7 @@ from sentry.models.dashboard_widget import (
 from sentry.models.dynamicsampling import CustomDynamicSamplingRule
 from sentry.models.groupassignee import GroupAssignee
 from sentry.models.groupbookmark import GroupBookmark
-from sentry.models.groupsearchview import GroupSearchView
+from sentry.models.groupsearchview import GroupSearchView, GroupSearchViewProject
 from sentry.models.groupseen import GroupSeen
 from sentry.models.groupshare import GroupShare
 from sentry.models.groupsubscription import GroupSubscription
@@ -613,7 +613,7 @@ class ExhaustiveFixtures(Fixtures):
 
         # Group*
         group = self.create_group(project=project)
-        GroupSearchView.objects.create(
+        group_search_view = GroupSearchView.objects.create(
             name=f"View 1 for {slug}",
             user_id=owner_id,
             organization=org,
@@ -621,6 +621,11 @@ class ExhaustiveFixtures(Fixtures):
             query_sort="date",
             position=0,
         )
+        GroupSearchViewProject.objects.create(
+            group_search_view=group_search_view,
+            project=project,
+        )
+
         Activity.objects.create(
             project=project,
             group=group,