diff --git a/static/app/types/workflowEngine/detectors.tsx b/static/app/types/workflowEngine/detectors.tsx
index b20ad1334b1102..60bd996c0ce721 100644
--- a/static/app/types/workflowEngine/detectors.tsx
+++ b/static/app/types/workflowEngine/detectors.tsx
@@ -48,15 +48,13 @@ export interface SnubaQueryDataSource extends BaseDataSource {
type: 'snuba_query_subscription';
}
-interface UptimeSubscriptionDataSource extends BaseDataSource {
+export interface UptimeSubscriptionDataSource extends BaseDataSource {
/**
* See UptimeSubscriptionSerializer
*/
queryObj: {
body: string | null;
headers: Array<[string, string]>;
- hostProviderId: string;
- hostProviderName: string;
intervalSeconds: number;
method: string;
timeoutMs: number;
@@ -133,7 +131,7 @@ export interface MetricDetector extends BaseDetector {
export interface UptimeDetector extends BaseDetector {
readonly config: UptimeDetectorConfig;
- readonly dataSources: UptimeSubscriptionDataSource[];
+ readonly dataSources: [UptimeSubscriptionDataSource];
readonly type: 'uptime_domain_failure';
}
diff --git a/static/app/views/detectors/components/details/common/assignee.tsx b/static/app/views/detectors/components/details/common/assignee.tsx
new file mode 100644
index 00000000000000..5ecf5cdaa815b1
--- /dev/null
+++ b/static/app/views/detectors/components/details/common/assignee.tsx
@@ -0,0 +1,63 @@
+import {Flex} from 'sentry/components/core/layout/flex';
+import {Tooltip} from 'sentry/components/core/tooltip';
+import Placeholder from 'sentry/components/placeholder';
+import Section from 'sentry/components/workflowEngine/ui/section';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {parseActorIdentifier} from 'sentry/utils/parseActorIdentifier';
+import {useTeamsById} from 'sentry/utils/useTeamsById';
+import useUserFromId from 'sentry/utils/useUserFromId';
+
+function AssignToTeam({teamId}: {teamId: string}) {
+ const {teams, isLoading} = useTeamsById({ids: [teamId]});
+ const team = teams.find(tm => tm.id === teamId);
+
+ if (isLoading) {
+ return (
+
+ {t('Assign to')}
+
+ );
+ }
+
+ return t('Assign to %s', `#${team?.slug ?? 'unknown'}`);
+}
+
+function AssignToUser({userId}: {userId: string}) {
+ const {isPending, data: user} = useUserFromId({id: parseInt(userId, 10)});
+
+ if (isPending) {
+ return (
+
+ {t('Assign to')}
+
+ );
+ }
+
+ const title = user?.name ?? user?.email ?? t('Unknown user');
+ return (
+
+ {t('Assign to %s', title)}
+
+ );
+}
+
+function DetectorOwner({owner}: {owner: string | null}) {
+ const parsed = parseActorIdentifier(owner);
+ if (parsed?.type === 'team') {
+ return ;
+ }
+ if (parsed?.type === 'user') {
+ return ;
+ }
+
+ return t('Unassigned');
+}
+
+export function DetectorDetailsAssignee({owner}: {owner: string | null}) {
+ return (
+
+ );
+}
diff --git a/static/app/views/detectors/components/details/common/extraDetails.tsx b/static/app/views/detectors/components/details/common/extraDetails.tsx
index 6ae85bc2c89504..f2593ae7bc1b43 100644
--- a/static/app/views/detectors/components/details/common/extraDetails.tsx
+++ b/static/app/views/detectors/components/details/common/extraDetails.tsx
@@ -10,6 +10,7 @@ import Section from 'sentry/components/workflowEngine/ui/section';
import {t} from 'sentry/locale';
import type {Detector} from 'sentry/types/workflowEngine/detectors';
import useUserFromId from 'sentry/utils/useUserFromId';
+import {getDetectorEnvironment} from 'sentry/views/detectors/utils/getDetectorEnvironment';
type Props = {
children: React.ReactNode;
@@ -97,19 +98,7 @@ DetectorExtraDetails.Environment = function DetectorExtraDetailsEnvironment({
}: {
detector: Detector;
}) {
- // TODO: Add common function for getting environment from a detector
- const getEnvironment = () => {
- if (detector.type !== 'metric_issue') {
- return '';
- }
-
- return (
- detector.dataSources?.find(ds => ds.type === 'snuba_query_subscription')?.queryObj
- ?.snubaQuery.environment ?? t('All environments')
- );
- };
-
- const environment = getEnvironment();
+ const environment = getDetectorEnvironment(detector);
return (
+
diff --git a/static/app/views/detectors/components/details/index.tsx b/static/app/views/detectors/components/details/index.tsx
index 3de6106ec040f1..9316ad79c30c2e 100644
--- a/static/app/views/detectors/components/details/index.tsx
+++ b/static/app/views/detectors/components/details/index.tsx
@@ -3,6 +3,7 @@ import type {Detector} from 'sentry/types/workflowEngine/detectors';
import {unreachable} from 'sentry/utils/unreachable';
import {FallbackDetectorDetails} from 'sentry/views/detectors/components/details/fallback';
import {MetricDetectorDetails} from 'sentry/views/detectors/components/details/metric';
+import {UptimeDetectorDetails} from 'sentry/views/detectors/components/details/uptime';
type DetectorDetailsContentProps = {
detector: Detector;
@@ -15,6 +16,7 @@ export function DetectorDetailsContent({detector, project}: DetectorDetailsConte
case 'metric_issue':
return ;
case 'uptime_domain_failure':
+ return ;
case 'uptime_subscription':
case 'error':
return ;
diff --git a/static/app/views/detectors/components/details/metric/sidebar.tsx b/static/app/views/detectors/components/details/metric/sidebar.tsx
index 666bb5e0e7a974..20670ceca252c1 100644
--- a/static/app/views/detectors/components/details/metric/sidebar.tsx
+++ b/static/app/views/detectors/components/details/metric/sidebar.tsx
@@ -2,9 +2,6 @@ import {Fragment} from 'react';
import styled from '@emotion/styled';
import {GroupPriorityBadge} from 'sentry/components/badge/groupPriority';
-import {Flex} from 'sentry/components/core/layout';
-import {Tooltip} from 'sentry/components/core/tooltip';
-import Placeholder from 'sentry/components/placeholder';
import Section from 'sentry/components/workflowEngine/ui/section';
import {IconArrow} from 'sentry/icons';
import {t} from 'sentry/locale';
@@ -15,8 +12,7 @@ import {
DetectorPriorityLevel,
} from 'sentry/types/workflowEngine/dataConditions';
import type {MetricDetector} from 'sentry/types/workflowEngine/detectors';
-import {useTeamsById} from 'sentry/utils/useTeamsById';
-import useUserFromId from 'sentry/utils/useUserFromId';
+import {DetectorDetailsAssignee} from 'sentry/views/detectors/components/details/common/assignee';
import {DetectorExtraDetails} from 'sentry/views/detectors/components/details/common/extraDetails';
import {MetricDetectorDetailsDetect} from 'sentry/views/detectors/components/details/metric/detect';
import {getResolutionDescription} from 'sentry/views/detectors/utils/getDetectorResolutionDescription';
@@ -26,40 +22,6 @@ interface DetectorDetailsSidebarProps {
detector: MetricDetector;
}
-function AssignToTeam({teamId}: {teamId: string}) {
- const {teams, isLoading} = useTeamsById({ids: [teamId]});
- const team = teams.find(tm => tm.id === teamId);
-
- if (isLoading) {
- return (
-
- {t('Assign to')}
-
- );
- }
-
- return t('Assign to %s', `#${team?.slug ?? 'unknown'}`);
-}
-
-function AssignToUser({userId}: {userId: string}) {
- const {isPending, data: user} = useUserFromId({id: parseInt(userId, 10)});
-
- if (isPending) {
- return (
-
- {t('Assign to')}
-
- );
- }
-
- const title = user?.name ?? user?.email ?? t('Unknown user');
- return (
-
- {t('Assign to %s', title)}
-
- );
-}
-
function DetectorPriorities({detector}: {detector: MetricDetector}) {
const detectionType = detector.config?.detectionType || 'static';
@@ -134,31 +96,13 @@ function DetectorResolve({detector}: {detector: MetricDetector}) {
return {description}
;
}
-function DetectorAssignee({owner}: {owner: string | null}) {
- if (!owner) {
- return t('Unassigned');
- }
-
- const [ownerType, ownerId] = owner.split(':');
- if (ownerType === 'team') {
- return ;
- }
- if (ownerType === 'user') {
- return ;
- }
-
- return t('Unassigned');
-}
-
export function MetricDetectorDetailsSidebar({detector}: DetectorDetailsSidebarProps) {
return (
-
+
diff --git a/static/app/views/detectors/components/details/uptime/index.spec.tsx b/static/app/views/detectors/components/details/uptime/index.spec.tsx
new file mode 100644
index 00000000000000..a6f9603801863e
--- /dev/null
+++ b/static/app/views/detectors/components/details/uptime/index.spec.tsx
@@ -0,0 +1,47 @@
+import {AutomationFixture} from 'sentry-fixture/automations';
+import {UptimeDetectorFixture} from 'sentry-fixture/detectors';
+import {ProjectFixture} from 'sentry-fixture/project';
+
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import {UptimeDetectorDetails} from 'sentry/views/detectors/components/details/uptime';
+
+describe('UptimeDetectorDetails', function () {
+ const defaultProps = {
+ detector: UptimeDetectorFixture(),
+ project: ProjectFixture(),
+ };
+
+ beforeEach(function () {
+ MockApiClient.addMockResponse({
+ url: '/organizations/org-slug/workflows/',
+ method: 'GET',
+ body: [AutomationFixture()],
+ });
+ });
+
+ it('displays correct detector details', async function () {
+ render();
+
+ expect(screen.getByText('Three consecutive failed checks.')).toBeInTheDocument();
+ expect(screen.getByText('Three consecutive successful checks.')).toBeInTheDocument();
+
+ // Interval
+ expect(screen.getByText('Every 1 minute')).toBeInTheDocument();
+ // URL
+ expect(screen.getByText('https://example.com')).toBeInTheDocument();
+ // Method
+ expect(screen.getByText('GET')).toBeInTheDocument();
+ // Environment
+ expect(screen.getByText('production')).toBeInTheDocument();
+
+ // Connected automation
+ expect(await screen.findByText('Automation 1')).toBeInTheDocument();
+
+ // Edit button takes you to the edit page
+ expect(screen.getByRole('button', {name: 'Edit'})).toHaveAttribute(
+ 'href',
+ '/organizations/org-slug/issues/monitors/3/edit/'
+ );
+ });
+});
diff --git a/static/app/views/detectors/components/details/uptime/index.tsx b/static/app/views/detectors/components/details/uptime/index.tsx
new file mode 100644
index 00000000000000..3593087cc6e1af
--- /dev/null
+++ b/static/app/views/detectors/components/details/uptime/index.tsx
@@ -0,0 +1,58 @@
+import {KeyValueTableRow} from 'sentry/components/keyValueTable';
+import DetailLayout from 'sentry/components/workflowEngine/layout/detail';
+import Section from 'sentry/components/workflowEngine/ui/section';
+import {t} from 'sentry/locale';
+import type {Project} from 'sentry/types/project';
+import type {UptimeDetector} from 'sentry/types/workflowEngine/detectors';
+import getDuration from 'sentry/utils/duration/getDuration';
+import {DetectorDetailsAssignee} from 'sentry/views/detectors/components/details/common/assignee';
+import {DetectorDetailsAutomations} from 'sentry/views/detectors/components/details/common/automations';
+import {DetectorExtraDetails} from 'sentry/views/detectors/components/details/common/extraDetails';
+import {DetectorDetailsHeader} from 'sentry/views/detectors/components/details/common/header';
+import {DetectorDetailsOngoingIssues} from 'sentry/views/detectors/components/details/common/ongoingIssues';
+
+type UptimeDetectorDetailsProps = {
+ detector: UptimeDetector;
+ project: Project;
+};
+
+export function UptimeDetectorDetails({detector, project}: UptimeDetectorDetailsProps) {
+ const dataSource = detector.dataSources[0];
+
+ return (
+
+
+
+
+
+
+
+
+ {t('Three consecutive failed checks.')}
+
+ {t('Three consecutive successful checks.')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/static/app/views/detectors/components/forms/metric/metricFormData.tsx b/static/app/views/detectors/components/forms/metric/metricFormData.tsx
index 73c8782e450f87..e478e1532190ab 100644
--- a/static/app/views/detectors/components/forms/metric/metricFormData.tsx
+++ b/static/app/views/detectors/components/forms/metric/metricFormData.tsx
@@ -22,6 +22,7 @@ import {
Dataset,
EventTypes,
} from 'sentry/views/alerts/rules/metric/types';
+import {getDetectorEnvironment} from 'sentry/views/detectors/utils/getDetectorEnvironment';
/**
* Dataset types for detectors
@@ -459,7 +460,7 @@ export function metricSavedDetectorToFormData(
// Core detector fields
name: detector.name,
projectId: detector.projectId,
- environment: snubaQuery?.environment || '',
+ environment: getDetectorEnvironment(detector) || '',
owner: detector.owner || '',
query: snubaQuery?.query || '',
aggregateFunction,
diff --git a/static/app/views/detectors/components/forms/uptime/fields.tsx b/static/app/views/detectors/components/forms/uptime/fields.tsx
index 998b26c34377c5..2de0fdd4193d3c 100644
--- a/static/app/views/detectors/components/forms/uptime/fields.tsx
+++ b/static/app/views/detectors/components/forms/uptime/fields.tsx
@@ -3,6 +3,7 @@ import type {
Detector,
UptimeDetectorUpdatePayload,
} from 'sentry/types/workflowEngine/detectors';
+import {getDetectorEnvironment} from 'sentry/views/detectors/utils/getDetectorEnvironment';
export interface UptimeDetectorFormData {
body: string;
@@ -76,7 +77,7 @@ export function uptimeSavedDetectorToFormData(
}
const dataSource = detector.dataSources?.[0];
- const environment = 'environment' in detector.config ? detector.config.environment : '';
+ const environment = getDetectorEnvironment(detector);
const common = {
name: detector.name,
diff --git a/static/app/views/detectors/utils/getDetectorEnvironment.tsx b/static/app/views/detectors/utils/getDetectorEnvironment.tsx
new file mode 100644
index 00000000000000..55ff2cb078c5d9
--- /dev/null
+++ b/static/app/views/detectors/utils/getDetectorEnvironment.tsx
@@ -0,0 +1,26 @@
+import {t} from 'sentry/locale';
+import type {Detector} from 'sentry/types/workflowEngine/detectors';
+import {unreachable} from 'sentry/utils/unreachable';
+
+const ALL_ENVIRONMENTS = t('All environments');
+
+export function getDetectorEnvironment(detector: Detector) {
+ const detectorType = detector.type;
+ switch (detectorType) {
+ case 'metric_issue':
+ return (
+ detector.dataSources?.find(ds => ds.type === 'snuba_query_subscription')?.queryObj
+ ?.snubaQuery.environment ?? ALL_ENVIRONMENTS
+ );
+ case 'uptime_domain_failure':
+ return detector.config.environment ?? ALL_ENVIRONMENTS;
+ case 'uptime_subscription':
+ // TODO: Implement this when we know the shape of object
+ return ALL_ENVIRONMENTS;
+ case 'error':
+ return ALL_ENVIRONMENTS;
+ default:
+ unreachable(detectorType);
+ return ALL_ENVIRONMENTS;
+ }
+}
diff --git a/tests/js/fixtures/detectors.ts b/tests/js/fixtures/detectors.ts
index 4893a032d0ab7e..5abde3acd3df83 100644
--- a/tests/js/fixtures/detectors.ts
+++ b/tests/js/fixtures/detectors.ts
@@ -1,3 +1,4 @@
+import {AutomationFixture} from 'sentry-fixture/automations';
import {DataConditionGroupFixture} from 'sentry-fixture/dataConditions';
import {UserFixture} from 'sentry-fixture/user';
@@ -5,6 +6,8 @@ import type {
ErrorDetector,
MetricDetector,
SnubaQueryDataSource,
+ UptimeDetector,
+ UptimeSubscriptionDataSource,
} from 'sentry/types/workflowEngine/detectors';
import {Dataset, EventTypes} from 'sentry/views/alerts/rules/metric/types';
@@ -40,7 +43,7 @@ export function ErrorDetectorFixture(params: Partial = {}): Error
dateCreated: '2025-01-01T00:00:00.000Z',
dateUpdated: '2025-01-01T00:00:00.000Z',
disabled: false,
- id: '1',
+ id: '2',
lastTriggered: '2025-01-01T00:00:00.000Z',
owner: null,
projectId: '1',
@@ -50,6 +53,50 @@ export function ErrorDetectorFixture(params: Partial = {}): Error
};
}
+export function UptimeDetectorFixture(
+ params: Partial = {}
+): UptimeDetector {
+ return {
+ name: 'Uptime Detector',
+ createdBy: null,
+ dateCreated: '2025-01-01T00:00:00.000Z',
+ dateUpdated: '2025-01-01T00:00:00.000Z',
+ disabled: false,
+ id: '3',
+ lastTriggered: '2025-01-01T00:00:00.000Z',
+ owner: null,
+ projectId: '1',
+ workflowIds: [AutomationFixture().id],
+ type: 'uptime_domain_failure',
+ config: {
+ environment: 'production',
+ },
+ dataSources: [UptimeSubscriptionDataSourceFixture()],
+ ...params,
+ };
+}
+
+function UptimeSubscriptionDataSourceFixture(
+ params: Partial = {}
+): UptimeSubscriptionDataSource {
+ return {
+ id: '1',
+ organizationId: '1',
+ sourceId: '1',
+ type: 'uptime_subscription',
+ queryObj: {
+ body: null,
+ headers: [],
+ intervalSeconds: 60,
+ method: 'GET',
+ timeoutMs: 5000,
+ traceSampling: false,
+ url: 'https://example.com',
+ },
+ ...params,
+ };
+}
+
export function SnubaQueryDataSourceFixture(
params: Partial = {}
): SnubaQueryDataSource {