Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 87 additions & 17 deletions src/sentry/dashboards/endpoints/organization_dashboards.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from enum import IntEnum
from typing import Any, TypedDict
from typing import Any, Required, TypedDict

import sentry_sdk
from django.db import IntegrityError, router, transaction
Expand Down Expand Up @@ -92,9 +92,10 @@ class PrebuiltDashboardId(IntEnum):
BACKEND_CACHES = 28


class PrebuiltDashboard(TypedDict):
prebuilt_id: PrebuiltDashboardId
title: str
class PrebuiltDashboard(TypedDict, total=False):
prebuilt_id: Required[PrebuiltDashboardId]
title: Required[str]
pre_favorited: bool


# Prebuilt dashboards store minimal fields in the database. The actual dashboard and widget settings are
Expand Down Expand Up @@ -130,6 +131,7 @@ class PrebuiltDashboard(TypedDict):
{
"prebuilt_id": PrebuiltDashboardId.WEB_VITALS,
"title": "Web Vitals",
"pre_favorited": True,
},
{
"prebuilt_id": PrebuiltDashboardId.WEB_VITALS_SUMMARY,
Expand All @@ -138,6 +140,7 @@ class PrebuiltDashboard(TypedDict):
{
"prebuilt_id": PrebuiltDashboardId.MOBILE_VITALS,
"title": "Mobile Vitals",
"pre_favorited": True,
},
{
"prebuilt_id": PrebuiltDashboardId.MOBILE_VITALS_APP_STARTS,
Expand All @@ -154,6 +157,7 @@ class PrebuiltDashboard(TypedDict):
{
"prebuilt_id": PrebuiltDashboardId.BACKEND_OVERVIEW,
"title": "Backend Overview",
"pre_favorited": True,
},
{
"prebuilt_id": PrebuiltDashboardId.MOBILE_SESSION_HEALTH,
Expand Down Expand Up @@ -190,10 +194,12 @@ class PrebuiltDashboard(TypedDict):
{
"prebuilt_id": PrebuiltDashboardId.AI_AGENTS_OVERVIEW,
"title": "AI Agents Overview",
"pre_favorited": True,
},
{
"prebuilt_id": PrebuiltDashboardId.MCP_OVERVIEW,
"title": "MCP Overview",
"pre_favorited": True,
},
{
"prebuilt_id": PrebuiltDashboardId.LARAVEL_OVERVIEW,
Expand Down Expand Up @@ -222,6 +228,28 @@ class PrebuiltDashboard(TypedDict):
]


def get_enabled_prebuilt_dashboards(
organization: Organization,
) -> list[PrebuiltDashboard]:
"""
Returns the list of prebuilt dashboards that are enabled for the given organization,
based on the prebuilt-dashboard-ids option and the sync-all feature flag.
"""
enabled_prebuilt_dashboard_ids = options.get("dashboards.prebuilt-dashboard-ids")
should_sync_all_registered_prebuilt_dashboards = features.has(
"organizations:dashboards-sync-all-registered-prebuilt-dashboards",
organization,
)
all_prebuilt_dashboards = [dashboard for dashboard in PREBUILT_DASHBOARDS]
if should_sync_all_registered_prebuilt_dashboards:
return all_prebuilt_dashboards
return [
dashboard
for dashboard in all_prebuilt_dashboards
if dashboard["prebuilt_id"] in enabled_prebuilt_dashboard_ids
]


def sync_prebuilt_dashboards(organization: Organization) -> None:
"""
Queries the database to check if prebuilt dashboards have a Dashboard record and
Expand All @@ -230,16 +258,7 @@ def sync_prebuilt_dashboards(organization: Organization) -> None:
"""

with transaction.atomic(router.db_for_write(Dashboard)):
enabled_prebuilt_dashboard_ids = options.get("dashboards.prebuilt-dashboard-ids")
enabled_prebuilt_dashboards = [
dashboard
for dashboard in PREBUILT_DASHBOARDS
if dashboard["prebuilt_id"] in enabled_prebuilt_dashboard_ids
or features.has(
"organizations:dashboards-sync-all-registered-prebuilt-dashboards",
organization,
)
]
enabled_prebuilt_dashboards = get_enabled_prebuilt_dashboards(organization)

saved_prebuilt_dashboards = Dashboard.objects.filter(
organization=organization,
Expand Down Expand Up @@ -277,6 +296,41 @@ def sync_prebuilt_dashboards(organization: Organization) -> None:
).exclude(prebuilt_id__in=prebuilt_ids).delete()


def sync_prebuilt_dashboards_favorited(organization: Organization, user_id: int) -> None:
"""
Checks if pre-favorited prebuilt dashboards have a DashboardFavoriteUser record for the
user, and creates them if they don't. This ensures that certain prebuilt dashboards are
favorited by default for all users.
"""
enabled_prebuilt_dashboards = get_enabled_prebuilt_dashboards(organization)
pre_favorited_ids = [
d["prebuilt_id"] for d in enabled_prebuilt_dashboards if d.get("pre_favorited")
]
if not pre_favorited_ids:
return

with transaction.atomic(router.db_for_write(DashboardFavoriteUser)):
prebuilt_dashboards_without_favorite = (
Dashboard.objects.filter(
organization=organization,
prebuilt_id__in=pre_favorited_ids,
)
.exclude(
id__in=DashboardFavoriteUser.objects.filter(
organization=organization,
user_id=user_id,
).values_list("dashboard_id", flat=True)
)
.order_by("prebuilt_id")
)
for dashboard in prebuilt_dashboards_without_favorite:
DashboardFavoriteUser.objects.insert_favorite_dashboard(
organization=organization,
user_id=user_id,
dashboard=dashboard,
)


class OrganizationDashboardsPermission(OrganizationPermission):
scope_map = {
"GET": ["org:read", "org:write", "org:admin"],
Expand Down Expand Up @@ -360,15 +414,31 @@ def get(self, request: Request, organization: Organization) -> Response:
name="sync_prebuilt_dashboards",
)
with lock.acquire():
# Adds prebuilt dashboards to the database if they don't exist.
# Deletes old prebuilt dashboards from the database if they should no longer exist.
sync_prebuilt_dashboards(organization)
except UnableToAcquireLock:
# Another process is already syncing the prebuilt dashboards. We can skip syncing this time.
pass
except Exception as err:
sentry_sdk.capture_exception(err)

# Favorite pre-favorited prebuilt dashboards for the user
# TODO - remove this flag check once we confirm the sync is proper, this should be done for all users
if features.has(
"organizations:dashboards-sync-all-registered-prebuilt-dashboards",
organization,
):
try:
favorite_lock = locks.get(
f"dashboards:sync_prebuilt_dashboards_favorited:{organization.id}:{request.user.id}",
duration=10,
name="sync_prebuilt_dashboards_favorited",
)
with favorite_lock.acquire():
sync_prebuilt_dashboards_favorited(organization, request.user.id)
except UnableToAcquireLock:
pass
except Exception as err:
sentry_sdk.capture_exception(err)

filters = request.query_params.getlist("filter")

dashboards = Dashboard.objects.filter(organization_id=organization.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2269,6 +2269,37 @@ def test_get_with_exclude_prebuilt(self) -> None:
assert response.status_code == 200
assert len(response.data) == total_count - prebuilt_dashboards_count

def test_endpoint_creates_favorites_for_pre_favorited_prebuilt_dashboards(self) -> None:
assert (
DashboardFavoriteUser.objects.filter(
organization=self.organization, user_id=self.user.id
).count()
== 0
)

pre_favorited_prebuilt_ids = {
d["prebuilt_id"] for d in PREBUILT_DASHBOARDS if d.get("pre_favorited")
}

with self.feature(
[
"organizations:dashboards-prebuilt-insights-dashboards",
"organizations:dashboards-sync-all-registered-prebuilt-dashboards",
]
):
response = self.do_request("get", self.url)
assert response.status_code == 200

favorites = DashboardFavoriteUser.objects.filter(
organization=self.organization, user_id=self.user.id
)
favorited_prebuilt_ids = set(
Dashboard.objects.filter(
id__in=favorites.values_list("dashboard_id", flat=True)
).values_list("prebuilt_id", flat=True)
)
assert favorited_prebuilt_ids == pre_favorited_prebuilt_ids

def test_post_with_text_widget(self) -> None:
with self.feature("organizations:dashboards-text-widgets"):
data = {
Expand Down
Loading