From dc133f6ca6420f6b1366077d4259254346edb6ec Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Tue, 11 Nov 2025 17:07:44 -0500 Subject: [PATCH] ref(crons): Refresh detector when environment is muted/unmuted Fixed [NEW-592: Muting cron environment does not refresh detector](https://linear.app/getsentry/issue/NEW-592/muting-cron-environment-does-not-refresh-detector) When muting/unmuting or deleting a cron monitor environment from the detector details page, the parent detector state wasn't being refreshed, causing stale data to display. Added onEnvironmentUpdated callback to DetailsTimeline component that invalidates the detector query when environment status changes, ensuring the UI stays in sync with the backend state. --- static/app/actionCreators/monitors.tsx | 2 +- .../components/details/cron/index.spec.tsx | 67 +++++++++++++++++++ .../components/details/cron/index.tsx | 16 ++++- .../crons/components/detailsTimeline.tsx | 10 ++- 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/static/app/actionCreators/monitors.tsx b/static/app/actionCreators/monitors.tsx index 3e45c06d2d7a68..60060d8e7349d9 100644 --- a/static/app/actionCreators/monitors.tsx +++ b/static/app/actionCreators/monitors.tsx @@ -102,7 +102,7 @@ export async function setEnvironmentIsMuted( try { const resp = await api.requestPromise( - `/projects/${orgId}/${monitor.project.slug}/monitors/${monitor.slug}/environments/${environment}`, + `/projects/${orgId}/${monitor.project.slug}/monitors/${monitor.slug}/environments/${environment}/`, {method: 'PUT', data: {isMuted}} ); clearIndicators(); diff --git a/static/app/views/detectors/components/details/cron/index.spec.tsx b/static/app/views/detectors/components/details/cron/index.spec.tsx index d5d1e677525ae3..79a4cebb719216 100644 --- a/static/app/views/detectors/components/details/cron/index.spec.tsx +++ b/static/app/views/detectors/components/details/cron/index.spec.tsx @@ -342,4 +342,71 @@ describe('CronDetectorDetails - check-ins', () => { expect(screen.getByRole('button', {name: 'Enable'})).toBeInTheDocument(); }); }); + + describe('environment muting', () => { + it('refetches detector when environment is muted', async () => { + const detectorWithMultipleEnvs = CronDetectorFixture({ + id: '1', + projectId: project.id, + dataSources: [ + CronMonitorDataSourceFixture({ + queryObj: { + ...CronMonitorDataSourceFixture().queryObj, + environments: [ + CronMonitorEnvironmentFixture({ + name: 'production', + lastCheckIn: '2025-01-01T00:00:00Z', + isMuted: false, + }), + CronMonitorEnvironmentFixture({ + name: 'staging', + lastCheckIn: '2025-01-01T00:00:00Z', + isMuted: false, + }), + ], + }, + }), + ], + }); + + const muteRequest = MockApiClient.addMockResponse({ + url: `/projects/org-slug/${project.slug}/monitors/${detectorWithMultipleEnvs.dataSources[0].queryObj.slug}/environments/production/`, + method: 'PUT', + body: {}, + }); + + const detectorRefetchRequest = MockApiClient.addMockResponse({ + url: `/organizations/org-slug/detectors/1/`, + body: detectorWithMultipleEnvs, + }); + + render( + + ); + + await screen.findByText('Recent Check-Ins'); + + expect(detectorRefetchRequest).toHaveBeenCalledTimes(1); + + const envButtons = screen.getAllByRole('button', { + name: 'Monitor environment actions', + }); + await userEvent.click(envButtons[0]!); + + await userEvent.click( + await screen.findByRole('menuitemradio', {name: 'Mute Environment'}) + ); + + expect(muteRequest).toHaveBeenCalledTimes(1); + expect(muteRequest).toHaveBeenCalledWith( + expect.stringContaining('/environments/production'), + expect.objectContaining({ + method: 'PUT', + data: {isMuted: true}, + }) + ); + + expect(detectorRefetchRequest).toHaveBeenCalledTimes(2); + }); + }); }); diff --git a/static/app/views/detectors/components/details/cron/index.tsx b/static/app/views/detectors/components/details/cron/index.tsx index 377d0f618eac39..e1364ca64ebab3 100644 --- a/static/app/views/detectors/components/details/cron/index.tsx +++ b/static/app/views/detectors/components/details/cron/index.tsx @@ -22,6 +22,7 @@ import {t, tn} from 'sentry/locale'; import type {Project} from 'sentry/types/project'; import type {CronDetector} from 'sentry/types/workflowEngine/detectors'; import toArray from 'sentry/utils/array/toArray'; +import {useQueryClient} from 'sentry/utils/queryClient'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import { @@ -35,7 +36,10 @@ import {DisabledAlert} from 'sentry/views/detectors/components/details/common/di import {DetectorExtraDetails} from 'sentry/views/detectors/components/details/common/extraDetails'; import {DetectorDetailsHeader} from 'sentry/views/detectors/components/details/common/header'; import {DetectorDetailsOpenPeriodIssues} from 'sentry/views/detectors/components/details/common/openPeriodIssues'; -import {useDetectorQuery} from 'sentry/views/detectors/hooks'; +import { + makeDetectorDetailsQueryKey, + useDetectorQuery, +} from 'sentry/views/detectors/hooks'; import {DetailsTimeline} from 'sentry/views/insights/crons/components/detailsTimeline'; import {DetailsTimelineLegend} from 'sentry/views/insights/crons/components/detailsTimelineLegend'; import {MonitorCheckIns} from 'sentry/views/insights/crons/components/monitorCheckIns'; @@ -69,6 +73,7 @@ export function CronDetectorDetails({detector, project}: CronDetectorDetailsProp const userTimezone = useTimezone(); const [timezoneOverride, setTimezoneOverride] = useState(userTimezone); const openDocsPanel = useDocsPanel(dataSource.queryObj.slug, project); + const queryClient = useQueryClient(); useDetectorQuery(detector.id, { staleTime: 0, @@ -83,6 +88,14 @@ export function CronDetectorDetails({detector, project}: CronDetectorDetailsProp }, }); + const handleEnvironmentUpdated = useCallback(() => { + const queryKey = makeDetectorDetailsQueryKey({ + orgSlug: organization.slug, + detectorId: detector.id, + }); + queryClient.invalidateQueries({queryKey}); + }, [queryClient, organization.slug, detector.id]); + const {checkinErrors, handleDismissError} = useMonitorProcessingErrors({ organization, projectId: project.id, @@ -174,6 +187,7 @@ export function CronDetectorDetails({detector, project}: CronDetectorDetailsProp void; /** * Called when monitor stats have been loaded for this timeline. */ onStatsLoaded?: (stats: MonitorBucket[]) => void; } -export function DetailsTimeline({monitor, onStatsLoaded}: Props) { +export function DetailsTimeline({monitor, onStatsLoaded, onEnvironmentUpdated}: Props) { const organization = useOrganization(); const location = useLocation(); const api = useApi(); @@ -89,6 +93,8 @@ export function DetailsTimeline({monitor, onStatsLoaded}: Props) { } : undefined; }); + + onEnvironmentUpdated?.(); }; const handleToggleMuteEnvironment = async (env: string, isMuted: boolean) => { @@ -119,6 +125,8 @@ export function DetailsTimeline({monitor, onStatsLoaded}: Props) { } : undefined; }); + + onEnvironmentUpdated?.(); }; return (