Skip to content

Commit 7ae9831

Browse files
narsaynorathandrewshie-sentry
authored andcommitted
feat(dashboards): Add model for storing visit information (#95361)
Adds a model that will be created for a singular visit by a user to a particular dashboard. This is to move us away from a generic global "last visited" timestamp on dashboards and something more focused on the user themselves. Closes DAIN-731
1 parent a464724 commit 7ae9831

File tree

5 files changed

+102
-2
lines changed

5 files changed

+102
-2
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: 0946_add_has_mcp_insights_flag
30+
sentry: 0947_add_dashboard_last_visited_model
3131

3232
social_auth: 0003_social_auth_json_field
3333

src/sentry/backup/comparators.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,9 @@ def get_default_comparators() -> dict[str, list[JSONScrubbingComparator]]:
798798
"sentry.dashboardfavoriteuser": [
799799
DateUpdatedComparator("date_added", "date_updated"),
800800
],
801+
"sentry.dashboardlastvisited": [
802+
DateUpdatedComparator("last_visited", "date_added", "date_updated"),
803+
],
801804
"sentry.groupsearchview": [DateUpdatedComparator("date_updated")],
802805
"sentry.groupsearchviewlastvisited": [
803806
DateUpdatedComparator("last_visited", "date_added", "date_updated")
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Generated by Django 5.2.1 on 2025-07-14 14:23
2+
3+
import django.db.models.deletion
4+
import django.utils.timezone
5+
from django.db import migrations, models
6+
7+
import sentry.db.models.fields.bounded
8+
import sentry.db.models.fields.foreignkey
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", "0946_add_has_mcp_insights_flag"),
29+
]
30+
31+
operations = [
32+
migrations.CreateModel(
33+
name="DashboardLastVisited",
34+
fields=[
35+
(
36+
"id",
37+
sentry.db.models.fields.bounded.BoundedBigAutoField(
38+
primary_key=True, serialize=False
39+
),
40+
),
41+
("date_updated", models.DateTimeField(auto_now=True)),
42+
("date_added", models.DateTimeField(auto_now_add=True)),
43+
("last_visited", models.DateTimeField(default=django.utils.timezone.now)),
44+
(
45+
"dashboard",
46+
sentry.db.models.fields.foreignkey.FlexibleForeignKey(
47+
on_delete=django.db.models.deletion.CASCADE, to="sentry.dashboard"
48+
),
49+
),
50+
(
51+
"member",
52+
sentry.db.models.fields.foreignkey.FlexibleForeignKey(
53+
on_delete=django.db.models.deletion.CASCADE, to="sentry.organizationmember"
54+
),
55+
),
56+
],
57+
options={
58+
"db_table": "sentry_dashboardlastvisited",
59+
"constraints": [
60+
models.UniqueConstraint(
61+
fields=("member_id", "dashboard_id"),
62+
name="sentry_dashboardlastvisited_unique_last_visited_per_org_member",
63+
)
64+
],
65+
},
66+
),
67+
]

src/sentry/models/dashboard.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,26 @@ class Meta:
360360
__repr__ = sane_repr("organization", "slug")
361361

362362

363+
@region_silo_model
364+
class DashboardLastVisited(DefaultFieldsModel):
365+
__relocation_scope__ = RelocationScope.Organization
366+
367+
dashboard = FlexibleForeignKey("sentry.Dashboard", on_delete=models.CASCADE)
368+
member = FlexibleForeignKey("sentry.OrganizationMember", on_delete=models.CASCADE)
369+
370+
last_visited = models.DateTimeField(null=False, default=timezone.now)
371+
372+
class Meta:
373+
app_label = "sentry"
374+
db_table = "sentry_dashboardlastvisited"
375+
constraints = [
376+
UniqueConstraint(
377+
fields=["member_id", "dashboard_id"],
378+
name="sentry_dashboardlastvisited_unique_last_visited_per_org_member",
379+
)
380+
]
381+
382+
363383
# Prebuilt dashboards are added to API responses for all accounts that have
364384
# not added a tombstone for the id value. If you change the id of a prebuilt dashboard
365385
# it will invalidate all the tombstone records that already exist.

src/sentry/testutils/helpers/backups.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@
5959
from sentry.models.authidentity import AuthIdentity
6060
from sentry.models.authprovider import AuthProvider
6161
from sentry.models.counter import Counter
62-
from sentry.models.dashboard import Dashboard, DashboardFavoriteUser, DashboardTombstone
62+
from sentry.models.dashboard import (
63+
Dashboard,
64+
DashboardFavoriteUser,
65+
DashboardLastVisited,
66+
DashboardTombstone,
67+
)
6368
from sentry.models.dashboard_permissions import DashboardPermissions
6469
from sentry.models.dashboard_widget import (
6570
DashboardWidget,
@@ -567,6 +572,11 @@ def create_exhaustive_organization(
567572
user_id=owner_id,
568573
organization=org,
569574
)
575+
DashboardLastVisited.objects.create(
576+
dashboard=dashboard,
577+
member=invited,
578+
last_visited=timezone.now(),
579+
)
570580
permissions = DashboardPermissions.objects.create(
571581
is_editable_by_everyone=True, dashboard=dashboard
572582
)

0 commit comments

Comments
 (0)