index.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825
  1. import {type ComponentProps, Fragment, PureComponent} from 'react';
  2. import React from 'react';
  3. import styled from '@emotion/styled';
  4. import type {Location} from 'history';
  5. import isEqual from 'lodash/isEqual';
  6. import maxBy from 'lodash/maxBy';
  7. import minBy from 'lodash/minBy';
  8. import {fetchTotalCount} from 'sentry/actionCreators/events';
  9. import {Client} from 'sentry/api';
  10. import ErrorPanel from 'sentry/components/charts/errorPanel';
  11. import EventsRequest, {
  12. type EventsRequestProps,
  13. } from 'sentry/components/charts/eventsRequest';
  14. import type {LineChartSeries} from 'sentry/components/charts/lineChart';
  15. import {OnDemandMetricRequest} from 'sentry/components/charts/onDemandMetricRequest';
  16. import SessionsRequest from 'sentry/components/charts/sessionsRequest';
  17. import {
  18. ChartControls,
  19. InlineContainer,
  20. SectionHeading,
  21. SectionValue,
  22. } from 'sentry/components/charts/styles';
  23. import {CompactSelect} from 'sentry/components/compactSelect';
  24. import LoadingMask from 'sentry/components/loadingMask';
  25. import PanelAlert from 'sentry/components/panels/panelAlert';
  26. import Placeholder from 'sentry/components/placeholder';
  27. import {IconWarning} from 'sentry/icons';
  28. import {t} from 'sentry/locale';
  29. import {space} from 'sentry/styles/space';
  30. import type {Series} from 'sentry/types/echarts';
  31. import type {
  32. Confidence,
  33. EventsStats,
  34. MultiSeriesEventsStats,
  35. NewQuery,
  36. Organization,
  37. } from 'sentry/types/organization';
  38. import type {Project} from 'sentry/types/project';
  39. import type {TableData} from 'sentry/utils/discover/discoverQuery';
  40. import EventView from 'sentry/utils/discover/eventView';
  41. import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
  42. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  43. import {parsePeriodToHours} from 'sentry/utils/duration/parsePeriodToHours';
  44. import {shouldShowOnDemandMetricAlertUI} from 'sentry/utils/onDemandMetrics/features';
  45. import {
  46. getCrashFreeRateSeries,
  47. MINUTES_THRESHOLD_TO_DISPLAY_SECONDS,
  48. } from 'sentry/utils/sessions';
  49. import {capitalize} from 'sentry/utils/string/capitalize';
  50. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  51. import withApi from 'sentry/utils/withApi';
  52. import {COMPARISON_DELTA_OPTIONS} from 'sentry/views/alerts/rules/metric/constants';
  53. import {shouldUseErrorsDiscoverDataset} from 'sentry/views/alerts/rules/utils';
  54. import type {Anomaly} from 'sentry/views/alerts/types';
  55. import {isSessionAggregate, SESSION_AGGREGATE_TO_FIELD} from 'sentry/views/alerts/utils';
  56. import {getComparisonMarkLines} from 'sentry/views/alerts/utils/getComparisonMarkLines';
  57. import {AlertWizardAlertNames} from 'sentry/views/alerts/wizard/options';
  58. import {getAlertTypeFromAggregateDataset} from 'sentry/views/alerts/wizard/utils';
  59. import {ConfidenceFooter} from 'sentry/views/explore/charts/confidenceFooter';
  60. import {showConfidence} from 'sentry/views/explore/utils';
  61. import type {MetricRule, Trigger} from '../../types';
  62. import {
  63. AlertRuleComparisonType,
  64. Dataset,
  65. SessionsAggregate,
  66. TimePeriod,
  67. TimeWindow,
  68. } from '../../types';
  69. import {getMetricDatasetQueryExtras} from '../../utils/getMetricDatasetQueryExtras';
  70. import ThresholdsChart from './thresholdsChart';
  71. type Props = {
  72. aggregate: MetricRule['aggregate'];
  73. api: Client;
  74. comparisonType: AlertRuleComparisonType;
  75. dataset: MetricRule['dataset'];
  76. environment: string | null;
  77. isQueryValid: boolean;
  78. location: Location;
  79. newAlertOrQuery: boolean;
  80. organization: Organization;
  81. projects: Project[];
  82. query: MetricRule['query'];
  83. resolveThreshold: MetricRule['resolveThreshold'];
  84. thresholdType: MetricRule['thresholdType'];
  85. timeWindow: MetricRule['timeWindow'];
  86. triggers: Trigger[];
  87. anomalies?: Anomaly[];
  88. comparisonDelta?: number;
  89. confidence?: Confidence;
  90. formattedAggregate?: string;
  91. header?: React.ReactNode;
  92. includeConfidence?: boolean;
  93. includeHistorical?: boolean;
  94. isOnDemandMetricAlert?: boolean;
  95. isSampled?: boolean | null;
  96. onConfidenceDataLoaded?: (data: EventsStats | MultiSeriesEventsStats | null) => void;
  97. onDataLoaded?: (data: EventsStats | MultiSeriesEventsStats | null) => void;
  98. onHistoricalDataLoaded?: (data: EventsStats | MultiSeriesEventsStats | null) => void;
  99. showTotalCount?: boolean;
  100. };
  101. type TimePeriodMap = Omit<Record<TimePeriod, string>, TimePeriod.TWENTY_EIGHT_DAYS>;
  102. const TIME_PERIOD_MAP: TimePeriodMap = {
  103. [TimePeriod.SIX_HOURS]: t('Last 6 hours'),
  104. [TimePeriod.ONE_DAY]: t('Last 24 hours'),
  105. [TimePeriod.THREE_DAYS]: t('Last 3 days'),
  106. [TimePeriod.SEVEN_DAYS]: t('Last 7 days'),
  107. [TimePeriod.FOURTEEN_DAYS]: t('Last 14 days'),
  108. };
  109. /**
  110. * Just to avoid repeating it
  111. */
  112. const MOST_TIME_PERIODS: readonly TimePeriod[] = [
  113. TimePeriod.ONE_DAY,
  114. TimePeriod.THREE_DAYS,
  115. TimePeriod.SEVEN_DAYS,
  116. TimePeriod.FOURTEEN_DAYS,
  117. ];
  118. /**
  119. * TimeWindow determines data available in TimePeriod
  120. * If TimeWindow is small, lower TimePeriod to limit data points
  121. */
  122. export const AVAILABLE_TIME_PERIODS: Record<TimeWindow, readonly TimePeriod[]> = {
  123. [TimeWindow.ONE_MINUTE]: [
  124. TimePeriod.SIX_HOURS,
  125. TimePeriod.ONE_DAY,
  126. TimePeriod.THREE_DAYS,
  127. TimePeriod.SEVEN_DAYS,
  128. ],
  129. [TimeWindow.FIVE_MINUTES]: MOST_TIME_PERIODS,
  130. [TimeWindow.TEN_MINUTES]: MOST_TIME_PERIODS,
  131. [TimeWindow.FIFTEEN_MINUTES]: MOST_TIME_PERIODS,
  132. [TimeWindow.THIRTY_MINUTES]: MOST_TIME_PERIODS,
  133. [TimeWindow.ONE_HOUR]: MOST_TIME_PERIODS,
  134. [TimeWindow.TWO_HOURS]: MOST_TIME_PERIODS,
  135. [TimeWindow.FOUR_HOURS]: [
  136. TimePeriod.THREE_DAYS,
  137. TimePeriod.SEVEN_DAYS,
  138. TimePeriod.FOURTEEN_DAYS,
  139. ],
  140. [TimeWindow.ONE_DAY]: [TimePeriod.FOURTEEN_DAYS],
  141. };
  142. const MOST_EAP_TIME_PERIOD = [
  143. TimePeriod.ONE_DAY,
  144. TimePeriod.THREE_DAYS,
  145. TimePeriod.SEVEN_DAYS,
  146. ];
  147. const EAP_AVAILABLE_TIME_PERIODS = {
  148. [TimeWindow.ONE_MINUTE]: [], // One minute intervals are not allowed on EAP Alerts
  149. [TimeWindow.FIVE_MINUTES]: MOST_EAP_TIME_PERIOD,
  150. [TimeWindow.TEN_MINUTES]: MOST_EAP_TIME_PERIOD,
  151. [TimeWindow.FIFTEEN_MINUTES]: MOST_EAP_TIME_PERIOD,
  152. [TimeWindow.THIRTY_MINUTES]: MOST_EAP_TIME_PERIOD,
  153. [TimeWindow.ONE_HOUR]: MOST_EAP_TIME_PERIOD,
  154. [TimeWindow.TWO_HOURS]: MOST_EAP_TIME_PERIOD,
  155. [TimeWindow.FOUR_HOURS]: [TimePeriod.SEVEN_DAYS],
  156. [TimeWindow.ONE_DAY]: [TimePeriod.SEVEN_DAYS],
  157. };
  158. export const TIME_WINDOW_TO_INTERVAL = {
  159. [TimeWindow.FIVE_MINUTES]: '5m',
  160. [TimeWindow.TEN_MINUTES]: '10m',
  161. [TimeWindow.FIFTEEN_MINUTES]: '15m',
  162. [TimeWindow.THIRTY_MINUTES]: '30m',
  163. [TimeWindow.ONE_HOUR]: '1h',
  164. [TimeWindow.TWO_HOURS]: '2h',
  165. [TimeWindow.FOUR_HOURS]: '4h',
  166. [TimeWindow.ONE_DAY]: '1d',
  167. };
  168. const SESSION_AGGREGATE_TO_HEADING = {
  169. [SessionsAggregate.CRASH_FREE_SESSIONS]: t('Total Sessions'),
  170. [SessionsAggregate.CRASH_FREE_USERS]: t('Total Users'),
  171. };
  172. const HISTORICAL_TIME_PERIOD_MAP: TimePeriodMap = {
  173. [TimePeriod.SIX_HOURS]: '678h',
  174. [TimePeriod.ONE_DAY]: '29d',
  175. [TimePeriod.THREE_DAYS]: '31d',
  176. [TimePeriod.SEVEN_DAYS]: '35d',
  177. [TimePeriod.FOURTEEN_DAYS]: '42d',
  178. };
  179. const HISTORICAL_TIME_PERIOD_MAP_FIVE_MINS: TimePeriodMap = {
  180. ...HISTORICAL_TIME_PERIOD_MAP,
  181. [TimePeriod.SEVEN_DAYS]: '28d', // fetching 28 + 7 days of historical data at 5 minute increments exceeds the max number of data points that snuba can return
  182. [TimePeriod.FOURTEEN_DAYS]: '28d', // fetching 28 + 14 days of historical data at 5 minute increments exceeds the max number of data points that snuba can return
  183. };
  184. const noop: any = () => {};
  185. type State = {
  186. extrapolationSampleCount: number | null;
  187. sampleRate: number;
  188. statsPeriod: TimePeriod;
  189. totalCount: number | null;
  190. };
  191. const getStatsPeriodFromQuery = (
  192. queryParam: string | string[] | null | undefined
  193. ): TimePeriod => {
  194. if (typeof queryParam !== 'string') {
  195. return TimePeriod.SEVEN_DAYS;
  196. }
  197. const inMinutes = parsePeriodToHours(queryParam || '') * 60;
  198. switch (inMinutes) {
  199. case 6 * 60:
  200. return TimePeriod.SIX_HOURS;
  201. case 24 * 60:
  202. return TimePeriod.ONE_DAY;
  203. case 3 * 24 * 60:
  204. return TimePeriod.THREE_DAYS;
  205. case 9999:
  206. return TimePeriod.SEVEN_DAYS;
  207. case 14 * 24 * 60:
  208. return TimePeriod.FOURTEEN_DAYS;
  209. default:
  210. return TimePeriod.SEVEN_DAYS;
  211. }
  212. };
  213. /**
  214. * This is a chart to be used in Metric Alert rules that fetches events based on
  215. * query, timewindow, and aggregations.
  216. */
  217. class TriggersChart extends PureComponent<Props, State> {
  218. state: State = {
  219. statsPeriod: getStatsPeriodFromQuery(this.props.location.query.statsPeriod),
  220. totalCount: null,
  221. sampleRate: 1,
  222. extrapolationSampleCount: null,
  223. };
  224. componentDidMount() {
  225. const {aggregate, showTotalCount} = this.props;
  226. if (showTotalCount && !isSessionAggregate(aggregate)) {
  227. this.fetchTotalCount();
  228. }
  229. if (this.props.dataset === Dataset.EVENTS_ANALYTICS_PLATFORM) {
  230. this.fetchExtrapolationSampleCount();
  231. }
  232. }
  233. componentDidUpdate(prevProps: Props, prevState: State) {
  234. const {query, environment, timeWindow, aggregate, projects, showTotalCount} =
  235. this.props;
  236. const {statsPeriod} = this.state;
  237. if (
  238. !isEqual(prevProps.projects, projects) ||
  239. prevProps.environment !== environment ||
  240. prevProps.query !== query ||
  241. !isEqual(prevProps.timeWindow, timeWindow) ||
  242. !isEqual(prevState.statsPeriod, statsPeriod)
  243. ) {
  244. if (showTotalCount && !isSessionAggregate(aggregate)) {
  245. this.fetchTotalCount();
  246. }
  247. if (this.props.dataset === Dataset.EVENTS_ANALYTICS_PLATFORM) {
  248. this.fetchExtrapolationSampleCount();
  249. }
  250. }
  251. }
  252. // Create new API Client so that historical requests aren't automatically deduplicated
  253. historicalAPI = new Client();
  254. confidenceAPI = new Client();
  255. get availableTimePeriods() {
  256. // We need to special case sessions, because sub-hour windows are available
  257. // only when time period is six hours or less (backend limitation)
  258. if (isSessionAggregate(this.props.aggregate)) {
  259. return {
  260. ...AVAILABLE_TIME_PERIODS,
  261. [TimeWindow.THIRTY_MINUTES]: [TimePeriod.SIX_HOURS],
  262. };
  263. }
  264. if (this.props.dataset === Dataset.EVENTS_ANALYTICS_PLATFORM) {
  265. return EAP_AVAILABLE_TIME_PERIODS;
  266. }
  267. return AVAILABLE_TIME_PERIODS;
  268. }
  269. handleStatsPeriodChange = (timePeriod: TimePeriod) => {
  270. this.setState({statsPeriod: timePeriod});
  271. };
  272. getStatsPeriod = () => {
  273. const {statsPeriod} = this.state;
  274. const {timeWindow} = this.props;
  275. const statsPeriodOptions = this.availableTimePeriods[timeWindow];
  276. const period = statsPeriodOptions.includes(statsPeriod)
  277. ? statsPeriod
  278. : statsPeriodOptions[statsPeriodOptions.length - 1];
  279. return period;
  280. };
  281. get comparisonSeriesName() {
  282. return capitalize(
  283. COMPARISON_DELTA_OPTIONS.find(({value}) => value === this.props.comparisonDelta)
  284. ?.label || ''
  285. );
  286. }
  287. async fetchTotalCount() {
  288. const {
  289. api,
  290. organization,
  291. location,
  292. newAlertOrQuery,
  293. environment,
  294. projects,
  295. query,
  296. dataset,
  297. } = this.props;
  298. const statsPeriod = this.getStatsPeriod();
  299. const queryExtras = getMetricDatasetQueryExtras({
  300. organization,
  301. location,
  302. dataset,
  303. newAlertOrQuery,
  304. });
  305. let queryDataset = queryExtras.dataset as undefined | DiscoverDatasets;
  306. const queryOverride = (queryExtras.query as string | undefined) ?? query;
  307. if (shouldUseErrorsDiscoverDataset(query, dataset, organization)) {
  308. queryDataset = DiscoverDatasets.ERRORS;
  309. }
  310. try {
  311. const totalCount = await fetchTotalCount(api, organization.slug, {
  312. field: [],
  313. project: projects.map(({id}) => id),
  314. query: queryOverride,
  315. statsPeriod,
  316. environment: environment ? [environment] : [],
  317. dataset: queryDataset,
  318. });
  319. this.setState({totalCount});
  320. } catch (e) {
  321. this.setState({totalCount: null});
  322. }
  323. }
  324. async fetchExtrapolationSampleCount() {
  325. const {location, api, organization, environment, projects, query} = this.props;
  326. const search = new MutableSearch(query);
  327. // Filtering out all spans with op like 'ui.interaction*' which aren't
  328. // embedded under transactions. The trace view does not support rendering
  329. // such spans yet.
  330. search.addFilterValues('!transaction.span_id', ['00']);
  331. const discoverQuery: NewQuery = {
  332. id: undefined,
  333. name: 'Alerts - Extrapolation Meta',
  334. fields: ['count_sample()', 'min(sampling_rate)'],
  335. query: search.formatString(),
  336. version: 2,
  337. dataset: DiscoverDatasets.SPANS_EAP_RPC,
  338. };
  339. const eventView = EventView.fromNewQueryWithPageFilters(discoverQuery, {
  340. datetime: {
  341. period: TimePeriod.SEVEN_DAYS,
  342. start: null,
  343. end: null,
  344. utc: false,
  345. },
  346. environments: environment ? [environment] : [],
  347. projects: projects.map(({id}) => Number(id)),
  348. });
  349. const response = await doDiscoverQuery<TableData>(
  350. api,
  351. `/organizations/${organization.slug}/events/`,
  352. eventView.getEventsAPIPayload(location)
  353. );
  354. const extrapolationSampleCount = response[0]?.data?.[0]?.['count_sample()'];
  355. this.setState({
  356. extrapolationSampleCount: extrapolationSampleCount
  357. ? Number(extrapolationSampleCount)
  358. : null,
  359. });
  360. }
  361. renderChart({
  362. isLoading,
  363. isReloading,
  364. timeseriesData = [],
  365. comparisonData,
  366. comparisonMarkLines,
  367. errorMessage,
  368. minutesThresholdToDisplaySeconds,
  369. isQueryValid,
  370. errored,
  371. orgFeatures,
  372. seriesAdditionalInfo,
  373. }: {
  374. isLoading: boolean;
  375. isQueryValid: boolean;
  376. isReloading: boolean;
  377. orgFeatures: string[];
  378. timeseriesData: Series[];
  379. comparisonData?: Series[];
  380. comparisonMarkLines?: LineChartSeries[];
  381. errorMessage?: string;
  382. errored?: boolean;
  383. minutesThresholdToDisplaySeconds?: number;
  384. seriesAdditionalInfo?: Record<string, any>;
  385. }) {
  386. const {
  387. triggers,
  388. resolveThreshold,
  389. thresholdType,
  390. header,
  391. timeWindow,
  392. aggregate,
  393. comparisonType,
  394. organization,
  395. showTotalCount,
  396. anomalies = [],
  397. confidence,
  398. dataset,
  399. } = this.props;
  400. const {statsPeriod, totalCount, extrapolationSampleCount} = this.state;
  401. const statsPeriodOptions = this.availableTimePeriods[timeWindow];
  402. const period = this.getStatsPeriod();
  403. const error = orgFeatures.includes('alert-allow-indexed')
  404. ? errored || errorMessage
  405. : errored || errorMessage || !isQueryValid;
  406. const showExtrapolatedChartData =
  407. shouldShowOnDemandMetricAlertUI(organization) &&
  408. seriesAdditionalInfo?.[timeseriesData[0]?.seriesName!]?.isExtrapolatedData;
  409. const totalCountLabel = isSessionAggregate(aggregate)
  410. ? // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  411. SESSION_AGGREGATE_TO_HEADING[aggregate]
  412. : showExtrapolatedChartData
  413. ? t('Estimated Transactions')
  414. : t('Total');
  415. return (
  416. <Fragment>
  417. {header}
  418. <TransparentLoadingMask visible={isReloading} />
  419. {isLoading && !error ? (
  420. <ChartPlaceholder />
  421. ) : error ? (
  422. <ErrorChart
  423. isAllowIndexed={orgFeatures.includes('alert-allow-indexed')}
  424. errorMessage={errorMessage}
  425. isQueryValid={isQueryValid}
  426. />
  427. ) : (
  428. <ThresholdsChart
  429. period={statsPeriod}
  430. minValue={minBy(timeseriesData[0]?.data, ({value}) => value)?.value}
  431. maxValue={maxBy(timeseriesData[0]?.data, ({value}) => value)?.value}
  432. data={timeseriesData}
  433. comparisonData={comparisonData ?? []}
  434. comparisonSeriesName={this.comparisonSeriesName}
  435. comparisonMarkLines={comparisonMarkLines ?? []}
  436. hideThresholdLines={comparisonType !== AlertRuleComparisonType.COUNT}
  437. triggers={triggers}
  438. anomalies={anomalies}
  439. resolveThreshold={resolveThreshold}
  440. thresholdType={thresholdType}
  441. aggregate={aggregate}
  442. minutesThresholdToDisplaySeconds={minutesThresholdToDisplaySeconds}
  443. isExtrapolatedData={showExtrapolatedChartData}
  444. />
  445. )}
  446. <ChartControls>
  447. {showTotalCount ? (
  448. <InlineContainer data-test-id="alert-total-events">
  449. {dataset === Dataset.EVENTS_ANALYTICS_PLATFORM &&
  450. showConfidence(this.props.isSampled) ? (
  451. <ConfidenceFooter
  452. sampleCount={extrapolationSampleCount ?? undefined}
  453. confidence={confidence}
  454. />
  455. ) : (
  456. <React.Fragment>
  457. <SectionHeading>{totalCountLabel}</SectionHeading>
  458. <SectionValue>
  459. {totalCount !== null ? totalCount.toLocaleString() : '\u2014'}
  460. </SectionValue>
  461. </React.Fragment>
  462. )}
  463. </InlineContainer>
  464. ) : (
  465. <InlineContainer />
  466. )}
  467. <InlineContainer>
  468. <CompactSelect
  469. size="sm"
  470. options={statsPeriodOptions.map(timePeriod => ({
  471. value: timePeriod,
  472. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  473. label: TIME_PERIOD_MAP[timePeriod],
  474. }))}
  475. value={period}
  476. onChange={opt => this.handleStatsPeriodChange(opt.value)}
  477. position="bottom-end"
  478. triggerProps={{
  479. borderless: true,
  480. prefix: t('Display'),
  481. }}
  482. />
  483. </InlineContainer>
  484. </ChartControls>
  485. </Fragment>
  486. );
  487. }
  488. render() {
  489. const {
  490. api,
  491. organization,
  492. projects,
  493. timeWindow,
  494. query,
  495. location,
  496. aggregate,
  497. dataset,
  498. newAlertOrQuery,
  499. onDataLoaded,
  500. onHistoricalDataLoaded,
  501. environment,
  502. formattedAggregate,
  503. comparisonDelta,
  504. triggers,
  505. thresholdType,
  506. isQueryValid,
  507. isOnDemandMetricAlert,
  508. onConfidenceDataLoaded,
  509. } = this.props;
  510. const period = this.getStatsPeriod()!;
  511. const renderComparisonStats = Boolean(
  512. organization.features.includes('change-alerts') && comparisonDelta
  513. );
  514. const queryExtras = getMetricDatasetQueryExtras({
  515. organization,
  516. location,
  517. dataset,
  518. query,
  519. newAlertOrQuery,
  520. });
  521. if (isOnDemandMetricAlert) {
  522. const {sampleRate} = this.state;
  523. const baseProps: EventsRequestProps = {
  524. api,
  525. organization,
  526. query,
  527. queryExtras,
  528. sampleRate,
  529. period,
  530. environment: environment ? [environment] : undefined,
  531. project: projects.map(({id}) => Number(id)),
  532. interval: `${timeWindow}m`,
  533. comparisonDelta: comparisonDelta ? comparisonDelta * 60 : undefined,
  534. yAxis: aggregate,
  535. includePrevious: false,
  536. currentSeriesNames: [formattedAggregate || aggregate],
  537. partial: false,
  538. limit: 15,
  539. children: noop,
  540. };
  541. return (
  542. <Fragment>
  543. {this.props.includeHistorical ? (
  544. <OnDemandMetricRequest
  545. {...baseProps}
  546. api={this.historicalAPI}
  547. period={
  548. timeWindow === 5
  549. ? // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  550. HISTORICAL_TIME_PERIOD_MAP_FIVE_MINS[period]!
  551. : // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  552. HISTORICAL_TIME_PERIOD_MAP[period]!
  553. }
  554. dataLoadedCallback={onHistoricalDataLoaded}
  555. />
  556. ) : null}
  557. <OnDemandMetricRequest {...baseProps} dataLoadedCallback={onDataLoaded}>
  558. {({
  559. loading,
  560. errored,
  561. errorMessage,
  562. reloading,
  563. timeseriesData,
  564. comparisonTimeseriesData,
  565. seriesAdditionalInfo,
  566. }) => {
  567. let comparisonMarkLines: LineChartSeries[] = [];
  568. if (renderComparisonStats && comparisonTimeseriesData) {
  569. comparisonMarkLines = getComparisonMarkLines(
  570. timeseriesData,
  571. comparisonTimeseriesData,
  572. timeWindow,
  573. triggers,
  574. thresholdType
  575. );
  576. }
  577. return this.renderChart({
  578. timeseriesData: timeseriesData as Series[],
  579. isLoading: loading,
  580. isReloading: reloading,
  581. comparisonData: comparisonTimeseriesData,
  582. comparisonMarkLines,
  583. errorMessage,
  584. isQueryValid,
  585. errored,
  586. orgFeatures: organization.features,
  587. seriesAdditionalInfo,
  588. });
  589. }}
  590. </OnDemandMetricRequest>
  591. </Fragment>
  592. );
  593. }
  594. if (isSessionAggregate(aggregate)) {
  595. const baseProps: ComponentProps<typeof SessionsRequest> = {
  596. api,
  597. organization,
  598. project: projects.map(({id}) => Number(id)),
  599. environment: environment ? [environment] : undefined,
  600. statsPeriod: period,
  601. query,
  602. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  603. interval: TIME_WINDOW_TO_INTERVAL[timeWindow],
  604. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  605. field: SESSION_AGGREGATE_TO_FIELD[aggregate],
  606. groupBy: ['session.status'],
  607. children: noop,
  608. };
  609. return (
  610. <SessionsRequest {...baseProps}>
  611. {({loading, errored, reloading, response}) => {
  612. const {groups, intervals} = response || {};
  613. const sessionTimeSeries = [
  614. {
  615. seriesName:
  616. AlertWizardAlertNames[
  617. getAlertTypeFromAggregateDataset({
  618. aggregate,
  619. dataset: Dataset.SESSIONS,
  620. })
  621. ],
  622. data: getCrashFreeRateSeries(
  623. groups,
  624. intervals,
  625. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  626. SESSION_AGGREGATE_TO_FIELD[aggregate]
  627. ),
  628. },
  629. ];
  630. return this.renderChart({
  631. timeseriesData: sessionTimeSeries,
  632. isLoading: loading,
  633. isReloading: reloading,
  634. comparisonData: undefined,
  635. comparisonMarkLines: undefined,
  636. minutesThresholdToDisplaySeconds: MINUTES_THRESHOLD_TO_DISPLAY_SECONDS,
  637. isQueryValid,
  638. errored,
  639. orgFeatures: organization.features,
  640. });
  641. }}
  642. </SessionsRequest>
  643. );
  644. }
  645. const useRpc = dataset === Dataset.EVENTS_ANALYTICS_PLATFORM;
  646. const baseProps = {
  647. api,
  648. organization,
  649. query,
  650. period,
  651. queryExtras,
  652. environment: environment ? [environment] : undefined,
  653. project: projects.map(({id}) => Number(id)),
  654. interval: `${timeWindow}m`,
  655. comparisonDelta: comparisonDelta ? comparisonDelta * 60 : undefined,
  656. yAxis: aggregate,
  657. includePrevious: false,
  658. currentSeriesNames: [formattedAggregate || aggregate],
  659. partial: false,
  660. useRpc,
  661. };
  662. return (
  663. <Fragment>
  664. {this.props.includeHistorical ? (
  665. <EventsRequest
  666. {...baseProps}
  667. api={this.historicalAPI}
  668. period={
  669. timeWindow === 5
  670. ? // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  671. HISTORICAL_TIME_PERIOD_MAP_FIVE_MINS[period]!
  672. : // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  673. HISTORICAL_TIME_PERIOD_MAP[period]!
  674. }
  675. dataLoadedCallback={onHistoricalDataLoaded}
  676. >
  677. {noop}
  678. </EventsRequest>
  679. ) : null}
  680. {this.props.includeConfidence ? (
  681. <EventsRequest
  682. {...baseProps}
  683. api={this.confidenceAPI}
  684. period={TimePeriod.SEVEN_DAYS}
  685. dataLoadedCallback={onConfidenceDataLoaded}
  686. >
  687. {noop}
  688. </EventsRequest>
  689. ) : null}
  690. <EventsRequest {...baseProps} period={period} dataLoadedCallback={onDataLoaded}>
  691. {({
  692. loading,
  693. errored,
  694. errorMessage,
  695. reloading,
  696. timeseriesData,
  697. comparisonTimeseriesData,
  698. }) => {
  699. let comparisonMarkLines: LineChartSeries[] = [];
  700. if (renderComparisonStats && comparisonTimeseriesData) {
  701. comparisonMarkLines = getComparisonMarkLines(
  702. timeseriesData,
  703. comparisonTimeseriesData,
  704. timeWindow,
  705. triggers,
  706. thresholdType
  707. );
  708. }
  709. return this.renderChart({
  710. timeseriesData: timeseriesData as Series[],
  711. isLoading: loading,
  712. isReloading: reloading,
  713. comparisonData: comparisonTimeseriesData,
  714. comparisonMarkLines,
  715. errorMessage,
  716. isQueryValid,
  717. errored,
  718. orgFeatures: organization.features,
  719. });
  720. }}
  721. </EventsRequest>
  722. </Fragment>
  723. );
  724. }
  725. }
  726. export default withApi(TriggersChart);
  727. const TransparentLoadingMask = styled(LoadingMask)<{visible: boolean}>`
  728. ${p => !p.visible && 'display: none;'};
  729. opacity: 0.4;
  730. z-index: 1;
  731. `;
  732. const ChartPlaceholder = styled(Placeholder)`
  733. /* Height and margin should add up to graph size (200px) */
  734. margin: 0 0 ${space(2)};
  735. height: 184px;
  736. `;
  737. const StyledErrorPanel = styled(ErrorPanel)`
  738. /* Height and margin should with the alert should match up placeholder height of (184px) */
  739. padding: ${space(2)};
  740. height: 119px;
  741. `;
  742. const ChartErrorWrapper = styled('div')`
  743. margin-top: ${space(2)};
  744. `;
  745. export function ErrorChart({isAllowIndexed, isQueryValid, errorMessage, ...props}: any) {
  746. return (
  747. <ChartErrorWrapper {...props}>
  748. <PanelAlert type="error">
  749. {!isAllowIndexed && !isQueryValid
  750. ? t('Your filter conditions contain an unsupported field - please review.')
  751. : typeof errorMessage === 'string'
  752. ? errorMessage
  753. : t('An error occurred while fetching data')}
  754. </PanelAlert>
  755. <StyledErrorPanel>
  756. <IconWarning color="gray500" size="lg" />
  757. </StyledErrorPanel>
  758. </ChartErrorWrapper>
  759. );
  760. }