From 47743c6dc39a2c34d46e2f9461fd382a9c9e1ac2 Mon Sep 17 00:00:00 2001 From: Nar Saynorath Date: Wed, 9 Sep 2026 11:00:23 -0400 Subject: [PATCH 1/3] fix(prebuilt-dashboards): Allow feature gating prebuilt dashboards --- .../endpoints/organization_dashboards.py | 34 +++++++++++++------ .../endpoints/test_organization_dashboards.py | 26 ++++++++++++-- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/sentry/dashboards/endpoints/organization_dashboards.py b/src/sentry/dashboards/endpoints/organization_dashboards.py index d1e8322615f5..81857f224a47 100644 --- a/src/sentry/dashboards/endpoints/organization_dashboards.py +++ b/src/sentry/dashboards/endpoints/organization_dashboards.py @@ -60,6 +60,8 @@ RpcOrganization, RpcUserOrganizationContext, ) +from sentry.users.models.user import User +from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service from sentry.utils.locking import UnableToAcquireLock @@ -104,6 +106,7 @@ class PrebuiltDashboard(TypedDict, total=False): title: Required[str] hidden: bool pre_favorited: bool + required_feature_flags: list[str] # Prebuilt dashboards store minimal fields in the database. The actual dashboard and widget settings are @@ -240,12 +243,14 @@ class PrebuiltDashboard(TypedDict, total=False): { "prebuilt_id": PrebuiltDashboardId.NODE_RUNTIME_METRICS, "title": "Node.js Runtime Metrics", + "required_feature_flags": ["organizations:tracemetrics-enabled"], }, ] def get_enabled_prebuilt_dashboards( organization: Organization, + actor: User | RpcUser, ) -> list[PrebuiltDashboard]: """ Returns the list of prebuilt dashboards that are enabled for the given organization, @@ -256,7 +261,14 @@ def get_enabled_prebuilt_dashboards( "organizations:dashboards-sync-all-registered-prebuilt-dashboards", organization, ) - all_prebuilt_dashboards = [dashboard for dashboard in PREBUILT_DASHBOARDS] + all_prebuilt_dashboards = [ + dashboard + for dashboard in PREBUILT_DASHBOARDS + if all( + features.has(feature, organization, actor=actor) + for feature in dashboard.get("required_feature_flags", []) + ) + ] if should_sync_all_registered_prebuilt_dashboards: return all_prebuilt_dashboards return [ @@ -266,7 +278,7 @@ def get_enabled_prebuilt_dashboards( ] -def sync_prebuilt_dashboards(organization: Organization) -> None: +def sync_prebuilt_dashboards(organization: Organization, actor: User | RpcUser) -> None: """ Queries the database to check if prebuilt dashboards have a Dashboard record and creates them if they don't, updates titles if they've changed, or deletes them @@ -274,7 +286,7 @@ def sync_prebuilt_dashboards(organization: Organization) -> None: """ with transaction.atomic(router.db_for_write(Dashboard)): - enabled_prebuilt_dashboards = get_enabled_prebuilt_dashboards(organization) + enabled_prebuilt_dashboards = get_enabled_prebuilt_dashboards(organization, actor) saved_prebuilt_dashboards = Dashboard.objects.filter( organization=organization, @@ -312,7 +324,7 @@ 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: +def sync_prebuilt_dashboards_favorited(organization: Organization, user: User | RpcUser) -> 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 @@ -321,7 +333,7 @@ def sync_prebuilt_dashboards_favorited(organization: Organization, user_id: int) New prebuilts are inserted alphabetically while the user's prebuilt stars are still in their default (alphabetical) order. """ - enabled_prebuilt_dashboards = get_enabled_prebuilt_dashboards(organization) + enabled_prebuilt_dashboards = get_enabled_prebuilt_dashboards(organization, user) pre_favorited_ids = [ d["prebuilt_id"] for d in enabled_prebuilt_dashboards if d.get("pre_favorited") ] @@ -332,7 +344,7 @@ def sync_prebuilt_dashboards_favorited(organization: Organization, user_id: int) prebuilt_favorited = list( DashboardFavoriteUser.objects.filter( organization=organization, - user_id=user_id, + user_id=user.id, favorited=True, dashboard__prebuilt_id__isnull=False, ) @@ -354,7 +366,7 @@ def sync_prebuilt_dashboards_favorited(organization: Organization, user_id: int) .exclude( id__in=DashboardFavoriteUser.objects.filter( organization=organization, - user_id=user_id, + user_id=user.id, ).values_list("dashboard_id", flat=True) ) .order_by("title") @@ -362,12 +374,12 @@ def sync_prebuilt_dashboards_favorited(organization: Organization, user_id: int) for dashboard in missing_dashboards: if is_default_order: DashboardFavoriteUser.objects.insert_favorite_dashboard_alphabetically( - organization, user_id, dashboard + organization, user.id, dashboard ) else: DashboardFavoriteUser.objects.insert_favorite_dashboard( organization=organization, - user_id=user_id, + user_id=user.id, dashboard=dashboard, ) @@ -470,7 +482,7 @@ def get( name="sync_prebuilt_dashboards", ) with lock.acquire(): - sync_prebuilt_dashboards(organization) + sync_prebuilt_dashboards(organization, request.user) except UnableToAcquireLock: pass except Exception as err: @@ -484,7 +496,7 @@ def get( name="sync_prebuilt_dashboards_favorited", ) with favorite_lock.acquire(): - sync_prebuilt_dashboards_favorited(organization, request.user.id) + sync_prebuilt_dashboards_favorited(organization, request.user) except UnableToAcquireLock: pass except Exception as err: diff --git a/tests/sentry/dashboards/endpoints/test_organization_dashboards.py b/tests/sentry/dashboards/endpoints/test_organization_dashboards.py index 65454f7de255..5a7891ec7990 100644 --- a/tests/sentry/dashboards/endpoints/test_organization_dashboards.py +++ b/tests/sentry/dashboards/endpoints/test_organization_dashboards.py @@ -2416,8 +2416,13 @@ def test_hidden_prebuilt_dashboards_included_with_show_hidden_filter(self) -> No assert PrebuiltDashboardId.BACKEND_QUERIES_SUMMARY in prebuilt_ids_in_response def test_node_runtime_metrics_prebuilt_dashboard_sync(self) -> None: - """The Node.js Runtime Metrics prebuilt dashboard syncs when enabled via options.""" - with self.feature("organizations:dashboards-prebuilt-insights-dashboards"): + """The Node.js Runtime Metrics dashboard syncs when metrics and its option are enabled.""" + with self.feature( + [ + "organizations:dashboards-prebuilt-insights-dashboards", + "organizations:tracemetrics-enabled", + ] + ): with override_options( {"dashboards.prebuilt-dashboard-ids": [PrebuiltDashboardId.NODE_RUNTIME_METRICS]} ): @@ -2437,6 +2442,23 @@ def test_node_runtime_metrics_prebuilt_dashboard_sync(self) -> None: ] assert len(prebuilt_in_response) == 1 + def test_node_runtime_metrics_prebuilt_dashboard_not_synced_without_metrics(self) -> None: + with self.feature("organizations:dashboards-prebuilt-insights-dashboards"): + with override_options( + {"dashboards.prebuilt-dashboard-ids": [PrebuiltDashboardId.NODE_RUNTIME_METRICS]} + ): + response = self.do_request("get", self.url) + assert response.status_code == 200 + + assert not Dashboard.objects.filter( + organization=self.organization, + prebuilt_id=PrebuiltDashboardId.NODE_RUNTIME_METRICS, + ).exists() + assert all( + dashboard.get("prebuiltId") != PrebuiltDashboardId.NODE_RUNTIME_METRICS + for dashboard in response.data + ) + def test_endpoint_creates_pre_favorited_prebuilt_dashboards(self) -> None: assert ( DashboardFavoriteUser.objects.filter( From c1b49d52bc1a5442ac3f7eda2f0fe207c08eed56 Mon Sep 17 00:00:00 2001 From: Nar Saynorath Date: Wed, 9 Sep 2026 11:16:08 -0400 Subject: [PATCH 2/3] Make flag checks organization based --- .../endpoints/organization_dashboards.py | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/sentry/dashboards/endpoints/organization_dashboards.py b/src/sentry/dashboards/endpoints/organization_dashboards.py index 81857f224a47..6ba81cb0fa88 100644 --- a/src/sentry/dashboards/endpoints/organization_dashboards.py +++ b/src/sentry/dashboards/endpoints/organization_dashboards.py @@ -60,8 +60,6 @@ RpcOrganization, RpcUserOrganizationContext, ) -from sentry.users.models.user import User -from sentry.users.services.user import RpcUser from sentry.users.services.user.service import user_service from sentry.utils.locking import UnableToAcquireLock @@ -250,7 +248,6 @@ class PrebuiltDashboard(TypedDict, total=False): def get_enabled_prebuilt_dashboards( organization: Organization, - actor: User | RpcUser, ) -> list[PrebuiltDashboard]: """ Returns the list of prebuilt dashboards that are enabled for the given organization, @@ -265,7 +262,7 @@ def get_enabled_prebuilt_dashboards( dashboard for dashboard in PREBUILT_DASHBOARDS if all( - features.has(feature, organization, actor=actor) + features.has(feature, organization) for feature in dashboard.get("required_feature_flags", []) ) ] @@ -278,7 +275,7 @@ def get_enabled_prebuilt_dashboards( ] -def sync_prebuilt_dashboards(organization: Organization, actor: User | RpcUser) -> None: +def sync_prebuilt_dashboards(organization: Organization) -> None: """ Queries the database to check if prebuilt dashboards have a Dashboard record and creates them if they don't, updates titles if they've changed, or deletes them @@ -286,7 +283,7 @@ def sync_prebuilt_dashboards(organization: Organization, actor: User | RpcUser) """ with transaction.atomic(router.db_for_write(Dashboard)): - enabled_prebuilt_dashboards = get_enabled_prebuilt_dashboards(organization, actor) + enabled_prebuilt_dashboards = get_enabled_prebuilt_dashboards(organization) saved_prebuilt_dashboards = Dashboard.objects.filter( organization=organization, @@ -324,7 +321,7 @@ def sync_prebuilt_dashboards(organization: Organization, actor: User | RpcUser) ).exclude(prebuilt_id__in=prebuilt_ids).delete() -def sync_prebuilt_dashboards_favorited(organization: Organization, user: User | RpcUser) -> None: +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 @@ -333,7 +330,7 @@ def sync_prebuilt_dashboards_favorited(organization: Organization, user: User | New prebuilts are inserted alphabetically while the user's prebuilt stars are still in their default (alphabetical) order. """ - enabled_prebuilt_dashboards = get_enabled_prebuilt_dashboards(organization, user) + 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") ] @@ -344,7 +341,7 @@ def sync_prebuilt_dashboards_favorited(organization: Organization, user: User | prebuilt_favorited = list( DashboardFavoriteUser.objects.filter( organization=organization, - user_id=user.id, + user_id=user_id, favorited=True, dashboard__prebuilt_id__isnull=False, ) @@ -366,7 +363,7 @@ def sync_prebuilt_dashboards_favorited(organization: Organization, user: User | .exclude( id__in=DashboardFavoriteUser.objects.filter( organization=organization, - user_id=user.id, + user_id=user_id, ).values_list("dashboard_id", flat=True) ) .order_by("title") @@ -374,12 +371,12 @@ def sync_prebuilt_dashboards_favorited(organization: Organization, user: User | for dashboard in missing_dashboards: if is_default_order: DashboardFavoriteUser.objects.insert_favorite_dashboard_alphabetically( - organization, user.id, dashboard + organization, user_id, dashboard ) else: DashboardFavoriteUser.objects.insert_favorite_dashboard( organization=organization, - user_id=user.id, + user_id=user_id, dashboard=dashboard, ) @@ -482,7 +479,7 @@ def get( name="sync_prebuilt_dashboards", ) with lock.acquire(): - sync_prebuilt_dashboards(organization, request.user) + sync_prebuilt_dashboards(organization) except UnableToAcquireLock: pass except Exception as err: @@ -496,7 +493,7 @@ def get( name="sync_prebuilt_dashboards_favorited", ) with favorite_lock.acquire(): - sync_prebuilt_dashboards_favorited(organization, request.user) + sync_prebuilt_dashboards_favorited(organization, request.user.id) except UnableToAcquireLock: pass except Exception as err: From 8b607da95a8af14b95a418cf9ba0e0ee5207760a Mon Sep 17 00:00:00 2001 From: Nar Saynorath Date: Wed, 9 Sep 2026 11:41:08 -0400 Subject: [PATCH 3/3] Move check down past dev flag --- .../endpoints/organization_dashboards.py | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/sentry/dashboards/endpoints/organization_dashboards.py b/src/sentry/dashboards/endpoints/organization_dashboards.py index 6ba81cb0fa88..1ca07ab93139 100644 --- a/src/sentry/dashboards/endpoints/organization_dashboards.py +++ b/src/sentry/dashboards/endpoints/organization_dashboards.py @@ -5,17 +5,7 @@ import sentry_sdk from django.db import IntegrityError, router, transaction -from django.db.models import ( - Case, - Exists, - F, - IntegerField, - OrderBy, - OuterRef, - Subquery, - Value, - When, -) +from django.db.models import Case, Exists, F, IntegerField, OrderBy, OuterRef, Subquery, Value, When from drf_spectacular.utils import extend_schema from rest_framework import status from rest_framework.request import Request @@ -258,20 +248,17 @@ def get_enabled_prebuilt_dashboards( "organizations:dashboards-sync-all-registered-prebuilt-dashboards", organization, ) - all_prebuilt_dashboards = [ - dashboard - for dashboard in PREBUILT_DASHBOARDS - if all( - features.has(feature, organization) - for feature in dashboard.get("required_feature_flags", []) - ) - ] + 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 + and all( + features.has(feature, organization) + for feature in dashboard.get("required_feature_flags", []) + ) ]