Skip to content

Commit

Permalink
feat(alerts): Support crons via frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
evanpurkhiser committed Jan 21, 2025
1 parent a5e9581 commit 0bf72a1
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 31 deletions.
20 changes: 17 additions & 3 deletions static/app/components/badge/alertBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import styled from '@emotion/styled';

import {DiamondStatus} from 'sentry/components/diamondStatus';
import {IconCheckmark, IconExclamation, IconFire, IconIssues} from 'sentry/icons';
import {
IconCheckmark,
IconExclamation,
IconFire,
IconIssues,
IconMute,
} from 'sentry/icons';
import type {SVGIconProps} from 'sentry/icons/svgIcon';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
Expand All @@ -13,6 +19,10 @@ type Props = {
* @deprecated use withText
*/
hideText?: true;
/**
* Displays a "disabled" badge
*/
isDisabled?: boolean;
/**
* There is no status for issue, this is to facilitate this custom usage.
*/
Expand All @@ -31,12 +41,16 @@ type Props = {
* This badge is a composition of DiamondStatus specifically used for incident
* alerts.
*/
function AlertBadge({status, withText, isIssue}: Props) {
function AlertBadge({status, withText, isIssue, isDisabled}: Props) {
let statusText = t('Resolved');
let Icon: React.ComponentType<SVGIconProps> = IconCheckmark;
let color: ColorOrAlias = 'successText';

if (isIssue) {
if (isDisabled) {
statusText = t('Disabled');
Icon = IconMute;
color = 'disabled';
} else if (isIssue) {
statusText = t('Issue');
Icon = SizedIconIssue;
color = 'subText';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import {hasActiveIncident} from 'sentry/views/alerts/list/rules/utils';
import {
type CombinedAlerts,
CombinedAlertType,
type CronRule,
type IssueAlert,
type MetricAlert,
type UptimeAlert,
} from 'sentry/views/alerts/types';
import {scheduleAsText} from 'sentry/views/monitors/utils/scheduleAsText';

interface Props {
rule: CombinedAlerts;
Expand All @@ -18,12 +20,21 @@ interface Props {
* Displays the time since the last uptime incident given an uptime alert rule
*/
function LastUptimeIncident({rule}: {rule: UptimeAlert}) {
// TODO(davidenwang): Once we have a lastTriggered field returned from backend, display that info here
// TODO(davidenwang): Once we have a lastTriggered field returned from
// backend, display that info here
return tct('Actively monitoring every [interval]', {
interval: getDuration(rule.intervalSeconds),
});
}

function LastCronMonitorIncident({rule}: {rule: CronRule}) {
// TODO(evanpurkhiser): Would probably be better if we had a way to get the
// most recent incident.
return tct('Expected every [interval]', {
interval: scheduleAsText(rule.config),
});
}

/**
* Displays the last time an issue alert was triggered
*/
Expand Down Expand Up @@ -68,6 +79,8 @@ export default function AlertLastIncidentActivationInfo({rule}: Props) {
switch (rule.type) {
case CombinedAlertType.UPTIME:
return <LastUptimeIncident rule={rule} />;
case CombinedAlertType.CRONS:
return <LastCronMonitorIncident rule={rule} />;
case CombinedAlertType.ISSUE:
return <LastIssueTrigger rule={rule} />;
case CombinedAlertType.METRIC:
Expand Down
3 changes: 2 additions & 1 deletion static/app/views/alerts/list/rules/alertRulesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,11 @@ function AlertRulesList() {
};

const handleDeleteRule = async (projectId: string, rule: CombinedAlerts) => {
const deleteEndpoints = {
const deleteEndpoints: Record<CombinedAlertType, string> = {
[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}/`,
};

try {
Expand Down
31 changes: 31 additions & 0 deletions static/app/views/alerts/list/rules/combinedAlertBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import AlertBadge from 'sentry/components/badge/alertBadge';
import {Tooltip} from 'sentry/components/tooltip';
import {t, tct} from 'sentry/locale';
import {getAggregateEnvStatus} from 'sentry/views/alerts/rules/crons/utils';
import {UptimeMonitorStatus} from 'sentry/views/alerts/rules/uptime/types';
import {
type CombinedAlerts,
CombinedAlertType,
IncidentStatus,
} from 'sentry/views/alerts/types';
import {isIssueAlert} from 'sentry/views/alerts/utils';
import {MonitorStatus} from 'sentry/views/monitors/types';

interface Props {
rule: CombinedAlerts;
Expand All @@ -31,6 +33,25 @@ const UptimeStatusText: Record<
},
};

const CronsStatusText: Record<
MonitorStatus,
{statusText: string; disabled?: boolean; incidentStatus?: IncidentStatus}
> = {
[MonitorStatus.ACTIVE]: {
statusText: t('Active'),
incidentStatus: IncidentStatus.CLOSED,
},
[MonitorStatus.OK]: {statusText: t('Ok'), incidentStatus: IncidentStatus.CLOSED},
[MonitorStatus.ERROR]: {
statusText: t('Failing'),
incidentStatus: IncidentStatus.CRITICAL,
},
[MonitorStatus.DISABLED]: {
statusText: t('Disabled'),
disabled: true,
},
};

/**
* Takes in an alert rule (metric or issue) and renders the
* appropriate tooltip and AlertBadge
Expand All @@ -45,6 +66,16 @@ export default function CombinedAlertBadge({rule}: Props) {
);
}

if (rule.type === CombinedAlertType.CRONS) {
const envStatus = getAggregateEnvStatus(rule.environments);
const {statusText, incidentStatus, disabled} = CronsStatusText[envStatus];
return (
<Tooltip title={tct('Cron Monitor Status: [statusText]', {statusText})}>
<AlertBadge status={incidentStatus} isDisabled={disabled} />
</Tooltip>
);
}

return (
<Tooltip
title={
Expand Down
50 changes: 26 additions & 24 deletions static/app/views/alerts/list/rules/row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ import AlertLastIncidentActivationInfo from 'sentry/views/alerts/list/rules/aler
import AlertRuleStatus from 'sentry/views/alerts/list/rules/alertRuleStatus';
import CombinedAlertBadge from 'sentry/views/alerts/list/rules/combinedAlertBadge';
import {getActor} from 'sentry/views/alerts/list/rules/utils';
import {
UptimeMonitorMode,
UptimeMonitorStatus,
} from 'sentry/views/alerts/rules/uptime/types';
import {UptimeMonitorMode} from 'sentry/views/alerts/rules/uptime/types';

import type {CombinedAlerts} from '../../types';
import {CombinedAlertType} from '../../types';
Expand Down Expand Up @@ -60,8 +57,13 @@ function RuleListRow({
const [assignee, setAssignee] = useState<string>('');

const isUptime = rule.type === CombinedAlertType.UPTIME;
const isCron = rule.type === CombinedAlertType.CRONS;

const slug = isUptime ? rule.projectSlug : rule.projects[0]!;
const slug = isUptime
? rule.projectSlug
: isCron
? rule.project.slug
: rule.projects[0]!;

const editKey = {
[CombinedAlertType.ISSUE]: 'rules',
Expand Down Expand Up @@ -99,6 +101,7 @@ function RuleListRow({
[CombinedAlertType.ISSUE]: ['edit', 'duplicate', 'delete'],
[CombinedAlertType.METRIC]: ['edit', 'duplicate', 'delete'],
[CombinedAlertType.UPTIME]: ['edit', 'delete'],
[CombinedAlertType.CRONS]: ['edit', 'delete'],
};

const actions: MenuItemProps[] = [
Expand Down Expand Up @@ -216,20 +219,25 @@ function RuleListRow({
</Tag>
) : null;

function ruleUrl() {
switch (rule.type) {
case CombinedAlertType.METRIC:
return `/organizations/${orgId}/alerts/rules/details/${rule.id}/`;
case CombinedAlertType.CRONS:
return `/organizations/${orgId}/alerts/rules/crons/${rule.project.slug}/${rule.id}/details/`;
case CombinedAlertType.UPTIME:
return `/organizations/${orgId}/alerts/rules/uptime/${rule.projectSlug}/${rule.id}/details/`;
default:
return `/organizations/${orgId}/alerts/rules/${rule.projects[0]}/${rule.id}/details/`;
}
}

return (
<ErrorBoundary>
<AlertNameWrapper isIssueAlert={isIssueAlert(rule)}>
<AlertNameAndStatus>
<AlertName>
<Link
to={
rule.type === CombinedAlertType.ISSUE
? `/organizations/${orgId}/alerts/rules/${rule.projects[0]}/${rule.id}/details/`
: rule.type === CombinedAlertType.METRIC
? `/organizations/${orgId}/alerts/rules/details/${rule.id}/`
: `/organizations/${orgId}/alerts/rules/uptime/${rule.projectSlug}/${rule.id}/details/`
}
>
<Link to={ruleUrl()}>
{rule.name} {titleBadge}
</Link>
</AlertName>
Expand All @@ -242,17 +250,11 @@ function RuleListRow({
<FlexCenter>
<CombinedAlertBadge rule={rule} />
</FlexCenter>
<MarginLeft>
{isUptime ? (
rule.status === UptimeMonitorStatus.FAILED ? (
t('Down')
) : (
t('Up')
)
) : (
{!isUptime && !isCron && (
<MarginLeft>
<AlertRuleStatus rule={rule} />
)}
</MarginLeft>
</MarginLeft>
)}
</FlexCenter>
<FlexCenter>
<ProjectBadgeContainer>
Expand Down
3 changes: 3 additions & 0 deletions static/app/views/alerts/list/rules/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export function getActor(rule: CombinedAlerts): Actor | null {
if (rule.type === CombinedAlertType.UPTIME) {
return rule.owner;
}
if (rule.type === CombinedAlertType.CRONS) {
return rule.owner;
}

const ownerId = rule.owner?.split(':')[1];
return ownerId ? {type: 'team' as Actor['type'], id: ownerId, name: ''} : null;
Expand Down
19 changes: 19 additions & 0 deletions static/app/views/alerts/rules/crons/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {type MonitorEnvironment, MonitorStatus} from 'sentry/views/monitors/types';

const MONITOR_STATUS_PRECEDENT = [
MonitorStatus.ERROR,
MonitorStatus.OK,
MonitorStatus.ACTIVE,
MonitorStatus.DISABLED,
];

/**
* Get the aggregate MonitorStatus of a set of monitor environments.
*/
export function getAggregateEnvStatus(environments: MonitorEnvironment[]): MonitorStatus {
const status = MONITOR_STATUS_PRECEDENT.find(s =>
environments.some(env => env.status === s)
);

return status ?? MonitorStatus.ACTIVE;
}
9 changes: 7 additions & 2 deletions static/app/views/alerts/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {IssueAlertRule} from 'sentry/types/alerts';
import type {User} from 'sentry/types/user';
import type {MetricRule} from 'sentry/views/alerts/rules/metric/types';
import type {UptimeRule} from 'sentry/views/alerts/rules/uptime/types';
import type {Monitor} from 'sentry/views/monitors/types';

type Data = [number, {count: number}[]][];

Expand Down Expand Up @@ -89,7 +90,7 @@ export enum CombinedAlertType {
METRIC = 'alert_rule',
ISSUE = 'rule',
UPTIME = 'uptime',
CRONS = 'crons',
CRONS = 'monitor',
}

export interface IssueAlert extends IssueAlertRule {
Expand All @@ -105,9 +106,13 @@ export interface UptimeAlert extends UptimeRule {
type: CombinedAlertType.UPTIME;
}

export interface CronRule extends Monitor {
type: CombinedAlertType.CRONS;
}

export type CombinedMetricIssueAlerts = IssueAlert | MetricAlert;

export type CombinedAlerts = CombinedMetricIssueAlerts | UptimeAlert;
export type CombinedAlerts = CombinedMetricIssueAlerts | UptimeAlert | CronRule;

export type Anomaly = {
anomaly: {anomaly_score: number; anomaly_type: AnomalyType};
Expand Down

0 comments on commit 0bf72a1

Please sign in to comment.