body.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import moment from 'moment-timezone';
  5. import type {Client} from 'sentry/api';
  6. import {Alert} from 'sentry/components/alert';
  7. import {getInterval} from 'sentry/components/charts/utils';
  8. import * as Layout from 'sentry/components/layouts/thirds';
  9. import Link from 'sentry/components/links/link';
  10. import Panel from 'sentry/components/panels/panel';
  11. import PanelBody from 'sentry/components/panels/panelBody';
  12. import Placeholder from 'sentry/components/placeholder';
  13. import type {ChangeData} from 'sentry/components/timeRangeSelector';
  14. import {TimeRangeSelector} from 'sentry/components/timeRangeSelector';
  15. import {Tooltip} from 'sentry/components/tooltip';
  16. import {t, tct} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import {RuleActionsCategories} from 'sentry/types/alerts';
  19. import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
  20. import type {Organization} from 'sentry/types/organization';
  21. import type {Project} from 'sentry/types/project';
  22. import {formatMRIField} from 'sentry/utils/metrics/mri';
  23. import {shouldShowOnDemandMetricAlertUI} from 'sentry/utils/onDemandMetrics/features';
  24. import AnomalyDetectionFeedbackBanner from 'sentry/views/alerts/rules/metric/details/anomalyDetectionFeedbackBanner';
  25. import {ErrorMigrationWarning} from 'sentry/views/alerts/rules/metric/details/errorMigrationWarning';
  26. import MetricHistory from 'sentry/views/alerts/rules/metric/details/metricHistory';
  27. import type {MetricRule} from 'sentry/views/alerts/rules/metric/types';
  28. import {
  29. AlertRuleComparisonType,
  30. Dataset,
  31. TimePeriod,
  32. } from 'sentry/views/alerts/rules/metric/types';
  33. import {extractEventTypeFilterFromRule} from 'sentry/views/alerts/rules/metric/utils/getEventTypeFilter';
  34. import {isOnDemandMetricAlert} from 'sentry/views/alerts/rules/metric/utils/onDemandMetricAlert';
  35. import {getAlertRuleActionCategory} from 'sentry/views/alerts/rules/utils';
  36. import type {Anomaly, Incident} from 'sentry/views/alerts/types';
  37. import {AlertRuleStatus} from 'sentry/views/alerts/types';
  38. import {alertDetailsLink} from 'sentry/views/alerts/utils';
  39. import {MetricsBetaEndAlert} from 'sentry/views/metrics/metricsBetaEndAlert';
  40. import {isCrashFreeAlert} from '../utils/isCrashFreeAlert';
  41. import {isCustomMetricAlert} from '../utils/isCustomMetricAlert';
  42. import type {TimePeriodType} from './constants';
  43. import {
  44. API_INTERVAL_POINTS_LIMIT,
  45. SELECTOR_RELATIVE_PERIODS,
  46. TIME_WINDOWS,
  47. } from './constants';
  48. import MetricChart from './metricChart';
  49. import RelatedIssues from './relatedIssues';
  50. import RelatedTransactions from './relatedTransactions';
  51. import {MetricDetailsSidebar} from './sidebar';
  52. interface MetricDetailsBodyProps extends RouteComponentProps<{}, {}> {
  53. api: Client;
  54. location: Location;
  55. organization: Organization;
  56. timePeriod: TimePeriodType;
  57. anomalies?: Anomaly[];
  58. incidents?: Incident[];
  59. project?: Project;
  60. rule?: MetricRule;
  61. selectedIncident?: Incident | null;
  62. }
  63. export default function MetricDetailsBody({
  64. api,
  65. project,
  66. rule,
  67. incidents,
  68. organization,
  69. timePeriod,
  70. selectedIncident,
  71. location,
  72. router,
  73. anomalies,
  74. }: MetricDetailsBodyProps) {
  75. function getPeriodInterval() {
  76. const startDate = moment.utc(timePeriod.start);
  77. const endDate = moment.utc(timePeriod.end);
  78. const timeWindow = rule?.timeWindow;
  79. const startEndDifferenceMs = endDate.diff(startDate);
  80. if (
  81. timeWindow &&
  82. (startEndDifferenceMs < API_INTERVAL_POINTS_LIMIT * timeWindow * 60 * 1000 ||
  83. // Special case 7 days * 1m interval over the api limit
  84. startEndDifferenceMs === TIME_WINDOWS[TimePeriod.SEVEN_DAYS])
  85. ) {
  86. return `${timeWindow}m`;
  87. }
  88. return getInterval({start: timePeriod.start, end: timePeriod.end}, 'high');
  89. }
  90. function getFilter(): string[] | null {
  91. if (!rule) {
  92. return null;
  93. }
  94. const {aggregate, dataset, query} = rule;
  95. if (
  96. isCrashFreeAlert(dataset) ||
  97. isCustomMetricAlert(aggregate) ||
  98. dataset === Dataset.EVENTS_ANALYTICS_PLATFORM
  99. ) {
  100. return query.trim().split(' ');
  101. }
  102. const eventType = extractEventTypeFilterFromRule(rule);
  103. return (query ? `(${eventType}) AND (${query.trim()})` : eventType).split(' ');
  104. }
  105. const handleTimePeriodChange = (datetime: ChangeData) => {
  106. const {start, end, relative} = datetime;
  107. if (start && end) {
  108. return router.push({
  109. ...location,
  110. query: {
  111. start: moment(start).utc().format(),
  112. end: moment(end).utc().format(),
  113. },
  114. });
  115. }
  116. return router.push({
  117. ...location,
  118. query: {
  119. period: relative,
  120. },
  121. });
  122. };
  123. if (!rule || !project) {
  124. return (
  125. <Layout.Body>
  126. <Layout.Main>
  127. <Placeholder height="38px" />
  128. <ChartPanel>
  129. <PanelBody withPadding>
  130. <Placeholder height="200px" />
  131. </PanelBody>
  132. </ChartPanel>
  133. </Layout.Main>
  134. <Layout.Side>
  135. <Placeholder height="200px" />
  136. </Layout.Side>
  137. </Layout.Body>
  138. );
  139. }
  140. const {dataset, aggregate, query} = rule;
  141. const eventType = extractEventTypeFilterFromRule(rule);
  142. const queryWithTypeFilter =
  143. dataset === Dataset.EVENTS_ANALYTICS_PLATFORM
  144. ? query
  145. : (query ? `(${query}) AND (${eventType})` : eventType).trim();
  146. const relativeOptions = {
  147. ...SELECTOR_RELATIVE_PERIODS,
  148. ...(rule.timeWindow > 1 ? {[TimePeriod.FOURTEEN_DAYS]: t('Last 14 days')} : {}),
  149. ...(rule.detectionType === AlertRuleComparisonType.DYNAMIC
  150. ? {[TimePeriod.TWENTY_EIGHT_DAYS]: t('Last 28 days')}
  151. : {}),
  152. };
  153. const isSnoozed = rule.snooze;
  154. const ruleActionCategory = getAlertRuleActionCategory(rule);
  155. const showOnDemandMetricAlertUI =
  156. isOnDemandMetricAlert(dataset, aggregate, query) &&
  157. shouldShowOnDemandMetricAlertUI(organization);
  158. let formattedAggregate = aggregate;
  159. if (isCustomMetricAlert(aggregate)) {
  160. formattedAggregate = formatMRIField(aggregate);
  161. }
  162. return (
  163. <Fragment>
  164. {isCustomMetricAlert(rule.aggregate) && (
  165. <StyledLayoutBody>
  166. <MetricsBetaEndAlert style={{marginBottom: 0}} organization={organization} />
  167. </StyledLayoutBody>
  168. )}
  169. {selectedIncident?.alertRule.status === AlertRuleStatus.SNAPSHOT && (
  170. <StyledLayoutBody>
  171. <StyledAlert type="warning" showIcon>
  172. {t('Alert Rule settings have been updated since this alert was triggered.')}
  173. </StyledAlert>
  174. </StyledLayoutBody>
  175. )}
  176. <Layout.Body>
  177. <Layout.Main>
  178. {isSnoozed && (
  179. <Alert showIcon>
  180. {ruleActionCategory === RuleActionsCategories.NO_DEFAULT
  181. ? tct(
  182. "[creator] muted this alert so these notifications won't be sent in the future.",
  183. {creator: rule.snoozeCreatedBy}
  184. )
  185. : tct(
  186. "[creator] muted this alert[forEveryone]so you won't get these notifications in the future.",
  187. {
  188. creator: rule.snoozeCreatedBy,
  189. forEveryone: rule.snoozeForEveryone ? ' for everyone ' : ' ',
  190. }
  191. )}
  192. </Alert>
  193. )}
  194. <StyledSubHeader>
  195. <StyledTimeRangeSelector
  196. relative={timePeriod.period ?? ''}
  197. start={(timePeriod.custom && timePeriod.start) || null}
  198. end={(timePeriod.custom && timePeriod.end) || null}
  199. onChange={handleTimePeriodChange}
  200. relativeOptions={relativeOptions}
  201. showAbsolute={false}
  202. disallowArbitraryRelativeRanges
  203. triggerLabel={
  204. timePeriod.custom
  205. ? timePeriod.label
  206. : // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  207. relativeOptions[timePeriod.period ?? '']
  208. }
  209. />
  210. {selectedIncident && (
  211. <Tooltip
  212. title={`Click to clear filters`}
  213. isHoverable
  214. containerDisplayMode="inline-flex"
  215. >
  216. <Link
  217. to={{
  218. pathname: alertDetailsLink(organization, selectedIncident),
  219. }}
  220. >
  221. Remove filter on alert #{selectedIncident.identifier}
  222. </Link>
  223. </Tooltip>
  224. )}
  225. </StyledSubHeader>
  226. {selectedIncident?.alertRule.detectionType ===
  227. AlertRuleComparisonType.DYNAMIC && (
  228. <AnomalyDetectionFeedbackBanner
  229. // unique key to force re-render when incident changes
  230. key={selectedIncident.id}
  231. id={selectedIncident.id}
  232. organization={organization}
  233. selectedIncident={selectedIncident}
  234. />
  235. )}
  236. <ErrorMigrationWarning project={project} rule={rule} />
  237. <MetricChart
  238. api={api}
  239. rule={rule}
  240. incidents={incidents}
  241. anomalies={anomalies}
  242. timePeriod={timePeriod}
  243. formattedAggregate={formattedAggregate}
  244. organization={organization}
  245. project={project}
  246. interval={getPeriodInterval()}
  247. query={isCrashFreeAlert(dataset) ? query : queryWithTypeFilter}
  248. filter={getFilter()}
  249. isOnDemandAlert={isOnDemandMetricAlert(dataset, aggregate, query)}
  250. />
  251. <DetailWrapper>
  252. <ActivityWrapper>
  253. <MetricHistory incidents={incidents} />
  254. {[Dataset.METRICS, Dataset.SESSIONS, Dataset.ERRORS].includes(dataset) && (
  255. <RelatedIssues
  256. organization={organization}
  257. rule={rule}
  258. projects={[project]}
  259. timePeriod={timePeriod}
  260. query={
  261. dataset === Dataset.ERRORS
  262. ? // Not using (query) AND (event.type:x) because issues doesn't support it yet
  263. `${extractEventTypeFilterFromRule(rule)} ${query}`.trim()
  264. : isCrashFreeAlert(dataset)
  265. ? `${query} error.unhandled:true`.trim()
  266. : undefined
  267. }
  268. />
  269. )}
  270. {[Dataset.TRANSACTIONS, Dataset.GENERIC_METRICS].includes(dataset) && (
  271. <RelatedTransactions
  272. organization={organization}
  273. location={location}
  274. rule={rule}
  275. projects={[project]}
  276. timePeriod={timePeriod}
  277. filter={extractEventTypeFilterFromRule(rule)}
  278. />
  279. )}
  280. </ActivityWrapper>
  281. </DetailWrapper>
  282. </Layout.Main>
  283. <Layout.Side>
  284. <MetricDetailsSidebar
  285. rule={rule}
  286. showOnDemandMetricAlertUI={showOnDemandMetricAlertUI}
  287. />
  288. </Layout.Side>
  289. </Layout.Body>
  290. </Fragment>
  291. );
  292. }
  293. const DetailWrapper = styled('div')`
  294. display: flex;
  295. flex: 1;
  296. @media (max-width: ${p => p.theme.breakpoints.small}) {
  297. flex-direction: column-reverse;
  298. }
  299. `;
  300. const StyledLayoutBody = styled(Layout.Body)`
  301. flex-grow: 0;
  302. padding-bottom: 0 !important;
  303. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  304. grid-template-columns: auto;
  305. }
  306. `;
  307. const StyledAlert = styled(Alert)`
  308. margin: 0;
  309. `;
  310. const ActivityWrapper = styled('div')`
  311. display: flex;
  312. flex: 1;
  313. flex-direction: column;
  314. width: 100%;
  315. `;
  316. const ChartPanel = styled(Panel)`
  317. margin-top: ${space(2)};
  318. `;
  319. const StyledSubHeader = styled('div')`
  320. margin-bottom: ${space(2)};
  321. display: flex;
  322. align-items: center;
  323. `;
  324. const StyledTimeRangeSelector = styled(TimeRangeSelector)`
  325. margin-right: ${space(1)};
  326. `;