diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 1da7cc95e74f79..90847f0558c67b 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -31,7 +31,7 @@ replays: 0007_organizationmember_replay_access seer: 0006_add_night_shift_models -sentry: 1066_add_export_format_in_data_export_obj +sentry: 1067_add_dashboard_revision_model social_auth: 0003_social_auth_json_field diff --git a/src/sentry/backup/comparators.py b/src/sentry/backup/comparators.py index 3807557bea2bc5..6bd8362e7c926c 100644 --- a/src/sentry/backup/comparators.py +++ b/src/sentry/backup/comparators.py @@ -859,6 +859,9 @@ def get_default_comparators() -> dict[str, list[JSONScrubbingComparator]]: "sentry.dashboardlastvisited": [ DateUpdatedComparator("last_visited", "date_added", "date_updated"), ], + "sentry.dashboardrevision": [ + DateUpdatedComparator("date_added", "date_updated"), + ], "sentry.dataforwarder": [DateUpdatedComparator("date_updated", "date_added")], "sentry.dataforwarderproject": [DateUpdatedComparator("date_updated", "date_added")], "sentry.groupsearchview": [DateUpdatedComparator("date_updated")], diff --git a/src/sentry/migrations/1067_add_dashboard_revision_model.py b/src/sentry/migrations/1067_add_dashboard_revision_model.py new file mode 100644 index 00000000000000..8b023c264447b1 --- /dev/null +++ b/src/sentry/migrations/1067_add_dashboard_revision_model.py @@ -0,0 +1,70 @@ +# Generated by Django 5.2.12 on 2026-04-14 15:38 + +import django.db.models.deletion +import sentry.db.models.fields.bounded +import sentry.db.models.fields.foreignkey +import sentry.db.models.fields.hybrid_cloud_foreign_key +import sentry.db.models.fields.jsonfield +from django.db import migrations, models + +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", "1066_add_export_format_in_data_export_obj"), + ] + + operations = [ + migrations.CreateModel( + name="DashboardRevision", + fields=[ + ( + "id", + sentry.db.models.fields.bounded.BoundedBigAutoField( + primary_key=True, serialize=False + ), + ), + ( + "created_by_id", + sentry.db.models.fields.hybrid_cloud_foreign_key.HybridCloudForeignKey( + "sentry.User", db_index=True, null=True, on_delete="SET_NULL" + ), + ), + ("date_updated", models.DateTimeField(auto_now=True)), + ("date_added", models.DateTimeField(auto_now_add=True)), + ("title", models.CharField(max_length=255)), + ("source", models.CharField(default="edit", max_length=32)), + ("snapshot", sentry.db.models.fields.jsonfield.JSONField(default=dict)), + ("snapshot_schema_version", models.IntegerField()), + ( + "dashboard", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="sentry.dashboard" + ), + ), + ], + options={ + "db_table": "sentry_dashboardrevision", + "indexes": [ + models.Index( + fields=["dashboard", "-date_added"], name="sentry_dashrev_dash_date_idx" + ) + ], + }, + ), + ] diff --git a/src/sentry/models/dashboard.py b/src/sentry/models/dashboard.py index 691de3696b5c72..f917c81f3af347 100644 --- a/src/sentry/models/dashboard.py +++ b/src/sentry/models/dashboard.py @@ -427,6 +427,30 @@ class Meta: __repr__ = sane_repr("organization", "slug") +@cell_silo_model +class DashboardRevision(DefaultFieldsModel): + __relocation_scope__ = RelocationScope.Organization + + created_by_id = HybridCloudForeignKey( + "sentry.User", db_index=True, null=True, on_delete="SET_NULL" + ) + title = models.CharField(max_length=255) + source = models.CharField(max_length=32, default="edit") + snapshot: models.Field[dict[str, Any], dict[str, Any]] = JSONField(default=dict) + snapshot_schema_version = models.IntegerField() + dashboard = FlexibleForeignKey("sentry.Dashboard", on_delete=models.CASCADE) + + class Meta: + app_label = "sentry" + db_table = "sentry_dashboardrevision" + indexes = [ + models.Index( + fields=["dashboard", "-date_added"], + name="sentry_dashrev_dash_date_idx", + ) + ] + + @cell_silo_model class DashboardLastVisited(DefaultFieldsModel): __relocation_scope__ = RelocationScope.Organization diff --git a/src/sentry/organizations/services/organization/impl.py b/src/sentry/organizations/services/organization/impl.py index 5b7c8db37cf3fd..fc89dbce5c338e 100644 --- a/src/sentry/organizations/services/organization/impl.py +++ b/src/sentry/organizations/services/organization/impl.py @@ -19,7 +19,7 @@ from sentry.incidents.models.alert_rule import AlertRule, AlertRuleActivity from sentry.incidents.models.incident import IncidentActivity from sentry.models.activity import Activity -from sentry.models.dashboard import Dashboard, DashboardFavoriteUser +from sentry.models.dashboard import Dashboard, DashboardFavoriteUser, DashboardRevision from sentry.models.groupassignee import GroupAssignee from sentry.models.groupbookmark import GroupBookmark from sentry.models.groupsearchview import GroupSearchView @@ -582,6 +582,7 @@ def merge_users(self, *, organization_id: int, from_user_id: int, to_user_id: in AlertRuleActivity, Dashboard, DashboardFavoriteUser, + DashboardRevision, GroupAssignee, GroupBookmark, GroupSeen, diff --git a/src/sentry/testutils/helpers/backups.py b/src/sentry/testutils/helpers/backups.py index f000b211a16f6b..741a2e8adb630f 100644 --- a/src/sentry/testutils/helpers/backups.py +++ b/src/sentry/testutils/helpers/backups.py @@ -67,6 +67,7 @@ Dashboard, DashboardFavoriteUser, DashboardLastVisited, + DashboardRevision, DashboardTombstone, ) from sentry.models.dashboard_permissions import DashboardPermissions @@ -600,6 +601,12 @@ def create_exhaustive_organization( field="count()", dashboard=linked_dashboard, ) + DashboardRevision.objects.create( + dashboard=dashboard, + created_by_id=owner_id, + title=dashboard.title, + snapshot_schema_version=1, + ) DashboardTombstone.objects.create(organization=org, slug=f"test-tombstone-in-{slug}") # *Search diff --git a/tests/sentry/users/models/test_user.py b/tests/sentry/users/models/test_user.py index 945c8a7048e781..8c7459a1213e8e 100644 --- a/tests/sentry/users/models/test_user.py +++ b/tests/sentry/users/models/test_user.py @@ -16,7 +16,7 @@ from sentry.incidents.models.incident import IncidentActivity from sentry.models.activity import Activity from sentry.models.authidentity import AuthIdentity -from sentry.models.dashboard import Dashboard, DashboardFavoriteUser +from sentry.models.dashboard import Dashboard, DashboardFavoriteUser, DashboardRevision from sentry.models.groupassignee import GroupAssignee from sentry.models.groupbookmark import GroupBookmark from sentry.models.groupsearchview import GroupSearchView @@ -498,6 +498,7 @@ def test_duplicate_memberships(self, expected_models: list[type[Model]]) -> None AlertRuleActivity, Dashboard, DashboardFavoriteUser, + DashboardRevision, GroupAssignee, GroupBookmark, GroupSeen, @@ -541,6 +542,7 @@ def test_only_source_user_is_member_of_organization( AlertRuleActivity, Dashboard, DashboardFavoriteUser, + DashboardRevision, GroupAssignee, GroupBookmark, GroupSeen,