body.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import {Component, Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import moment from 'moment';
  6. import {Client} from 'sentry/api';
  7. import {Alert} from 'sentry/components/alert';
  8. import {getInterval} from 'sentry/components/charts/utils';
  9. import Duration from 'sentry/components/duration';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import {ChangeData} from 'sentry/components/organizations/timeRangeSelector';
  12. import PageTimeRangeSelector from 'sentry/components/pageTimeRangeSelector';
  13. import Panel from 'sentry/components/panels/panel';
  14. import PanelBody from 'sentry/components/panels/panelBody';
  15. import Placeholder from 'sentry/components/placeholder';
  16. import {t, tct} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import {Organization, Project} from 'sentry/types';
  19. import {RuleActionsCategories} from 'sentry/types/alerts';
  20. import MetricHistory from 'sentry/views/alerts/rules/metric/details/metricHistory';
  21. import {Dataset, MetricRule, TimePeriod} from 'sentry/views/alerts/rules/metric/types';
  22. import {extractEventTypeFilterFromRule} from 'sentry/views/alerts/rules/metric/utils/getEventTypeFilter';
  23. import {getAlertRuleActionCategory} from 'sentry/views/alerts/rules/utils';
  24. import {AlertRuleStatus, Incident} from '../../../types';
  25. import {isCrashFreeAlert} from '../utils/isCrashFreeAlert';
  26. import {
  27. API_INTERVAL_POINTS_LIMIT,
  28. SELECTOR_RELATIVE_PERIODS,
  29. TIME_WINDOWS,
  30. TimePeriodType,
  31. } from './constants';
  32. import MetricChart from './metricChart';
  33. import RelatedIssues from './relatedIssues';
  34. import RelatedTransactions from './relatedTransactions';
  35. import Sidebar from './sidebar';
  36. type Props = {
  37. api: Client;
  38. location: Location;
  39. organization: Organization;
  40. timePeriod: TimePeriodType;
  41. incidents?: Incident[];
  42. project?: Project;
  43. rule?: MetricRule;
  44. selectedIncident?: Incident | null;
  45. } & RouteComponentProps<{}, {}>;
  46. export default class DetailsBody extends Component<Props> {
  47. getTimeWindow(): React.ReactNode {
  48. const {rule} = this.props;
  49. if (!rule) {
  50. return '';
  51. }
  52. const {timeWindow} = rule;
  53. return tct('[window]', {
  54. window: <Duration seconds={timeWindow * 60} />,
  55. });
  56. }
  57. getInterval() {
  58. const {
  59. timePeriod: {start, end},
  60. rule,
  61. } = this.props;
  62. const startDate = moment.utc(start);
  63. const endDate = moment.utc(end);
  64. const timeWindow = rule?.timeWindow;
  65. const startEndDifferenceMs = endDate.diff(startDate);
  66. if (
  67. timeWindow &&
  68. (startEndDifferenceMs < API_INTERVAL_POINTS_LIMIT * timeWindow * 60 * 1000 ||
  69. // Special case 7 days * 1m interval over the api limit
  70. startEndDifferenceMs === TIME_WINDOWS[TimePeriod.SEVEN_DAYS])
  71. ) {
  72. return `${timeWindow}m`;
  73. }
  74. return getInterval({start, end}, 'high');
  75. }
  76. getFilter() {
  77. const {rule} = this.props;
  78. const {dataset, query} = rule ?? {};
  79. if (!rule) {
  80. return null;
  81. }
  82. const eventType = isCrashFreeAlert(dataset)
  83. ? null
  84. : extractEventTypeFilterFromRule(rule);
  85. return [eventType, query].join(' ').split(' ');
  86. }
  87. handleTimePeriodChange = (datetime: ChangeData) => {
  88. const {start, end, relative} = datetime;
  89. if (start && end) {
  90. return this.props.router.push({
  91. ...this.props.location,
  92. query: {
  93. start: moment(start).utc().format(),
  94. end: moment(end).utc().format(),
  95. },
  96. });
  97. }
  98. return this.props.router.push({
  99. ...this.props.location,
  100. query: {
  101. period: relative,
  102. },
  103. });
  104. };
  105. renderLoading() {
  106. return (
  107. <Layout.Body>
  108. <Layout.Main>
  109. <Placeholder height="38px" />
  110. <ChartPanel>
  111. <PanelBody withPadding>
  112. <Placeholder height="200px" />
  113. </PanelBody>
  114. </ChartPanel>
  115. </Layout.Main>
  116. <Layout.Side>
  117. <Placeholder height="200px" />
  118. </Layout.Side>
  119. </Layout.Body>
  120. );
  121. }
  122. render() {
  123. const {
  124. api,
  125. project,
  126. rule,
  127. incidents,
  128. location,
  129. organization,
  130. timePeriod,
  131. selectedIncident,
  132. } = this.props;
  133. if (!rule || !project) {
  134. return this.renderLoading();
  135. }
  136. const {query, dataset} = rule;
  137. const queryWithTypeFilter = `${query} ${extractEventTypeFilterFromRule(rule)}`.trim();
  138. const relativeOptions = {
  139. ...SELECTOR_RELATIVE_PERIODS,
  140. ...(rule.timeWindow > 1 ? {[TimePeriod.FOURTEEN_DAYS]: t('Last 14 days')} : {}),
  141. };
  142. const isSnoozed = rule.snooze;
  143. const ruleActionCategory = getAlertRuleActionCategory(rule);
  144. return (
  145. <Fragment>
  146. {selectedIncident &&
  147. selectedIncident.alertRule.status === AlertRuleStatus.SNAPSHOT && (
  148. <StyledLayoutBody>
  149. <StyledAlert type="warning" showIcon>
  150. {t(
  151. 'Alert Rule settings have been updated since this alert was triggered.'
  152. )}
  153. </StyledAlert>
  154. </StyledLayoutBody>
  155. )}
  156. <Layout.Body>
  157. <Layout.Main>
  158. {isSnoozed && (
  159. <Alert showIcon>
  160. {ruleActionCategory === RuleActionsCategories.NO_DEFAULT
  161. ? tct(
  162. "[creator] muted this alert so these notifications won't be sent in the future.",
  163. {creator: rule.snoozeCreatedBy}
  164. )
  165. : tct(
  166. "[creator] muted this alert[forEveryone]so you won't get these notifications in the future.",
  167. {
  168. creator: rule.snoozeCreatedBy,
  169. forEveryone: rule.snoozeForEveryone ? ' for everyone ' : ' ',
  170. }
  171. )}
  172. </Alert>
  173. )}
  174. <StyledPageTimeRangeSelector
  175. organization={organization}
  176. relative={timePeriod.period ?? ''}
  177. start={(timePeriod.custom && timePeriod.start) || null}
  178. end={(timePeriod.custom && timePeriod.end) || null}
  179. utc={null}
  180. onUpdate={this.handleTimePeriodChange}
  181. relativeOptions={relativeOptions}
  182. showAbsolute={false}
  183. />
  184. <MetricChart
  185. api={api}
  186. rule={rule}
  187. incidents={incidents}
  188. timePeriod={timePeriod}
  189. selectedIncident={selectedIncident}
  190. organization={organization}
  191. project={project}
  192. interval={this.getInterval()}
  193. query={isCrashFreeAlert(dataset) ? query : queryWithTypeFilter}
  194. filter={this.getFilter()}
  195. />
  196. <DetailWrapper>
  197. <ActivityWrapper>
  198. <MetricHistory incidents={incidents} />
  199. {[Dataset.METRICS, Dataset.SESSIONS, Dataset.ERRORS].includes(
  200. dataset
  201. ) && (
  202. <RelatedIssues
  203. organization={organization}
  204. rule={rule}
  205. projects={[project]}
  206. timePeriod={timePeriod}
  207. query={
  208. dataset === Dataset.ERRORS
  209. ? queryWithTypeFilter
  210. : isCrashFreeAlert(dataset)
  211. ? `${query} error.unhandled:true`
  212. : undefined
  213. }
  214. />
  215. )}
  216. {dataset === Dataset.TRANSACTIONS && (
  217. <RelatedTransactions
  218. organization={organization}
  219. location={location}
  220. rule={rule}
  221. projects={[project]}
  222. timePeriod={timePeriod}
  223. filter={extractEventTypeFilterFromRule(rule)}
  224. />
  225. )}
  226. </ActivityWrapper>
  227. </DetailWrapper>
  228. </Layout.Main>
  229. <Layout.Side>
  230. <Sidebar rule={rule} />
  231. </Layout.Side>
  232. </Layout.Body>
  233. </Fragment>
  234. );
  235. }
  236. }
  237. const DetailWrapper = styled('div')`
  238. display: flex;
  239. flex: 1;
  240. @media (max-width: ${p => p.theme.breakpoints.small}) {
  241. flex-direction: column-reverse;
  242. }
  243. `;
  244. const StyledLayoutBody = styled(Layout.Body)`
  245. flex-grow: 0;
  246. padding-bottom: 0 !important;
  247. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  248. grid-template-columns: auto;
  249. }
  250. `;
  251. const StyledAlert = styled(Alert)`
  252. margin: 0;
  253. `;
  254. const ActivityWrapper = styled('div')`
  255. display: flex;
  256. flex: 1;
  257. flex-direction: column;
  258. width: 100%;
  259. `;
  260. const ChartPanel = styled(Panel)`
  261. margin-top: ${space(2)};
  262. `;
  263. const StyledPageTimeRangeSelector = styled(PageTimeRangeSelector)`
  264. margin-bottom: ${space(2)};
  265. `;