body.tsx 11 KB

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