Skip to content

Commit c356a07

Browse files
shruthilayajandrewshie-sentry
authored andcommitted
feat: Add dashboard widget snapshot model (#95236)
We'd like to store a snapshot of the last valid transaction widget while we do an in-place migration of widgets from transactions -> spans. This allows us to recover the original transaction state of the widget in case something goes horribly wrong.
1 parent 15b1797 commit c356a07

File tree

4 files changed

+67
-1
lines changed

4 files changed

+67
-1
lines changed

migrations_lockfile.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ preprod: 0010_actual_drop_preprod_artifact_analysis_file_id_col
2727

2828
replays: 0006_add_bulk_delete_job
2929

30-
sentry: 0948_ds_waiver_org_fk_not_db_constr
30+
sentry: 0949_add_dashboard_widget_snapshot_model
3131

3232
social_auth: 0003_social_auth_json_field
3333

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Generated by Django 5.2.1 on 2025-07-15 15:03
2+
3+
import django.db.models.deletion
4+
from django.db import migrations
5+
6+
import sentry.db.models.fields.bounded
7+
import sentry.db.models.fields.foreignkey
8+
import sentry.db.models.fields.jsonfield
9+
from sentry.new_migrations.migrations import CheckedMigration
10+
11+
12+
class Migration(CheckedMigration):
13+
# This flag is used to mark that a migration shouldn't be automatically run in production.
14+
# This should only be used for operations where it's safe to run the migration after your
15+
# code has deployed. So this should not be used for most operations that alter the schema
16+
# of a table.
17+
# Here are some things that make sense to mark as post deployment:
18+
# - Large data migrations. Typically we want these to be run manually so that they can be
19+
# monitored and not block the deploy for a long period of time while they run.
20+
# - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
21+
# run this outside deployments so that we don't block them. Note that while adding an index
22+
# is a schema change, it's completely safe to run the operation after the code has deployed.
23+
# Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment
24+
25+
is_post_deployment = False
26+
27+
dependencies = [
28+
("sentry", "0948_ds_waiver_org_fk_not_db_constr"),
29+
]
30+
31+
operations = [
32+
migrations.CreateModel(
33+
name="DashboardWidgetSnapshot",
34+
fields=[
35+
(
36+
"id",
37+
sentry.db.models.fields.bounded.BoundedBigAutoField(
38+
primary_key=True, serialize=False
39+
),
40+
),
41+
("data", sentry.db.models.fields.jsonfield.JSONField(default=dict)),
42+
(
43+
"widget",
44+
sentry.db.models.fields.foreignkey.FlexibleForeignKey(
45+
on_delete=django.db.models.deletion.CASCADE, to="sentry.dashboardwidget"
46+
),
47+
),
48+
],
49+
options={
50+
"abstract": False,
51+
},
52+
),
53+
]

src/sentry/models/dashboard_widget.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,14 @@ class Meta:
258258
__repr__ = sane_repr("extraction_state", "spec_hashes")
259259

260260

261+
@region_silo_model
262+
class DashboardWidgetSnapshot(Model):
263+
__relocation_scope__ = RelocationScope.Organization
264+
265+
widget = FlexibleForeignKey("sentry.DashboardWidget")
266+
data: models.Field[dict[str, Any], dict[str, Any]] = JSONField()
267+
268+
261269
@region_silo_model
262270
class DashboardWidget(Model):
263271
"""

src/sentry/testutils/helpers/backups.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
DashboardWidget,
7171
DashboardWidgetQuery,
7272
DashboardWidgetQueryOnDemand,
73+
DashboardWidgetSnapshot,
7374
DashboardWidgetTypes,
7475
)
7576
from sentry.models.dynamicsampling import CustomDynamicSamplingRule
@@ -596,6 +597,10 @@ def create_exhaustive_organization(
596597
extraction_state=DashboardWidgetQueryOnDemand.OnDemandExtractionState.DISABLED_NOT_APPLICABLE,
597598
spec_hashes=[],
598599
)
600+
DashboardWidgetSnapshot.objects.create(
601+
widget=widget,
602+
data={"test": "data"},
603+
)
599604
DashboardTombstone.objects.create(organization=org, slug=f"test-tombstone-in-{slug}")
600605

601606
# *Search

0 commit comments

Comments
 (0)