Skip to content

feat(aci): Implement uptime detector details page #95391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions static/app/types/workflowEngine/detectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Flex align="center" gap={space(0.5)}>
{t('Assign to')} <Placeholder width="80px" height="16px" />
</Flex>
);
}

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 (
<Flex align="center" gap={space(0.5)}>
{t('Assign to')} <Placeholder width="80px" height="16px" />
</Flex>
);
}

const title = user?.name ?? user?.email ?? t('Unknown user');
return (
<Tooltip title={title} showOnlyOnOverflow>
{t('Assign to %s', title)}
</Tooltip>
);
}

function DetectorOwner({owner}: {owner: string | null}) {
const parsed = parseActorIdentifier(owner);
if (parsed?.type === 'team') {
return <AssignToTeam teamId={parsed.id} />;
}
if (parsed?.type === 'user') {
return <AssignToUser userId={parsed.id} />;
}

return t('Unassigned');
}

export function DetectorDetailsAssignee({owner}: {owner: string | null}) {
return (
<Section title={t('Assign')}>
<DetectorOwner owner={owner} />
</Section>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 '<placeholder>';
}

return (
detector.dataSources?.find(ds => ds.type === 'snuba_query_subscription')?.queryObj
?.snubaQuery.environment ?? t('All environments')
);
};

const environment = getEnvironment();
const environment = getDetectorEnvironment(detector);

return (
<KeyValueTableRow
Expand Down
2 changes: 2 additions & 0 deletions static/app/views/detectors/components/details/fallback.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import DetailLayout from 'sentry/components/workflowEngine/layout/detail';
import type {Project} from 'sentry/types/project';
import type {Detector} from 'sentry/types/workflowEngine/detectors';
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';
Expand All @@ -24,6 +25,7 @@ export function FallbackDetectorDetails({
<DetectorDetailsAutomations detector={detector} />
</DetailLayout.Main>
<DetailLayout.Sidebar>
<DetectorDetailsAssignee owner={detector.owner} />
<DetectorExtraDetails>
<DetectorExtraDetails.DateCreated detector={detector} />
<DetectorExtraDetails.CreatedBy detector={detector} />
Expand Down
2 changes: 2 additions & 0 deletions static/app/views/detectors/components/details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,6 +16,7 @@ export function DetectorDetailsContent({detector, project}: DetectorDetailsConte
case 'metric_issue':
return <MetricDetectorDetails detector={detector} project={project} />;
case 'uptime_domain_failure':
return <UptimeDetectorDetails detector={detector} project={project} />;
case 'uptime_subscription':
case 'error':
return <FallbackDetectorDetails detector={detector} project={project} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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 (
<Flex align="center" gap={space(0.5)}>
{t('Assign to')} <Placeholder width="80px" height="16px" />
</Flex>
);
}

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 (
<Flex align="center" gap={space(0.5)}>
{t('Assign to')} <Placeholder width="80px" height="16px" />
</Flex>
);
}

const title = user?.name ?? user?.email ?? t('Unknown user');
return (
<Tooltip title={title} showOnlyOnOverflow>
{t('Assign to %s', title)}
</Tooltip>
);
}

function DetectorPriorities({detector}: {detector: MetricDetector}) {
const detectionType = detector.config?.detectionType || 'static';

Expand Down Expand Up @@ -134,31 +96,13 @@ function DetectorResolve({detector}: {detector: MetricDetector}) {
return <div>{description}</div>;
}

function DetectorAssignee({owner}: {owner: string | null}) {
if (!owner) {
return t('Unassigned');
}

const [ownerType, ownerId] = owner.split(':');
if (ownerType === 'team') {
return <AssignToTeam teamId={ownerId!} />;
}
if (ownerType === 'user') {
return <AssignToUser userId={ownerId!} />;
}

return t('Unassigned');
}

export function MetricDetectorDetailsSidebar({detector}: DetectorDetailsSidebarProps) {
return (
<Fragment>
<Section title={t('Detect')}>
<MetricDetectorDetailsDetect detector={detector} />
</Section>
<Section title={t('Assign')}>
<DetectorAssignee owner={detector.owner} />
</Section>
<DetectorDetailsAssignee owner={detector.owner} />
<Section title={t('Prioritize')}>
<DetectorPriorities detector={detector} />
</Section>
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<UptimeDetectorDetails {...defaultProps} />);

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/'
);
});
});
58 changes: 58 additions & 0 deletions static/app/views/detectors/components/details/uptime/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<DetailLayout>
<DetectorDetailsHeader detector={detector} project={project} />
<DetailLayout.Body>
<DetailLayout.Main>
<DetectorDetailsOngoingIssues />
<DetectorDetailsAutomations detector={detector} />
</DetailLayout.Main>
<DetailLayout.Sidebar>
<Section title={t('Detect')}>{t('Three consecutive failed checks.')}</Section>
<Section title={t('Resolve')}>
{t('Three consecutive successful checks.')}
</Section>
<DetectorDetailsAssignee owner={detector.owner} />
<DetectorExtraDetails>
<KeyValueTableRow
keyName={t('Interval')}
value={t('Every %s', getDuration(dataSource.queryObj.intervalSeconds))}
/>
<KeyValueTableRow
keyName={t('URL')}
value={detector.dataSources[0].queryObj.url}
/>
<KeyValueTableRow
keyName={t('Method')}
value={detector.dataSources[0].queryObj.method}
/>
<DetectorExtraDetails.Environment detector={detector} />
<DetectorExtraDetails.DateCreated detector={detector} />
<DetectorExtraDetails.CreatedBy detector={detector} />
<DetectorExtraDetails.LastModified detector={detector} />
</DetectorExtraDetails>
</DetailLayout.Sidebar>
</DetailLayout.Body>
</DetailLayout>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading