metricActivity.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import {Fragment, type ReactElement} from 'react';
  2. import styled from '@emotion/styled';
  3. import moment from 'moment-timezone';
  4. import Duration from 'sentry/components/duration';
  5. import GlobalSelectionLink from 'sentry/components/globalSelectionLink';
  6. import Link from 'sentry/components/links/link';
  7. import {StatusIndicator} from 'sentry/components/statusIndicator';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {ActivationConditionType} from 'sentry/types/alerts';
  11. import type {Organization} from 'sentry/types/organization';
  12. import getDuration from 'sentry/utils/duration/getDuration';
  13. import getDynamicText from 'sentry/utils/getDynamicText';
  14. import {capitalize} from 'sentry/utils/string/capitalize';
  15. import {COMPARISON_DELTA_OPTIONS} from 'sentry/views/alerts/rules/metric/constants';
  16. import {StyledDateTime} from 'sentry/views/alerts/rules/metric/details/styles';
  17. import {AlertRuleThresholdType} from 'sentry/views/alerts/rules/metric/types';
  18. import type {ActivityType, Incident} from 'sentry/views/alerts/types';
  19. import {IncidentActivityType, IncidentStatus} from 'sentry/views/alerts/types';
  20. import {alertDetailsLink} from 'sentry/views/alerts/utils';
  21. import {AlertWizardAlertNames} from 'sentry/views/alerts/wizard/options';
  22. import {getAlertTypeFromAggregateDataset} from 'sentry/views/alerts/wizard/utils';
  23. type MetricAlertActivityProps = {
  24. incident: Incident;
  25. organization: Organization;
  26. };
  27. function MetricAlertActivity({organization, incident}: MetricAlertActivityProps) {
  28. // NOTE: while _possible_, we should never expect an incident to _not_ have a status_change activity
  29. const activities: ActivityType[] = (incident.activities ?? []).filter(
  30. activity => activity.type === IncidentActivityType.STATUS_CHANGE
  31. );
  32. const statusValues = [String(IncidentStatus.CRITICAL), String(IncidentStatus.WARNING)];
  33. // TODO: kinda cheating with the forced `!`. Is there a better way to type this?
  34. const latestActivity: ActivityType = activities.find(activity =>
  35. statusValues.includes(String(activity.value))
  36. )!;
  37. const isCritical = Number(latestActivity.value) === IncidentStatus.CRITICAL;
  38. // Find the _final_ most recent activity _after_ our triggered activity
  39. // This exists for the `CLOSED` state (or any state NOT WARNING/CRITICAL)
  40. const finalActivity = activities.find(
  41. activity => activity.previousValue === latestActivity.value
  42. );
  43. const activityDuration = (
  44. finalActivity ? moment(finalActivity.dateCreated) : moment()
  45. ).diff(moment(latestActivity.dateCreated), 'milliseconds');
  46. const triggerLabel = isCritical ? 'critical' : 'warning';
  47. const curentTrigger = incident.alertRule.triggers.find(
  48. trigger => trigger.label === triggerLabel
  49. );
  50. const timeWindow = getDuration(incident.alertRule.timeWindow * 60);
  51. const alertName = capitalize(
  52. AlertWizardAlertNames[getAlertTypeFromAggregateDataset(incident.alertRule)]
  53. );
  54. const project = incident.alertRule.projects[0];
  55. const activation = incident.activation;
  56. let activationBlock: ReactElement | null = null;
  57. // TODO: Split this string check into a separate component
  58. if (activation) {
  59. let condition;
  60. let activator;
  61. switch (activation.conditionType) {
  62. case String(ActivationConditionType.RELEASE_CREATION):
  63. condition = 'Release';
  64. activator = (
  65. <GlobalSelectionLink
  66. to={{
  67. pathname: `/organizations/${
  68. organization.slug
  69. }/releases/${encodeURIComponent(activation.activator)}/`,
  70. query: {project: project},
  71. }}
  72. style={{textOverflow: 'ellipsis', overflowX: 'inherit'}}
  73. >
  74. {activation.activator}
  75. </GlobalSelectionLink>
  76. );
  77. break;
  78. case String(ActivationConditionType.DEPLOY_CREATION):
  79. condition = 'Deploy';
  80. activator = activation.activator;
  81. break;
  82. default:
  83. condition = '--';
  84. }
  85. activationBlock = (
  86. <Fragment>
  87. &nbsp;from {condition}&nbsp;{activator}
  88. </Fragment>
  89. );
  90. }
  91. return (
  92. <Fragment>
  93. <Cell>
  94. {latestActivity.value && (
  95. <StatusIndicator
  96. status={isCritical ? 'error' : 'warning'}
  97. tooltipTitle={t('Status: %s', isCritical ? t('Critical') : t('Warning'))}
  98. />
  99. )}
  100. <Link
  101. to={{
  102. pathname: alertDetailsLink(organization, incident),
  103. query: {alert: incident.identifier},
  104. }}
  105. >
  106. #{incident.identifier}
  107. </Link>
  108. </Cell>
  109. <Cell>
  110. {/* If an alert rule is a % comparison based detection type */}
  111. {incident.alertRule.detectionType !== 'dynamic' &&
  112. incident.alertRule.comparisonDelta && (
  113. <Fragment>
  114. {alertName} {curentTrigger?.alertThreshold}%
  115. {t(
  116. ' %s in %s compared to the ',
  117. incident.alertRule.thresholdType === AlertRuleThresholdType.ABOVE
  118. ? t('higher')
  119. : t('lower'),
  120. timeWindow
  121. )}
  122. {COMPARISON_DELTA_OPTIONS.find(
  123. ({value}) => value === incident.alertRule.comparisonDelta
  124. )?.label ?? COMPARISON_DELTA_OPTIONS[0].label}
  125. </Fragment>
  126. )}
  127. {/* If an alert rule is a static detection type */}
  128. {incident.alertRule.detectionType !== 'dynamic' &&
  129. !incident.alertRule.comparisonDelta && (
  130. <Fragment>
  131. {alertName}{' '}
  132. {incident.alertRule.thresholdType === AlertRuleThresholdType.ABOVE
  133. ? t('above')
  134. : t('below')}{' '}
  135. {curentTrigger?.alertThreshold || '_'} {t('within')} {timeWindow}
  136. {activationBlock}
  137. </Fragment>
  138. )}
  139. {/* If an alert rule is a dynamic detection type */}
  140. {incident.alertRule.detectionType === 'dynamic' && (
  141. <Fragment>
  142. {t('Detected an anomaly in the query for ')}
  143. {alertName}
  144. </Fragment>
  145. )}
  146. </Cell>
  147. <Cell>
  148. {activityDuration &&
  149. getDynamicText({
  150. value: <Duration abbreviation seconds={activityDuration / 1000} />,
  151. fixed: '30s',
  152. })}
  153. </Cell>
  154. <Cell>
  155. <StyledDateTime
  156. date={getDynamicText({
  157. value: incident.dateCreated,
  158. fixed: 'Mar 4, 2022 10:44:13 AM UTC',
  159. })}
  160. year
  161. seconds
  162. timeZone
  163. />
  164. </Cell>
  165. </Fragment>
  166. );
  167. }
  168. export default MetricAlertActivity;
  169. const Cell = styled('div')`
  170. display: flex;
  171. align-items: center;
  172. white-space: nowrap;
  173. font-size: ${p => p.theme.fontSizeMedium};
  174. padding: ${space(1)};
  175. overflow-x: hidden;
  176. `;