diff --git a/static/app/actionCreators/uptime.tsx b/static/app/actionCreators/uptime.tsx index d13b82ba46081a..5068a64dd19a73 100644 --- a/static/app/actionCreators/uptime.tsx +++ b/static/app/actionCreators/uptime.tsx @@ -7,12 +7,13 @@ import { } from 'sentry/actionCreators/indicator'; import type {Client} from 'sentry/api'; import {t} from 'sentry/locale'; +import type {Organization} from 'sentry/types/organization'; import type RequestError from 'sentry/utils/requestError/requestError'; import type {UptimeRule} from 'sentry/views/alerts/rules/uptime/types'; export async function updateUptimeRule( api: Client, - orgId: string, + org: Organization, uptimeRule: UptimeRule, data: Partial ): Promise { @@ -20,8 +21,13 @@ export async function updateUptimeRule( try { const resp = await api.requestPromise( - `/projects/${orgId}/${uptimeRule.projectSlug}/uptime/${uptimeRule.id}/`, - {method: 'PUT', data} + `/projects/${org.slug}/${uptimeRule.projectSlug}/uptime/${uptimeRule.detectorId}/`, + { + method: 'PUT', + data, + // TODO(epurkhiser): Can be removed once these APIs only take detectors + query: {useDetectorId: 1}, + } ); clearIndicators(); return resp; @@ -42,3 +48,25 @@ export async function updateUptimeRule( return null; } + +export async function deleteUptimeRule( + api: Client, + org: Organization, + uptimeRule: UptimeRule +) { + addLoadingMessage('Deleting uptime alert rule...'); + + try { + await api.requestPromise( + `/projects/${org.slug}/${uptimeRule.projectSlug}/uptime/${uptimeRule.detectorId}/`, + { + method: 'DELETE', + // TODO(epurkhiser): Can be removed once these APIs only take detectors + query: {useDetectorId: 1}, + } + ); + clearIndicators(); + } catch (_err) { + addErrorMessage(t('Error deleting rule')); + } +} diff --git a/static/app/components/events/interfaces/uptime/uptimeDataSection.spec.tsx b/static/app/components/events/interfaces/uptime/uptimeDataSection.spec.tsx index 5ccb11646274bb..45d0c8d78f3fc4 100644 --- a/static/app/components/events/interfaces/uptime/uptimeDataSection.spec.tsx +++ b/static/app/components/events/interfaces/uptime/uptimeDataSection.spec.tsx @@ -39,12 +39,9 @@ describe('Uptime Data Section', () => { }); const event = EventFixture({ - tags: [ - { - key: 'uptime_rule', - value: '1234', - }, - ], + occurrence: { + evidenceData: {detectorId: 1234}, + }, }); render(); @@ -94,12 +91,9 @@ describe('Uptime Data Section', () => { }); const event = EventFixture({ - tags: [ - { - key: 'uptime_rule', - value: '1234', - }, - ], + occurrence: { + evidenceData: {detectorId: 1234}, + }, }); render(); diff --git a/static/app/components/events/interfaces/uptime/uptimeDataSection.tsx b/static/app/components/events/interfaces/uptime/uptimeDataSection.tsx index 89ff9abb3d4e13..e3a0ce6b94b2bf 100644 --- a/static/app/components/events/interfaces/uptime/uptimeDataSection.tsx +++ b/static/app/components/events/interfaces/uptime/uptimeDataSection.tsx @@ -105,7 +105,7 @@ export function UptimeDataSection({group, event, project}: Props) { const now = useMemo(() => new Date(), []); const isResolved = group.status === GroupStatus.RESOLVED; - const alertRuleId = event.tags.find(tag => tag.key === 'uptime_rule')?.value; + const detectorId: number | undefined = event.occurrence?.evidenceData.detectorId; const elementRef = useRef(null); const {width: containerWidth} = useDimensions({elementRef}); @@ -122,19 +122,19 @@ export function UptimeDataSection({group, event, project}: Props) { }, [timeWindow, timelineWidth, since, until, event, now]); const {data: uptimeStats, isPending} = useUptimeMonitorStats({ - ruleIds: alertRuleId ? [alertRuleId] : [], + detectorIds: detectorId ? [String(detectorId)] : [], timeWindowConfig, }); - const bucketedData = alertRuleId ? (uptimeStats?.[alertRuleId] ?? []) : []; + const bucketedData = detectorId ? (uptimeStats?.[detectorId] ?? []) : []; const actions = ( - {defined(alertRuleId) && ( + {defined(detectorId) && ( } size="xs" to={makeAlertsPathname({ - path: `/rules/uptime/${project.slug}/${alertRuleId}/details/`, + path: `/rules/uptime/${project.slug}/${detectorId}/details/`, organization, })} > diff --git a/static/app/routes.tsx b/static/app/routes.tsx index 3cf545a3ac74f6..5f56cb0886182d 100644 --- a/static/app/routes.tsx +++ b/static/app/routes.tsx @@ -1545,7 +1545,7 @@ function buildRoutes(): RouteObject[] { deprecatedRouteProps: true, children: [ { - path: ':projectId/:uptimeRuleId/details/', + path: ':projectId/:detectorId/details/', component: make(() => import('sentry/views/alerts/rules/uptime/details')), deprecatedRouteProps: true, }, diff --git a/static/app/views/alerts/list/rules/alertRulesList.spec.tsx b/static/app/views/alerts/list/rules/alertRulesList.spec.tsx index 18b021bbc7689c..11c80c6ccebaf8 100644 --- a/static/app/views/alerts/list/rules/alertRulesList.spec.tsx +++ b/static/app/views/alerts/list/rules/alertRulesList.spec.tsx @@ -624,7 +624,7 @@ describe('AlertRulesList', () => { renderGlobalModal(); const deleteMock = MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/uptime/${uptimeRule.id}/`, + url: `/projects/${organization.slug}/${project.slug}/uptime/${uptimeRule.detectorId}/?useDetectorId=1`, method: 'DELETE', body: {}, }); diff --git a/static/app/views/alerts/list/rules/alertRulesList.tsx b/static/app/views/alerts/list/rules/alertRulesList.tsx index cd311f835d4260..6f4bf0770b77ea 100644 --- a/static/app/views/alerts/list/rules/alertRulesList.tsx +++ b/static/app/views/alerts/list/rules/alertRulesList.tsx @@ -149,11 +149,14 @@ function AlertRulesList() { }; const handleDeleteRule = async (projectId: string, rule: CombinedAlerts) => { + // TODO(epurkhiser): To be removed when uptime rules use detector ID as the `id` + const id = rule.type === CombinedAlertType.UPTIME ? rule.detectorId : rule.id; + const deleteEndpoints: Record = { - [CombinedAlertType.ISSUE]: `/projects/${organization.slug}/${projectId}/rules/${rule.id}/`, - [CombinedAlertType.METRIC]: `/organizations/${organization.slug}/alert-rules/${rule.id}/`, - [CombinedAlertType.UPTIME]: `/projects/${organization.slug}/${projectId}/uptime/${rule.id}/`, - [CombinedAlertType.CRONS]: `/projects/${organization.slug}/${projectId}/monitors/${rule.id}/`, + [CombinedAlertType.ISSUE]: `/projects/${organization.slug}/${projectId}/rules/${id}/`, + [CombinedAlertType.METRIC]: `/organizations/${organization.slug}/alert-rules/${id}/`, + [CombinedAlertType.UPTIME]: `/projects/${organization.slug}/${projectId}/uptime/${id}/?useDetectorId=1`, + [CombinedAlertType.CRONS]: `/projects/${organization.slug}/${projectId}/monitors/${id}/`, }; try { @@ -161,7 +164,12 @@ function AlertRulesList() { setApiQueryData>( queryClient, getAlertListQueryKey(organization.slug, location.query), - data => data?.filter(r => r?.id !== rule.id && r?.type !== rule.type) + data => + data?.filter(r => { + // TODO(epurkhiser): To be removed when uptime rules use detector ID as the `id` + const rId = r?.type === CombinedAlertType.UPTIME ? r.detectorId : r?.id; + return rId !== id && r?.type !== rule.type; + }) ); refetch(); addSuccessMessage(t('Deleted rule')); @@ -283,7 +291,7 @@ function AlertRulesList() { return ( ; + // TODO(epurkhiser): To be removed when uptime rules use detector ID as the `id` + const ruleId = rule.type === CombinedAlertType.UPTIME ? rule.detectorId : rule.id; + const editLink = makeAlertsPathname({ - path: `/${editKey[rule.type]}/${slug}/${rule.id}/`, + path: `/${editKey[rule.type]}/${slug}/${ruleId}/`, organization, }); @@ -111,7 +114,7 @@ function RuleListRow({ }), query: { project: slug, - duplicateRuleId: rule.id, + duplicateRuleId: ruleId, createFromDuplicate: 'true', referrer: 'alert_stream', }, @@ -260,7 +263,7 @@ function RuleListRow({ }); case CombinedAlertType.UPTIME: return makeAlertsPathname({ - path: `/rules/uptime/${rule.projectSlug}/${rule.id}/details/`, + path: `/rules/uptime/${rule.projectSlug}/${rule.detectorId}/details/`, organization, }); default: diff --git a/static/app/views/alerts/rules/uptime/details.spec.tsx b/static/app/views/alerts/rules/uptime/details.spec.tsx index 2a74030c5909c5..7c67f30349accd 100644 --- a/static/app/views/alerts/rules/uptime/details.spec.tsx +++ b/static/app/views/alerts/rules/uptime/details.spec.tsx @@ -19,11 +19,12 @@ describe('UptimeAlertDetails', () => { body: [], }); MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/issues/?limit=1&project=${project.id}&query=issue.type%3Auptime_domain_failure%20tags%5Buptime_rule%5D%3A1`, + url: `/organizations/org-slug/issues/?limit=1&project=2&query=issue.type%3Auptime_domain_failure%20title%3A%22Downtime%20detected%20for%20https%3A%2F%2Fsentry.io%2F%22`, body: [], }); MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/${project.slug}/uptime/1/checks/`, + query: {useDetectorId: '1'}, body: [], }); MockApiClient.addMockResponse({ @@ -37,13 +38,14 @@ describe('UptimeAlertDetails', () => { it('renders', async () => { MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/${project.slug}/uptime/1/`, + query: {useDetectorId: '1'}, body: UptimeRuleFixture({name: 'Uptime Test Rule'}), }); render( , {organization} ); @@ -53,13 +55,14 @@ describe('UptimeAlertDetails', () => { it('shows a message for invalid uptime alert', async () => { MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/${project.slug}/uptime/2/`, + query: {useDetectorId: '1'}, statusCode: 404, }); render( , {organization} ); @@ -71,17 +74,19 @@ describe('UptimeAlertDetails', () => { it('disables and enables the rule', async () => { MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/${project.slug}/uptime/2/`, + query: {useDetectorId: '1'}, statusCode: 404, }); MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/${project.slug}/uptime/1/`, + query: {useDetectorId: '1'}, body: UptimeRuleFixture({name: 'Uptime Test Rule'}), }); render( , {organization} ); @@ -89,6 +94,7 @@ describe('UptimeAlertDetails', () => { const disableMock = MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/${project.slug}/uptime/1/`, + query: {useDetectorId: '1'}, method: 'PUT', body: UptimeRuleFixture({name: 'Uptime Test Rule', status: 'disabled'}), }); @@ -105,6 +111,7 @@ describe('UptimeAlertDetails', () => { const enableMock = MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/${project.slug}/uptime/1/`, + query: {useDetectorId: '1'}, method: 'PUT', body: UptimeRuleFixture({name: 'Uptime Test Rule', status: 'active'}), }); diff --git a/static/app/views/alerts/rules/uptime/details.tsx b/static/app/views/alerts/rules/uptime/details.tsx index c9cadba8b62e45..d2ec790f95a03f 100644 --- a/static/app/views/alerts/rules/uptime/details.tsx +++ b/static/app/views/alerts/rules/uptime/details.tsx @@ -38,14 +38,14 @@ import {UptimeChecksTable} from './uptimeChecksTable'; import {UptimeIssues} from './uptimeIssues'; interface UptimeAlertDetailsProps - extends RouteComponentProps<{projectId: string; uptimeRuleId: string}> {} + extends RouteComponentProps<{detectorId: string; projectId: string}> {} export default function UptimeAlertDetails({params}: UptimeAlertDetailsProps) { const api = useApi(); const organization = useOrganization(); const queryClient = useQueryClient(); - const {projectId, uptimeRuleId} = params; + const {projectId, detectorId} = params; const {projects, fetching: loadingProject} = useProjects({slugs: [projectId]}); const project = projects.find(({slug}) => slug === projectId); @@ -54,10 +54,10 @@ export default function UptimeAlertDetails({params}: UptimeAlertDetailsProps) { data: uptimeRule, isPending, isError, - } = useUptimeRule({projectSlug: projectId, uptimeRuleId}); + } = useUptimeRule({projectSlug: projectId, detectorId}); - const {data: uptimeSummaries} = useUptimeMonitorSummaries({ruleIds: [uptimeRuleId]}); - const summary = uptimeSummaries?.[uptimeRuleId]; + const {data: uptimeSummaries} = useUptimeMonitorSummaries({detectorIds: [detectorId]}); + const summary = uptimeSummaries?.[detectorId]; // Only display the missed window legend when there are visible missed window // check-ins in the timeline @@ -95,7 +95,7 @@ export default function UptimeAlertDetails({params}: UptimeAlertDetailsProps) { } const handleUpdate = async (data: Partial) => { - const resp = await updateUptimeRule(api, organization.slug, uptimeRule, data); + const resp = await updateUptimeRule(api, organization, uptimeRule, data); if (resp !== null) { setUptimeRuleData({ @@ -157,7 +157,7 @@ export default function UptimeAlertDetails({params}: UptimeAlertDetailsProps) { disabled={!canEdit} title={canEdit ? undefined : permissionTooltipText} to={makeAlertsPathname({ - path: `/uptime-rules/${project.slug}/${uptimeRuleId}/`, + path: `/uptime-rules/${project.slug}/${detectorId}/`, organization, })} > @@ -190,7 +190,7 @@ export default function UptimeAlertDetails({params}: UptimeAlertDetailsProps) { )} - + diff --git a/static/app/views/alerts/rules/uptime/detailsTimeline.tsx b/static/app/views/alerts/rules/uptime/detailsTimeline.tsx index 2c610ff97e6a6a..e24050729061ea 100644 --- a/static/app/views/alerts/rules/uptime/detailsTimeline.tsx +++ b/static/app/views/alerts/rules/uptime/detailsTimeline.tsx @@ -23,6 +23,7 @@ interface Props { } export function DetailsTimeline({uptimeRule, onStatsLoaded}: Props) { + const {detectorId} = uptimeRule; const elementRef = useRef(null); const {width: containerWidth} = useDimensions({elementRef}); const timelineWidth = useDebouncedValue(containerWidth, 500); @@ -30,13 +31,13 @@ export function DetailsTimeline({uptimeRule, onStatsLoaded}: Props) { const timeWindowConfig = useTimeWindowConfig({timelineWidth}); const {data: uptimeStats} = useUptimeMonitorStats({ - ruleIds: [uptimeRule.id], + detectorIds: [uptimeRule.detectorId], timeWindowConfig, }); useEffect( - () => uptimeStats?.[uptimeRule.id] && onStatsLoaded?.(uptimeStats[uptimeRule.id]!), - [onStatsLoaded, uptimeStats, uptimeRule.id] + () => uptimeStats?.[detectorId] && onStatsLoaded?.(uptimeStats[detectorId]), + [onStatsLoaded, uptimeStats, detectorId] ); return ( diff --git a/static/app/views/alerts/rules/uptime/edit.spec.tsx b/static/app/views/alerts/rules/uptime/edit.spec.tsx index 3aaaa6166fec91..631e3b536267b9 100644 --- a/static/app/views/alerts/rules/uptime/edit.spec.tsx +++ b/static/app/views/alerts/rules/uptime/edit.spec.tsx @@ -37,7 +37,7 @@ describe('uptime/edit', () => { const handleChangeTitle = jest.fn(); MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/uptime/${uptimeRule.id}/`, + url: `/projects/${organization.slug}/${project.slug}/uptime/${uptimeRule.detectorId}/`, method: 'GET', body: uptimeRule, }); @@ -49,7 +49,7 @@ describe('uptime/edit', () => { userTeamIds={[]} organization={organization} project={project} - params={{projectId: project.slug, ruleId: uptimeRule.id}} + params={{projectId: project.slug, ruleId: uptimeRule.detectorId}} />, {organization} ); @@ -74,7 +74,7 @@ describe('uptime/edit', () => { const handleChangeTitle = jest.fn(); MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/uptime/${uptimeRule.id}/`, + url: `/projects/${organization.slug}/${project.slug}/uptime/${uptimeRule.detectorId}/`, method: 'GET', body: uptimeRule, }); @@ -86,14 +86,14 @@ describe('uptime/edit', () => { userTeamIds={[]} organization={organization} project={project} - params={{projectId: project.slug, ruleId: uptimeRule.id}} + params={{projectId: project.slug, ruleId: uptimeRule.detectorId}} />, {organization} ); await screen.findByText('Configure Request'); const deleteRule = MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/uptime/${uptimeRule.id}/`, + url: `/projects/${organization.slug}/${project.slug}/uptime/${uptimeRule.detectorId}/`, method: 'DELETE', }); diff --git a/static/app/views/alerts/rules/uptime/edit.tsx b/static/app/views/alerts/rules/uptime/edit.tsx index 3730f5fde547c7..88e77e0ac69143 100644 --- a/static/app/views/alerts/rules/uptime/edit.tsx +++ b/static/app/views/alerts/rules/uptime/edit.tsx @@ -1,7 +1,7 @@ import {useEffect} from 'react'; import styled from '@emotion/styled'; -import {addErrorMessage} from 'sentry/actionCreators/indicator'; +import {deleteUptimeRule} from 'sentry/actionCreators/uptime'; import {Alert} from 'sentry/components/core/alert'; import * as Layout from 'sentry/components/layouts/thirds'; import LoadingError from 'sentry/components/loadingError'; @@ -10,12 +10,11 @@ import {t} from 'sentry/locale'; import type {RouteComponentProps} from 'sentry/types/legacyReactRouter'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; -import {useApiQuery} from 'sentry/utils/queryClient'; import useApi from 'sentry/utils/useApi'; import {useNavigate} from 'sentry/utils/useNavigate'; import {makeAlertsPathname} from 'sentry/views/alerts/pathnames'; import {UptimeAlertForm} from 'sentry/views/alerts/rules/uptime/uptimeAlertForm'; -import type {UptimeAlert} from 'sentry/views/alerts/types'; +import {useUptimeRule} from 'sentry/views/insights/uptime/utils/useUptimeRule'; type RouteParams = { projectId: string; @@ -33,18 +32,13 @@ export function UptimeRulesEdit({params, onChangeTitle, organization, project}: const api = useApi(); const navigate = useNavigate(); - const apiUrl = `/projects/${organization.slug}/${params.projectId}/uptime/${params.ruleId}/`; - const { isPending, isSuccess, isError, data: rule, error, - } = useApiQuery([apiUrl], { - staleTime: 0, - retry: false, - }); + } = useUptimeRule({projectSlug: params.projectId, detectorId: params.ruleId}); useEffect(() => { if (isSuccess && rule) { @@ -69,17 +63,8 @@ export function UptimeRulesEdit({params, onChangeTitle, organization, project}: } const handleDelete = async () => { - try { - await api.requestPromise(apiUrl, {method: 'DELETE'}); - navigate( - makeAlertsPathname({ - path: `/rules/`, - organization, - }) - ); - } catch (_err) { - addErrorMessage(t('Error deleting rule')); - } + await deleteUptimeRule(api, organization, rule); + navigate(makeAlertsPathname({path: `/rules/`, organization})); }; return ( diff --git a/static/app/views/alerts/rules/uptime/existingOrCreate.spec.tsx b/static/app/views/alerts/rules/uptime/existingOrCreate.spec.tsx index 87217c57d8253d..735125fcacc9ac 100644 --- a/static/app/views/alerts/rules/uptime/existingOrCreate.spec.tsx +++ b/static/app/views/alerts/rules/uptime/existingOrCreate.spec.tsx @@ -34,7 +34,7 @@ describe('ExistingOrCreate', () => { it('redirects to the list when multiple eixst', async () => { MockApiClient.addMockResponse({ url: '/organizations/org-slug/combined-rules/', - body: [UptimeRuleFixture({id: '1'}), UptimeRuleFixture({id: '2'})], + body: [UptimeRuleFixture({detectorId: '1'}), UptimeRuleFixture({detectorId: '2'})], }); const {router} = render(); diff --git a/static/app/views/alerts/rules/uptime/existingOrCreate.tsx b/static/app/views/alerts/rules/uptime/existingOrCreate.tsx index d6a8637e90852a..ecb39827fefd0a 100644 --- a/static/app/views/alerts/rules/uptime/existingOrCreate.tsx +++ b/static/app/views/alerts/rules/uptime/existingOrCreate.tsx @@ -34,7 +34,7 @@ export default function ExistingOrCreate() { // Has one single alert rule if (existingRules.length === 1) { const url = makeAlertsPathname({ - path: `/uptime-rules/${existingRules[0]?.projectSlug}/${existingRules[0]?.id}/`, + path: `/uptime-rules/${existingRules[0]?.projectSlug}/${existingRules[0]?.detectorId}/`, organization, }); navigate(url, {replace: true}); diff --git a/static/app/views/alerts/rules/uptime/types.tsx b/static/app/views/alerts/rules/uptime/types.tsx index 71989ba46c184e..4c267a70f13f1c 100644 --- a/static/app/views/alerts/rules/uptime/types.tsx +++ b/static/app/views/alerts/rules/uptime/types.tsx @@ -13,9 +13,11 @@ export enum UptimeMonitorMode { export interface UptimeRule { body: string | null; + // TODO(epurkhiser): In the future this will change to id once the current id + // field is no longer representing the ProjectUptimeSubscription ID + detectorId: string; environment: string | null; headers: Array<[key: string, value: string]>; - id: string; intervalSeconds: number; method: string; mode: UptimeMonitorMode; diff --git a/static/app/views/alerts/rules/uptime/uptimeAlertForm.spec.tsx b/static/app/views/alerts/rules/uptime/uptimeAlertForm.spec.tsx index 69814e73eeda21..d2c804c720de06 100644 --- a/static/app/views/alerts/rules/uptime/uptimeAlertForm.spec.tsx +++ b/static/app/views/alerts/rules/uptime/uptimeAlertForm.spec.tsx @@ -66,7 +66,7 @@ describe('Uptime Alert Form', () => { await selectEvent.select(input('Owner'), 'Foo Bar'); const updateMock = MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/uptime/`, + url: `/projects/${organization.slug}/${project.slug}/uptime/?useDetectorId=1`, method: 'POST', }); @@ -150,7 +150,7 @@ describe('Uptime Alert Form', () => { await userEvent.type(input('URL'), '/test'); const updateMock = MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/uptime/${rule.id}/`, + url: `/projects/${organization.slug}/${project.slug}/uptime/${rule.detectorId}/?useDetectorId=1`, method: 'PUT', }); @@ -212,7 +212,7 @@ describe('Uptime Alert Form', () => { await selectEvent.select(input('Owner'), 'Foo Bar'); const updateMock = MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/uptime/${rule.id}/`, + url: `/projects/${organization.slug}/${project.slug}/uptime/${rule.detectorId}/?useDetectorId=1`, method: 'PUT', }); @@ -329,7 +329,7 @@ describe('Uptime Alert Form', () => { await userEvent.type(name, 'New Uptime Rule'); const updateMock = MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/uptime/`, + url: `/projects/${organization.slug}/${project.slug}/uptime/?useDetectorId=1`, method: 'POST', }); diff --git a/static/app/views/alerts/rules/uptime/uptimeAlertForm.tsx b/static/app/views/alerts/rules/uptime/uptimeAlertForm.tsx index 443f0f0a0f2d2a..5ba5a0799b4a80 100644 --- a/static/app/views/alerts/rules/uptime/uptimeAlertForm.tsx +++ b/static/app/views/alerts/rules/uptime/uptimeAlertForm.tsx @@ -105,13 +105,13 @@ export function UptimeAlertForm({project, handleDelete, rule}: Props) { const projectSlug = formModel.getValue('projectSlug'); const selectedProject = projects.find(p => p.slug === projectSlug); const apiEndpoint = rule - ? `/projects/${organization.slug}/${projectSlug}/uptime/${rule.id}/` - : `/projects/${organization.slug}/${projectSlug}/uptime/`; + ? `/projects/${organization.slug}/${projectSlug}/uptime/${rule.detectorId}/?useDetectorId=1` + : `/projects/${organization.slug}/${projectSlug}/uptime/?useDetectorId=1`; function onSubmitSuccess(response: any) { navigate( makeAlertsPathname({ - path: `/rules/uptime/${projectSlug}/${response.id}/details/`, + path: `/rules/uptime/${projectSlug}/${response.detectorId}/details/`, organization, }) ); diff --git a/static/app/views/alerts/rules/uptime/uptimeChecksTable.tsx b/static/app/views/alerts/rules/uptime/uptimeChecksTable.tsx index 163542beee24f7..4499ed144e77e6 100644 --- a/static/app/views/alerts/rules/uptime/uptimeChecksTable.tsx +++ b/static/app/views/alerts/rules/uptime/uptimeChecksTable.tsx @@ -36,7 +36,7 @@ export function UptimeChecksTable({uptimeRule}: UptimeChecksTableProps) { } = useUptimeChecks({ orgSlug: organization.slug, projectSlug: uptimeRule.projectSlug, - uptimeAlertId: uptimeRule.id, + detectorId: uptimeRule.detectorId, cursor: decodeScalar(location.query.cursor), ...timeRange, limit: 10, diff --git a/static/app/views/alerts/rules/uptime/uptimeIssues.tsx b/static/app/views/alerts/rules/uptime/uptimeIssues.tsx index 575eb22a05dfed..97063c31c51a45 100644 --- a/static/app/views/alerts/rules/uptime/uptimeIssues.tsx +++ b/static/app/views/alerts/rules/uptime/uptimeIssues.tsx @@ -6,14 +6,18 @@ import {t} from 'sentry/locale'; import {IssueType} from 'sentry/types/group'; import type {Project} from 'sentry/types/project'; +import type {UptimeRule} from './types'; + interface Props { project: Project; - ruleId: string; + uptimeRule: UptimeRule; } -export function UptimeIssues({project, ruleId}: Props) { - // TODO(davidenwang): Replace this with an actual query for the specific uptime alert rule - const query = `issue.type:${IssueType.UPTIME_DOMAIN_FAILURE} tags[uptime_rule]:${ruleId}`; +export function UptimeIssues({project, uptimeRule}: Props) { + // TODO(epurkhiser): We need a better way to query for uptime issues, using + // the title is brittle and means when the user changes the URL we'll have to + // wait for a new event before the issue matches again. + const query = `issue.type:${IssueType.UPTIME_DOMAIN_FAILURE} title:"Downtime detected for ${uptimeRule.url}"`; const emptyMessage = () => { return ( diff --git a/static/app/views/insights/uptime/components/overviewTimeline/index.tsx b/static/app/views/insights/uptime/components/overviewTimeline/index.tsx index 39743763686099..ff44b8a6662f90 100644 --- a/static/app/views/insights/uptime/components/overviewTimeline/index.tsx +++ b/static/app/views/insights/uptime/components/overviewTimeline/index.tsx @@ -33,7 +33,7 @@ export function OverviewTimeline({uptimeRules}: Props) { const {data: summaries} = useUptimeMonitorSummaries({ start: timeWindowConfig.start, end: timeWindowConfig.end, - ruleIds: uptimeRules.map(rule => rule.id), + detectorIds: uptimeRules.map(rule => rule.detectorId), }); return ( @@ -70,10 +70,10 @@ export function OverviewTimeline({uptimeRules}: Props) { {uptimeRules.map(uptimeRule => ( ))} diff --git a/static/app/views/insights/uptime/components/overviewTimeline/overviewRow.tsx b/static/app/views/insights/uptime/components/overviewTimeline/overviewRow.tsx index e49c0d70cc1572..9465dbddb79e2b 100644 --- a/static/app/views/insights/uptime/components/overviewTimeline/overviewRow.tsx +++ b/static/app/views/insights/uptime/components/overviewTimeline/overviewRow.tsx @@ -58,12 +58,12 @@ export function OverviewRow({ const query = pick(location.query, ['start', 'end', 'statsPeriod', 'environment']); const {data: uptimeStats, isPending} = useUptimeMonitorStats({ - ruleIds: [uptimeRule.id], + detectorIds: [uptimeRule.detectorId], timeWindowConfig, }); const detailsPath = makeAlertsPathname({ - path: `/rules/uptime/${uptimeRule.projectSlug}/${uptimeRule.id}/details/`, + path: `/rules/uptime/${uptimeRule.projectSlug}/${uptimeRule.detectorId}/details/`, organization, }); @@ -109,7 +109,7 @@ export function OverviewRow({ return ( @@ -119,7 +119,7 @@ export function OverviewRow({ ) : ( = {}; if (start) { @@ -39,7 +39,7 @@ export function useUptimeMonitorSummaries({ruleIds, start, end}: Options) { monitorStatsQueryKey, { query: { - projectUptimeSubscriptionId: ruleIds, + uptimeDetectorId: detectorIds, ...selectionQuery, }, }, diff --git a/static/app/views/insights/uptime/utils/useUptimeRule.tsx b/static/app/views/insights/uptime/utils/useUptimeRule.tsx index 1ef0ca76bfaa8d..ecd16770bf2a0d 100644 --- a/static/app/views/insights/uptime/utils/useUptimeRule.tsx +++ b/static/app/views/insights/uptime/utils/useUptimeRule.tsx @@ -9,18 +9,22 @@ import useOrganization from 'sentry/utils/useOrganization'; import type {UptimeRule} from 'sentry/views/alerts/rules/uptime/types'; interface UseUptimeRuleOptions { + detectorId: string; projectSlug: string; - uptimeRuleId: string; } export function useUptimeRule( - {projectSlug, uptimeRuleId}: UseUptimeRuleOptions, + {projectSlug, detectorId}: UseUptimeRuleOptions, options: Partial> = {} ) { const organization = useOrganization(); const queryKey: ApiQueryKey = [ - `/projects/${organization.slug}/${projectSlug}/uptime/${uptimeRuleId}/`, + `/projects/${organization.slug}/${projectSlug}/uptime/${detectorId}/`, + { + // TODO(epurkhiser): Can be removed once these APIs only take detectors + query: {useDetectorId: 1}, + }, ]; return useApiQuery(queryKey, {staleTime: 0, ...options}); } @@ -39,7 +43,11 @@ export function setUptimeRuleData({ uptimeRule, }: SetUptimeRuleDataOptions) { const queryKey: ApiQueryKey = [ - `/projects/${organizationSlug}/${projectSlug}/uptime/${uptimeRule.id}/`, + `/projects/${organizationSlug}/${projectSlug}/uptime/${uptimeRule.detectorId}/`, + { + // TODO(epurkhiser): Can be removed once these APIs only take detectors + query: {useDetectorId: 1}, + }, ]; setApiQueryData(queryClient, queryKey, uptimeRule); } diff --git a/static/app/views/insights/uptime/views/overview.spec.tsx b/static/app/views/insights/uptime/views/overview.spec.tsx index f8a202d2fa54e3..21f64d02647a01 100644 --- a/static/app/views/insights/uptime/views/overview.spec.tsx +++ b/static/app/views/insights/uptime/views/overview.spec.tsx @@ -26,7 +26,7 @@ describe('Uptime Overview', () => { url: '/organizations/org-slug/uptime/', body: [ UptimeRuleFixture({ - id: '123', + detectorId: '123', name: 'Test Monitor', projectSlug: project.slug, owner: undefined, diff --git a/static/app/views/issueDetails/groupUptimeChecks.spec.tsx b/static/app/views/issueDetails/groupUptimeChecks.spec.tsx index b4ae087efa40d0..b3ca5e98d730d5 100644 --- a/static/app/views/issueDetails/groupUptimeChecks.spec.tsx +++ b/static/app/views/issueDetails/groupUptimeChecks.spec.tsx @@ -17,14 +17,11 @@ import {statusToText} from 'sentry/views/insights/uptime/timelineConfig'; import GroupUptimeChecks from 'sentry/views/issueDetails/groupUptimeChecks'; describe('GroupUptimeChecks', () => { - const uptimeRuleId = '123'; + const detectorId = '123'; const event = EventFixture({ - tags: [ - { - key: 'uptime_rule', - value: uptimeRuleId, - }, - ], + occurrence: { + evidenceData: {detectorId}, + }, }); const group = GroupFixture({ issueCategory: IssueCategory.UPTIME, @@ -66,7 +63,7 @@ describe('GroupUptimeChecks', () => { it('renders the empty uptime check table', async () => { MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/uptime/${uptimeRuleId}/checks/`, + url: `/projects/${organization.slug}/${project.slug}/uptime/${detectorId}/checks/`, body: [], }); @@ -85,7 +82,7 @@ describe('GroupUptimeChecks', () => { it('renders the uptime check table with data', async () => { const uptimeCheck = UptimeCheckFixture(); MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/uptime/${uptimeRuleId}/checks/`, + url: `/projects/${organization.slug}/${project.slug}/uptime/${detectorId}/checks/`, body: [uptimeCheck], }); MockApiClient.addMockResponse({ @@ -121,7 +118,7 @@ describe('GroupUptimeChecks', () => { it('indicates when there are spans in a trace', async () => { const uptimeCheck = UptimeCheckFixture(); MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/uptime/${uptimeRuleId}/checks/`, + url: `/projects/${organization.slug}/${project.slug}/uptime/${detectorId}/checks/`, body: [uptimeCheck], }); MockApiClient.addMockResponse({ diff --git a/static/app/views/issueDetails/groupUptimeChecks.tsx b/static/app/views/issueDetails/groupUptimeChecks.tsx index 45da337355e426..ed0e6bc2db1fe8 100644 --- a/static/app/views/issueDetails/groupUptimeChecks.tsx +++ b/static/app/views/issueDetails/groupUptimeChecks.tsx @@ -11,7 +11,7 @@ import {UptimeChecksGrid} from 'sentry/views/alerts/rules/uptime/uptimeChecksGri import {useUptimeChecks} from 'sentry/views/insights/uptime/utils/useUptimeChecks'; import {useUptimeRule} from 'sentry/views/insights/uptime/utils/useUptimeRule'; import {EventListTable} from 'sentry/views/issueDetails/streamline/eventListTable'; -import {useUptimeIssueAlertId} from 'sentry/views/issueDetails/streamline/issueUptimeCheckTimeline'; +import {useUptimeIssueDetectorId} from 'sentry/views/issueDetails/streamline/issueUptimeCheckTimeline'; import {useGroup} from 'sentry/views/issueDetails/useGroup'; export default function GroupUptimeChecks() { @@ -19,7 +19,7 @@ export default function GroupUptimeChecks() { const {groupId} = useParams<{groupId: string}>(); const location = useLocation(); const {since, until} = usePageFilterDates(); - const uptimeAlertId = useUptimeIssueAlertId({groupId}); + const detectorId = useUptimeIssueDetectorId({groupId}); const { data: group, @@ -29,12 +29,12 @@ export default function GroupUptimeChecks() { } = useGroup({groupId}); const canFetchUptimeChecks = - Boolean(organization.slug) && Boolean(group?.project.slug) && Boolean(uptimeAlertId); + Boolean(organization.slug) && Boolean(group?.project.slug) && Boolean(detectorId); const {data: uptimeRule} = useUptimeRule( { projectSlug: group?.project.slug ?? '', - uptimeRuleId: uptimeAlertId ?? '', + detectorId: detectorId ?? '', }, {enabled: canFetchUptimeChecks} ); @@ -43,7 +43,7 @@ export default function GroupUptimeChecks() { { orgSlug: organization.slug, projectSlug: group?.project.slug ?? '', - uptimeAlertId: uptimeAlertId ?? '', + detectorId: detectorId ?? '', cursor: decodeScalar(location.query.cursor), limit: 50, start: since.toISOString(), diff --git a/static/app/views/issueDetails/streamline/eventDetailsHeader.spec.tsx b/static/app/views/issueDetails/streamline/eventDetailsHeader.spec.tsx index 67be662c32386f..53951794a1fa17 100644 --- a/static/app/views/issueDetails/streamline/eventDetailsHeader.spec.tsx +++ b/static/app/views/issueDetails/streamline/eventDetailsHeader.spec.tsx @@ -33,7 +33,11 @@ describe('EventDetailsHeader', () => { // first seen 19 days ago firstSeen: new Date(Date.now() - 19 * 24 * 60 * 60 * 1000).toISOString(), }); - const event = EventFixture({id: 'event-id'}); + const event = EventFixture({ + id: 'event-id', + occurrence: {evidenceData: {}}, + }); + const defaultProps = {group, event, project}; const router = RouterFixture(); @@ -167,6 +171,7 @@ describe('EventDetailsHeader', () => { })} event={EventFixture({ occurrence: { + evidenceData: {}, evidenceDisplay: [ {name: 'Status Code', value: '500'}, {name: 'Failure reason', value: 'bad things'}, diff --git a/static/app/views/issueDetails/streamline/issueUptimeCheckTimeline.spec.tsx b/static/app/views/issueDetails/streamline/issueUptimeCheckTimeline.spec.tsx index ff16c94476cd19..8f1966e6195ae0 100644 --- a/static/app/views/issueDetails/streamline/issueUptimeCheckTimeline.spec.tsx +++ b/static/app/views/issueDetails/streamline/issueUptimeCheckTimeline.spec.tsx @@ -29,7 +29,7 @@ jest ); describe('IssueUptimeCheckTimeline', () => { - const uptimeRuleId = '123'; + const detectorId = '123'; const organization = OrganizationFixture(); const project = ProjectFixture({ environments: ['production'], @@ -39,12 +39,9 @@ describe('IssueUptimeCheckTimeline', () => { issueType: IssueType.UPTIME_DOMAIN_FAILURE, }); const event = EventFixture({ - tags: [ - { - key: 'uptime_rule', - value: uptimeRuleId, - }, - ], + occurrence: { + evidenceData: {detectorId}, + }, }); beforeEach(() => { @@ -67,10 +64,10 @@ describe('IssueUptimeCheckTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/uptime-stats/`, query: { - projectUptimeSubscriptionId: [uptimeRuleId], + uptimeDetectorId: [detectorId], }, body: { - [uptimeRuleId]: [ + [detectorId]: [ [ new Date('2025-01-01T11:00:00Z').getTime() / 1000, { @@ -121,10 +118,10 @@ describe('IssueUptimeCheckTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/uptime-stats/`, query: { - projectUptimeSubscriptionId: [uptimeRuleId], + uptimeDetectorId: [detectorId], }, body: { - [uptimeRuleId]: [ + [detectorId]: [ [ startTime.getTime() / 1000, { diff --git a/static/app/views/issueDetails/streamline/issueUptimeCheckTimeline.tsx b/static/app/views/issueDetails/streamline/issueUptimeCheckTimeline.tsx index 457bfb4f0d9731..b8ba3e89203b3f 100644 --- a/static/app/views/issueDetails/streamline/issueUptimeCheckTimeline.tsx +++ b/static/app/views/issueDetails/streamline/issueUptimeCheckTimeline.tsx @@ -28,7 +28,11 @@ import {useIssueDetails} from 'sentry/views/issueDetails/streamline/context'; import {useIssueTimeWindowConfig} from 'sentry/views/issueDetails/streamline/useIssueTimeWindowConfig'; import {getGroupEventQueryKey} from 'sentry/views/issueDetails/utils'; -export function useUptimeIssueAlertId({groupId}: {groupId: string}): string | undefined { +export function useUptimeIssueDetectorId({ + groupId, +}: { + groupId: string; +}): string | undefined { /** * This should be removed once the uptime rule value is set on the issue. * This will fetch an event from the max range if the detector details @@ -55,28 +59,30 @@ export function useUptimeIssueAlertId({groupId}: {groupId: string}): string | un } ); + const evidenceDetectorId = event?.occurrence?.evidenceData.detectorId + ? String(event?.occurrence?.evidenceData.detectorId) + : undefined; + // Fall back to the fetched event since the legacy UI isn't nested within the provider the provider - return hasUptimeDetector - ? detectorId - : event?.tags?.find(tag => tag.key === 'uptime_rule')?.value; + return hasUptimeDetector ? detectorId : evidenceDetectorId; } export function IssueUptimeCheckTimeline({group}: {group: Group}) { - const uptimeAlertId = useUptimeIssueAlertId({groupId: group.id}); + const detectorId = useUptimeIssueDetectorId({groupId: group.id}); const elementRef = useRef(null); const {width: containerWidth} = useDimensions({elementRef}); const timelineWidth = useDebouncedValue(containerWidth, 500); const timeWindowConfig = useIssueTimeWindowConfig({timelineWidth, group}); const {data: uptimeStats, isPending} = useUptimeMonitorStats({ - ruleIds: uptimeAlertId ? [uptimeAlertId] : [], + detectorIds: detectorId ? [detectorId] : [], timeWindowConfig, }); const legendStatuses = useMemo(() => { const hasUnknownStatus = - uptimeAlertId && - uptimeStats?.[uptimeAlertId]?.some( + detectorId && + uptimeStats?.[detectorId]?.some( ([_, stats]) => stats[CheckStatus.MISSED_WINDOW] > 0 ); @@ -91,7 +97,7 @@ export function IssueUptimeCheckTimeline({group}: {group: Group}) { } return statuses; - }, [uptimeAlertId, uptimeStats]); + }, [detectorId, uptimeStats]); return ( @@ -117,7 +123,7 @@ export function IssueUptimeCheckTimeline({group}: {group: Group}) { ) : ( { issueType: IssueType.UPTIME_DOMAIN_FAILURE, }); const event = EventFixture({ - tags: [ - { - key: 'uptime_rule', - value: '123', - }, - ], + occurrence: { + evidenceData: {detectorId: 123}, + }, }); render(, {organization}); expect(screen.getByText('Monitor ID')).toBeInTheDocument(); @@ -83,6 +80,7 @@ describe('OccurrenceSummary', () => { }); const event = EventFixture({ occurrence: { + evidenceData: {}, evidenceDisplay: [ { name: 'Environment', @@ -124,6 +122,7 @@ describe('OccurrenceSummary', () => { }); const event = EventFixture({ occurrence: { + evidenceData: {}, evidenceDisplay: [ { name: 'Last successful check-in', diff --git a/static/app/views/issueDetails/streamline/sidebar/detectorSection.spec.tsx b/static/app/views/issueDetails/streamline/sidebar/detectorSection.spec.tsx index 45645e533f5339..18ad11bea2d741 100644 --- a/static/app/views/issueDetails/streamline/sidebar/detectorSection.spec.tsx +++ b/static/app/views/issueDetails/streamline/sidebar/detectorSection.spec.tsx @@ -107,12 +107,9 @@ describe('DetectorSection', () => { it('displays the detector details for an uptime monitor', () => { const event = EventFixture({ - tags: [ - { - key: 'uptime_rule', - value: detectorId, - }, - ], + occurrence: { + evidenceData: {detectorId}, + }, }); const group = GroupFixture({ issueCategory: IssueCategory.UPTIME, diff --git a/static/app/views/issueDetails/streamline/sidebar/detectorSection.tsx b/static/app/views/issueDetails/streamline/sidebar/detectorSection.tsx index 4203a8e7684598..13114f56912546 100644 --- a/static/app/views/issueDetails/streamline/sidebar/detectorSection.tsx +++ b/static/app/views/issueDetails/streamline/sidebar/detectorSection.tsx @@ -69,13 +69,13 @@ export function getDetectorDetails({ }; } - const uptimeAlertRuleId = event?.tags?.find(tag => tag?.key === 'uptime_rule')?.value; - if (uptimeAlertRuleId) { + const detectorId: number | undefined = event.occurrence?.evidenceData.detectorId; + if (detectorId) { return { detectorType: 'uptime_monitor', - detectorId: uptimeAlertRuleId, + detectorId: String(detectorId), detectorPath: makeAlertsPathname({ - path: `/rules/uptime/${project.slug}/${uptimeAlertRuleId}/details/`, + path: `/rules/uptime/${project.slug}/${detectorId}/details/`, organization, }), // TODO(issues): Update this to mention detectors when that language is user-facing diff --git a/tests/js/fixtures/uptimeRule.ts b/tests/js/fixtures/uptimeRule.ts index f3c031ef6c861c..6d96a58e2bf84c 100644 --- a/tests/js/fixtures/uptimeRule.ts +++ b/tests/js/fixtures/uptimeRule.ts @@ -8,7 +8,7 @@ import { export function UptimeRuleFixture(params: Partial = {}): UptimeRule { return { - id: '1', + detectorId: '1', intervalSeconds: 60, mode: UptimeMonitorMode.AUTO_DETECTED_ACTIVE, name: 'Uptime Rule',