Skip to content

Commit b2b37b8

Browse files
ref(crons): Refresh detector when environment is muted/unmuted (#103186)
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. Fixes [NEW-592: Muting cron environment does not refresh detector](https://linear.app/getsentry/issue/NEW-592/muting-cron-environment-does-not-refresh-detector)
1 parent b1fe652 commit b2b37b8

File tree

4 files changed

+92
-3
lines changed

4 files changed

+92
-3
lines changed

static/app/actionCreators/monitors.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export async function setEnvironmentIsMuted(
102102

103103
try {
104104
const resp = await api.requestPromise(
105-
`/projects/${orgId}/${monitor.project.slug}/monitors/${monitor.slug}/environments/${environment}`,
105+
`/projects/${orgId}/${monitor.project.slug}/monitors/${monitor.slug}/environments/${environment}/`,
106106
{method: 'PUT', data: {isMuted}}
107107
);
108108
clearIndicators();

static/app/views/detectors/components/details/cron/index.spec.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,4 +342,71 @@ describe('CronDetectorDetails - check-ins', () => {
342342
expect(screen.getByRole('button', {name: 'Enable'})).toBeInTheDocument();
343343
});
344344
});
345+
346+
describe('environment muting', () => {
347+
it('refetches detector when environment is muted', async () => {
348+
const detectorWithMultipleEnvs = CronDetectorFixture({
349+
id: '1',
350+
projectId: project.id,
351+
dataSources: [
352+
CronMonitorDataSourceFixture({
353+
queryObj: {
354+
...CronMonitorDataSourceFixture().queryObj,
355+
environments: [
356+
CronMonitorEnvironmentFixture({
357+
name: 'production',
358+
lastCheckIn: '2025-01-01T00:00:00Z',
359+
isMuted: false,
360+
}),
361+
CronMonitorEnvironmentFixture({
362+
name: 'staging',
363+
lastCheckIn: '2025-01-01T00:00:00Z',
364+
isMuted: false,
365+
}),
366+
],
367+
},
368+
}),
369+
],
370+
});
371+
372+
const muteRequest = MockApiClient.addMockResponse({
373+
url: `/projects/org-slug/${project.slug}/monitors/${detectorWithMultipleEnvs.dataSources[0].queryObj.slug}/environments/production/`,
374+
method: 'PUT',
375+
body: {},
376+
});
377+
378+
const detectorRefetchRequest = MockApiClient.addMockResponse({
379+
url: `/organizations/org-slug/detectors/1/`,
380+
body: detectorWithMultipleEnvs,
381+
});
382+
383+
render(
384+
<CronDetectorDetails detector={detectorWithMultipleEnvs} project={project} />
385+
);
386+
387+
await screen.findByText('Recent Check-Ins');
388+
389+
expect(detectorRefetchRequest).toHaveBeenCalledTimes(1);
390+
391+
const envButtons = screen.getAllByRole('button', {
392+
name: 'Monitor environment actions',
393+
});
394+
await userEvent.click(envButtons[0]!);
395+
396+
await userEvent.click(
397+
await screen.findByRole('menuitemradio', {name: 'Mute Environment'})
398+
);
399+
400+
expect(muteRequest).toHaveBeenCalledTimes(1);
401+
expect(muteRequest).toHaveBeenCalledWith(
402+
expect.stringContaining('/environments/production'),
403+
expect.objectContaining({
404+
method: 'PUT',
405+
data: {isMuted: true},
406+
})
407+
);
408+
409+
expect(detectorRefetchRequest).toHaveBeenCalledTimes(2);
410+
});
411+
});
345412
});

static/app/views/detectors/components/details/cron/index.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {t, tn} from 'sentry/locale';
2222
import type {Project} from 'sentry/types/project';
2323
import type {CronDetector} from 'sentry/types/workflowEngine/detectors';
2424
import toArray from 'sentry/utils/array/toArray';
25+
import {useQueryClient} from 'sentry/utils/queryClient';
2526
import {useLocation} from 'sentry/utils/useLocation';
2627
import useOrganization from 'sentry/utils/useOrganization';
2728
import {
@@ -35,7 +36,10 @@ import {DisabledAlert} from 'sentry/views/detectors/components/details/common/di
3536
import {DetectorExtraDetails} from 'sentry/views/detectors/components/details/common/extraDetails';
3637
import {DetectorDetailsHeader} from 'sentry/views/detectors/components/details/common/header';
3738
import {DetectorDetailsOpenPeriodIssues} from 'sentry/views/detectors/components/details/common/openPeriodIssues';
38-
import {useDetectorQuery} from 'sentry/views/detectors/hooks';
39+
import {
40+
makeDetectorDetailsQueryKey,
41+
useDetectorQuery,
42+
} from 'sentry/views/detectors/hooks';
3943
import {DetailsTimeline} from 'sentry/views/insights/crons/components/detailsTimeline';
4044
import {DetailsTimelineLegend} from 'sentry/views/insights/crons/components/detailsTimelineLegend';
4145
import {MonitorCheckIns} from 'sentry/views/insights/crons/components/monitorCheckIns';
@@ -69,6 +73,7 @@ export function CronDetectorDetails({detector, project}: CronDetectorDetailsProp
6973
const userTimezone = useTimezone();
7074
const [timezoneOverride, setTimezoneOverride] = useState(userTimezone);
7175
const openDocsPanel = useDocsPanel(dataSource.queryObj.slug, project);
76+
const queryClient = useQueryClient();
7277

7378
useDetectorQuery<CronDetector>(detector.id, {
7479
staleTime: 0,
@@ -83,6 +88,14 @@ export function CronDetectorDetails({detector, project}: CronDetectorDetailsProp
8388
},
8489
});
8590

91+
const handleEnvironmentUpdated = useCallback(() => {
92+
const queryKey = makeDetectorDetailsQueryKey({
93+
orgSlug: organization.slug,
94+
detectorId: detector.id,
95+
});
96+
queryClient.invalidateQueries({queryKey});
97+
}, [queryClient, organization.slug, detector.id]);
98+
8699
const {checkinErrors, handleDismissError} = useMonitorProcessingErrors({
87100
organization,
88101
projectId: project.id,
@@ -174,6 +187,7 @@ export function CronDetectorDetails({detector, project}: CronDetectorDetailsProp
174187
<DetailsTimeline
175188
monitor={filteredMonitor}
176189
onStatsLoaded={checkHasUnknown}
190+
onEnvironmentUpdated={handleEnvironmentUpdated}
177191
/>
178192
<ErrorBoundary mini>
179193
<DetectorDetailsOpenPeriodIssues

static/app/views/insights/crons/components/detailsTimeline.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@ import {CronServiceIncidents} from './serviceIncidents';
3030

3131
interface Props {
3232
monitor: Monitor;
33+
/**
34+
* Called when an environment is updated (muted/unmuted/deleted).
35+
*/
36+
onEnvironmentUpdated?: () => void;
3337
/**
3438
* Called when monitor stats have been loaded for this timeline.
3539
*/
3640
onStatsLoaded?: (stats: MonitorBucket[]) => void;
3741
}
3842

39-
export function DetailsTimeline({monitor, onStatsLoaded}: Props) {
43+
export function DetailsTimeline({monitor, onStatsLoaded, onEnvironmentUpdated}: Props) {
4044
const organization = useOrganization();
4145
const location = useLocation();
4246
const api = useApi();
@@ -89,6 +93,8 @@ export function DetailsTimeline({monitor, onStatsLoaded}: Props) {
8993
}
9094
: undefined;
9195
});
96+
97+
onEnvironmentUpdated?.();
9298
};
9399

94100
const handleToggleMuteEnvironment = async (env: string, isMuted: boolean) => {
@@ -119,6 +125,8 @@ export function DetailsTimeline({monitor, onStatsLoaded}: Props) {
119125
}
120126
: undefined;
121127
});
128+
129+
onEnvironmentUpdated?.();
122130
};
123131

124132
return (

0 commit comments

Comments
 (0)