index.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  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 {getForceMetricsLayerQueryExtras} from 'sentry/utils/metrics/features';
  45. import {shouldShowOnDemandMetricAlertUI} from 'sentry/utils/onDemandMetrics/features';
  46. import {
  47. getCrashFreeRateSeries,
  48. MINUTES_THRESHOLD_TO_DISPLAY_SECONDS,
  49. } from 'sentry/utils/sessions';
  50. import {capitalize} from 'sentry/utils/string/capitalize';
  51. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  52. import withApi from 'sentry/utils/withApi';
  53. import {COMPARISON_DELTA_OPTIONS} from 'sentry/views/alerts/rules/metric/constants';
  54. import {shouldUseErrorsDiscoverDataset} from 'sentry/views/alerts/rules/utils';
  55. import type {Anomaly} from 'sentry/views/alerts/types';
  56. import {isSessionAggregate, SESSION_AGGREGATE_TO_FIELD} from 'sentry/views/alerts/utils';
  57. import {getComparisonMarkLines} from 'sentry/views/alerts/utils/getComparisonMarkLines';
  58. import {AlertWizardAlertNames} from 'sentry/views/alerts/wizard/options';
  59. import {getAlertTypeFromAggregateDataset} from 'sentry/views/alerts/wizard/utils';
  60. import {ConfidenceFooter} from 'sentry/views/explore/charts/confidenceFooter';
  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. onConfidenceDataLoaded?: (data: EventsStats | MultiSeriesEventsStats | null) => void;
  96. onDataLoaded?: (data: EventsStats | MultiSeriesEventsStats | null) => void;
  97. onHistoricalDataLoaded?: (data: EventsStats | MultiSeriesEventsStats | null) => void;
  98. showTotalCount?: boolean;
  99. };
  100. type TimePeriodMap = Omit<Record<TimePeriod, string>, TimePeriod.TWENTY_EIGHT_DAYS>;
  101. const TIME_PERIOD_MAP: TimePeriodMap = {
  102. [TimePeriod.SIX_HOURS]: t('Last 6 hours'),
  103. [TimePeriod.ONE_DAY]: t('Last 24 hours'),
  104. [TimePeriod.THREE_DAYS]: t('Last 3 days'),
  105. [TimePeriod.SEVEN_DAYS]: t('Last 7 days'),
  106. [TimePeriod.FOURTEEN_DAYS]: t('Last 14 days'),
  107. };
  108. /**
  109. * Just to avoid repeating it
  110. */
  111. const MOST_TIME_PERIODS: readonly TimePeriod[] = [
  112. TimePeriod.ONE_DAY,
  113. TimePeriod.THREE_DAYS,
  114. TimePeriod.SEVEN_DAYS,
  115. TimePeriod.FOURTEEN_DAYS,
  116. ];
  117. /**
  118. * TimeWindow determines data available in TimePeriod
  119. * If TimeWindow is small, lower TimePeriod to limit data points
  120. */
  121. export const AVAILABLE_TIME_PERIODS: Record<TimeWindow, readonly TimePeriod[]> = {
  122. [TimeWindow.ONE_MINUTE]: [
  123. TimePeriod.SIX_HOURS,
  124. TimePeriod.ONE_DAY,
  125. TimePeriod.THREE_DAYS,
  126. TimePeriod.SEVEN_DAYS,
  127. ],
  128. [TimeWindow.FIVE_MINUTES]: MOST_TIME_PERIODS,
  129. [TimeWindow.TEN_MINUTES]: MOST_TIME_PERIODS,
  130. [TimeWindow.FIFTEEN_MINUTES]: MOST_TIME_PERIODS,
  131. [TimeWindow.THIRTY_MINUTES]: MOST_TIME_PERIODS,
  132. [TimeWindow.ONE_HOUR]: MOST_TIME_PERIODS,
  133. [TimeWindow.TWO_HOURS]: MOST_TIME_PERIODS,
  134. [TimeWindow.FOUR_HOURS]: [
  135. TimePeriod.THREE_DAYS,
  136. TimePeriod.SEVEN_DAYS,
  137. TimePeriod.FOURTEEN_DAYS,
  138. ],
  139. [TimeWindow.ONE_DAY]: [TimePeriod.FOURTEEN_DAYS],
  140. };
  141. const MOST_EAP_TIME_PERIOD = [
  142. TimePeriod.ONE_DAY,
  143. TimePeriod.THREE_DAYS,
  144. TimePeriod.SEVEN_DAYS,
  145. ];
  146. const EAP_AVAILABLE_TIME_PERIODS = {
  147. [TimeWindow.ONE_MINUTE]: [], // One minute intervals are not allowed on EAP Alerts
  148. [TimeWindow.FIVE_MINUTES]: MOST_EAP_TIME_PERIOD,
  149. [TimeWindow.TEN_MINUTES]: MOST_EAP_TIME_PERIOD,
  150. [TimeWindow.FIFTEEN_MINUTES]: MOST_EAP_TIME_PERIOD,
  151. [TimeWindow.THIRTY_MINUTES]: MOST_EAP_TIME_PERIOD,
  152. [TimeWindow.ONE_HOUR]: MOST_EAP_TIME_PERIOD,
  153. [TimeWindow.TWO_HOURS]: MOST_EAP_TIME_PERIOD,
  154. [TimeWindow.FOUR_HOURS]: [TimePeriod.SEVEN_DAYS],
  155. [TimeWindow.ONE_DAY]: [TimePeriod.SEVEN_DAYS],
  156. };
  157. export const TIME_WINDOW_TO_INTERVAL = {
  158. [TimeWindow.FIVE_MINUTES]: '5m',
  159. [TimeWindow.TEN_MINUTES]: '10m',
  160. [TimeWindow.FIFTEEN_MINUTES]: '15m',
  161. [TimeWindow.THIRTY_MINUTES]: '30m',
  162. [TimeWindow.ONE_HOUR]: '1h',
  163. [TimeWindow.TWO_HOURS]: '2h',
  164. [TimeWindow.FOUR_HOURS]: '4h',
  165. [TimeWindow.ONE_DAY]: '1d',
  166. };
  167. const SESSION_AGGREGATE_TO_HEADING = {
  168. [SessionsAggregate.CRASH_FREE_SESSIONS]: t('Total Sessions'),
  169. [SessionsAggregate.CRASH_FREE_USERS]: t('Total Users'),
  170. };
  171. const HISTORICAL_TIME_PERIOD_MAP: TimePeriodMap = {
  172. [TimePeriod.SIX_HOURS]: '678h',
  173. [TimePeriod.ONE_DAY]: '29d',
  174. [TimePeriod.THREE_DAYS]: '31d',
  175. [TimePeriod.SEVEN_DAYS]: '35d',
  176. [TimePeriod.FOURTEEN_DAYS]: '42d',
  177. };
  178. const HISTORICAL_TIME_PERIOD_MAP_FIVE_MINS: TimePeriodMap = {
  179. ...HISTORICAL_TIME_PERIOD_MAP,
  180. [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
  181. [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
  182. };
  183. const noop: any = () => {};
  184. type State = {
  185. extrapolationSampleCount: number | null;
  186. sampleRate: number;
  187. statsPeriod: TimePeriod;
  188. totalCount: number | null;
  189. };
  190. const getStatsPeriodFromQuery = (
  191. queryParam: string | string[] | null | undefined
  192. ): TimePeriod => {
  193. if (typeof queryParam !== 'string') {
  194. return TimePeriod.SEVEN_DAYS;
  195. }
  196. const inMinutes = parsePeriodToHours(queryParam || '') * 60;
  197. switch (inMinutes) {
  198. case 6 * 60:
  199. return TimePeriod.SIX_HOURS;
  200. case 24 * 60:
  201. return TimePeriod.ONE_DAY;
  202. case 3 * 24 * 60:
  203. return TimePeriod.THREE_DAYS;
  204. case 9999:
  205. return TimePeriod.SEVEN_DAYS;
  206. case 14 * 24 * 60:
  207. return TimePeriod.FOURTEEN_DAYS;
  208. default:
  209. return TimePeriod.SEVEN_DAYS;
  210. }
  211. };
  212. /**
  213. * This is a chart to be used in Metric Alert rules that fetches events based on
  214. * query, timewindow, and aggregations.
  215. */
  216. class TriggersChart extends PureComponent<Props, State> {
  217. state: State = {
  218. statsPeriod: getStatsPeriodFromQuery(this.props.location.query.statsPeriod),
  219. totalCount: null,
  220. sampleRate: 1,
  221. extrapolationSampleCount: null,
  222. };
  223. componentDidMount() {
  224. const {aggregate, showTotalCount} = this.props;
  225. if (showTotalCount && !isSessionAggregate(aggregate)) {
  226. this.fetchTotalCount();
  227. }
  228. if (this.props.dataset === Dataset.EVENTS_ANALYTICS_PLATFORM) {
  229. this.fetchExtrapolationSampleCount();
  230. }
  231. }
  232. componentDidUpdate(prevProps: Props, prevState: State) {
  233. const {query, environment, timeWindow, aggregate, projects, showTotalCount} =
  234. this.props;
  235. const {statsPeriod} = this.state;
  236. if (
  237. !isEqual(prevProps.projects, projects) ||
  238. prevProps.environment !== environment ||
  239. prevProps.query !== query ||
  240. !isEqual(prevProps.timeWindow, timeWindow) ||
  241. !isEqual(prevState.statsPeriod, statsPeriod)
  242. ) {
  243. if (showTotalCount && !isSessionAggregate(aggregate)) {
  244. this.fetchTotalCount();
  245. }
  246. if (this.props.dataset === Dataset.EVENTS_ANALYTICS_PLATFORM) {
  247. this.fetchExtrapolationSampleCount();
  248. }
  249. }
  250. }
  251. // Create new API Client so that historical requests aren't automatically deduplicated
  252. historicalAPI = new Client();
  253. confidenceAPI = new Client();
  254. get availableTimePeriods() {
  255. // We need to special case sessions, because sub-hour windows are available
  256. // only when time period is six hours or less (backend limitation)
  257. if (isSessionAggregate(this.props.aggregate)) {
  258. return {
  259. ...AVAILABLE_TIME_PERIODS,
  260. [TimeWindow.THIRTY_MINUTES]: [TimePeriod.SIX_HOURS],
  261. };
  262. }
  263. if (this.props.dataset === Dataset.EVENTS_ANALYTICS_PLATFORM) {
  264. return EAP_AVAILABLE_TIME_PERIODS;
  265. }
  266. return AVAILABLE_TIME_PERIODS;
  267. }
  268. handleStatsPeriodChange = (timePeriod: TimePeriod) => {
  269. this.setState({statsPeriod: timePeriod});
  270. };
  271. getStatsPeriod = () => {
  272. const {statsPeriod} = this.state;
  273. const {timeWindow} = this.props;
  274. const statsPeriodOptions = this.availableTimePeriods[timeWindow];
  275. const period = statsPeriodOptions.includes(statsPeriod)
  276. ? statsPeriod
  277. : statsPeriodOptions[statsPeriodOptions.length - 1];
  278. return period;
  279. };
  280. get comparisonSeriesName() {
  281. return capitalize(
  282. COMPARISON_DELTA_OPTIONS.find(({value}) => value === this.props.comparisonDelta)
  283. ?.label || ''
  284. );
  285. }
  286. async fetchTotalCount() {
  287. const {
  288. api,
  289. organization,
  290. location,
  291. newAlertOrQuery,
  292. environment,
  293. projects,
  294. query,
  295. dataset,
  296. } = this.props;
  297. const statsPeriod = this.getStatsPeriod();
  298. const queryExtras = getMetricDatasetQueryExtras({
  299. organization,
  300. location,
  301. dataset,
  302. newAlertOrQuery,
  303. });
  304. let queryDataset = queryExtras.dataset as undefined | DiscoverDatasets;
  305. const queryOverride = (queryExtras.query as string | undefined) ?? query;
  306. if (shouldUseErrorsDiscoverDataset(query, dataset, organization)) {
  307. queryDataset = DiscoverDatasets.ERRORS;
  308. }
  309. try {
  310. const totalCount = await fetchTotalCount(api, organization.slug, {
  311. field: [],
  312. project: projects.map(({id}) => id),
  313. query: queryOverride,
  314. statsPeriod,
  315. environment: environment ? [environment] : [],
  316. dataset: queryDataset,
  317. ...getForceMetricsLayerQueryExtras(organization, dataset),
  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. <ConfidenceFooter
  451. sampleCount={extrapolationSampleCount ?? undefined}
  452. confidence={confidence}
  453. />
  454. ) : (
  455. <React.Fragment>
  456. <SectionHeading>{totalCountLabel}</SectionHeading>
  457. <SectionValue>
  458. {totalCount !== null ? totalCount.toLocaleString() : '\u2014'}
  459. </SectionValue>
  460. </React.Fragment>
  461. )}
  462. </InlineContainer>
  463. ) : (
  464. <InlineContainer />
  465. )}
  466. <InlineContainer>
  467. <CompactSelect
  468. size="sm"
  469. options={statsPeriodOptions.map(timePeriod => ({
  470. value: timePeriod,
  471. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  472. label: TIME_PERIOD_MAP[timePeriod],
  473. }))}
  474. value={period}
  475. onChange={opt => this.handleStatsPeriodChange(opt.value)}
  476. position="bottom-end"
  477. triggerProps={{
  478. borderless: true,
  479. prefix: t('Display'),
  480. }}
  481. />
  482. </InlineContainer>
  483. </ChartControls>
  484. </Fragment>
  485. );
  486. }
  487. render() {
  488. const {
  489. api,
  490. organization,
  491. projects,
  492. timeWindow,
  493. query,
  494. location,
  495. aggregate,
  496. dataset,
  497. newAlertOrQuery,
  498. onDataLoaded,
  499. onHistoricalDataLoaded,
  500. environment,
  501. formattedAggregate,
  502. comparisonDelta,
  503. triggers,
  504. thresholdType,
  505. isQueryValid,
  506. isOnDemandMetricAlert,
  507. onConfidenceDataLoaded,
  508. } = this.props;
  509. const period = this.getStatsPeriod()!;
  510. const renderComparisonStats = Boolean(
  511. organization.features.includes('change-alerts') && comparisonDelta
  512. );
  513. const queryExtras = {
  514. ...getMetricDatasetQueryExtras({
  515. organization,
  516. location,
  517. dataset,
  518. newAlertOrQuery,
  519. }),
  520. ...getForceMetricsLayerQueryExtras(organization, dataset),
  521. ...(shouldUseErrorsDiscoverDataset(query, dataset, organization)
  522. ? {dataset: DiscoverDatasets.ERRORS}
  523. : {}),
  524. };
  525. if (isOnDemandMetricAlert) {
  526. const {sampleRate} = this.state;
  527. const baseProps: EventsRequestProps = {
  528. api,
  529. organization,
  530. query,
  531. queryExtras,
  532. sampleRate,
  533. period,
  534. environment: environment ? [environment] : undefined,
  535. project: projects.map(({id}) => Number(id)),
  536. interval: `${timeWindow}m`,
  537. comparisonDelta: comparisonDelta ? comparisonDelta * 60 : undefined,
  538. yAxis: aggregate,
  539. includePrevious: false,
  540. currentSeriesNames: [formattedAggregate || aggregate],
  541. partial: false,
  542. limit: 15,
  543. children: noop,
  544. };
  545. return (
  546. <Fragment>
  547. {this.props.includeHistorical ? (
  548. <OnDemandMetricRequest
  549. {...baseProps}
  550. api={this.historicalAPI}
  551. period={
  552. timeWindow === 5
  553. ? // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  554. HISTORICAL_TIME_PERIOD_MAP_FIVE_MINS[period]!
  555. : // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  556. HISTORICAL_TIME_PERIOD_MAP[period]!
  557. }
  558. dataLoadedCallback={onHistoricalDataLoaded}
  559. />
  560. ) : null}
  561. <OnDemandMetricRequest {...baseProps} dataLoadedCallback={onDataLoaded}>
  562. {({
  563. loading,
  564. errored,
  565. errorMessage,
  566. reloading,
  567. timeseriesData,
  568. comparisonTimeseriesData,
  569. seriesAdditionalInfo,
  570. }) => {
  571. let comparisonMarkLines: LineChartSeries[] = [];
  572. if (renderComparisonStats && comparisonTimeseriesData) {
  573. comparisonMarkLines = getComparisonMarkLines(
  574. timeseriesData,
  575. comparisonTimeseriesData,
  576. timeWindow,
  577. triggers,
  578. thresholdType
  579. );
  580. }
  581. return this.renderChart({
  582. timeseriesData: timeseriesData as Series[],
  583. isLoading: loading,
  584. isReloading: reloading,
  585. comparisonData: comparisonTimeseriesData,
  586. comparisonMarkLines,
  587. errorMessage,
  588. isQueryValid,
  589. errored,
  590. orgFeatures: organization.features,
  591. seriesAdditionalInfo,
  592. });
  593. }}
  594. </OnDemandMetricRequest>
  595. </Fragment>
  596. );
  597. }
  598. if (isSessionAggregate(aggregate)) {
  599. const baseProps: ComponentProps<typeof SessionsRequest> = {
  600. api,
  601. organization,
  602. project: projects.map(({id}) => Number(id)),
  603. environment: environment ? [environment] : undefined,
  604. statsPeriod: period,
  605. query,
  606. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  607. interval: TIME_WINDOW_TO_INTERVAL[timeWindow],
  608. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  609. field: SESSION_AGGREGATE_TO_FIELD[aggregate],
  610. groupBy: ['session.status'],
  611. children: noop,
  612. };
  613. return (
  614. <SessionsRequest {...baseProps}>
  615. {({loading, errored, reloading, response}) => {
  616. const {groups, intervals} = response || {};
  617. const sessionTimeSeries = [
  618. {
  619. seriesName:
  620. AlertWizardAlertNames[
  621. getAlertTypeFromAggregateDataset({
  622. aggregate,
  623. dataset: Dataset.SESSIONS,
  624. })
  625. ],
  626. data: getCrashFreeRateSeries(
  627. groups,
  628. intervals,
  629. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  630. SESSION_AGGREGATE_TO_FIELD[aggregate]
  631. ),
  632. },
  633. ];
  634. return this.renderChart({
  635. timeseriesData: sessionTimeSeries,
  636. isLoading: loading,
  637. isReloading: reloading,
  638. comparisonData: undefined,
  639. comparisonMarkLines: undefined,
  640. minutesThresholdToDisplaySeconds: MINUTES_THRESHOLD_TO_DISPLAY_SECONDS,
  641. isQueryValid,
  642. errored,
  643. orgFeatures: organization.features,
  644. });
  645. }}
  646. </SessionsRequest>
  647. );
  648. }
  649. const useRpc = dataset === Dataset.EVENTS_ANALYTICS_PLATFORM;
  650. const baseProps = {
  651. api,
  652. organization,
  653. query,
  654. period,
  655. queryExtras,
  656. environment: environment ? [environment] : undefined,
  657. project: projects.map(({id}) => Number(id)),
  658. interval: `${timeWindow}m`,
  659. comparisonDelta: comparisonDelta ? comparisonDelta * 60 : undefined,
  660. yAxis: aggregate,
  661. includePrevious: false,
  662. currentSeriesNames: [formattedAggregate || aggregate],
  663. partial: false,
  664. useRpc,
  665. };
  666. return (
  667. <Fragment>
  668. {this.props.includeHistorical ? (
  669. <EventsRequest
  670. {...baseProps}
  671. api={this.historicalAPI}
  672. period={
  673. timeWindow === 5
  674. ? // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  675. HISTORICAL_TIME_PERIOD_MAP_FIVE_MINS[period]!
  676. : // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  677. HISTORICAL_TIME_PERIOD_MAP[period]!
  678. }
  679. dataLoadedCallback={onHistoricalDataLoaded}
  680. >
  681. {noop}
  682. </EventsRequest>
  683. ) : null}
  684. {this.props.includeConfidence ? (
  685. <EventsRequest
  686. {...baseProps}
  687. api={this.confidenceAPI}
  688. period={TimePeriod.SEVEN_DAYS}
  689. dataLoadedCallback={onConfidenceDataLoaded}
  690. >
  691. {noop}
  692. </EventsRequest>
  693. ) : null}
  694. <EventsRequest {...baseProps} period={period} dataLoadedCallback={onDataLoaded}>
  695. {({
  696. loading,
  697. errored,
  698. errorMessage,
  699. reloading,
  700. timeseriesData,
  701. comparisonTimeseriesData,
  702. }) => {
  703. let comparisonMarkLines: LineChartSeries[] = [];
  704. if (renderComparisonStats && comparisonTimeseriesData) {
  705. comparisonMarkLines = getComparisonMarkLines(
  706. timeseriesData,
  707. comparisonTimeseriesData,
  708. timeWindow,
  709. triggers,
  710. thresholdType
  711. );
  712. }
  713. return this.renderChart({
  714. timeseriesData: timeseriesData as Series[],
  715. isLoading: loading,
  716. isReloading: reloading,
  717. comparisonData: comparisonTimeseriesData,
  718. comparisonMarkLines,
  719. errorMessage,
  720. isQueryValid,
  721. errored,
  722. orgFeatures: organization.features,
  723. });
  724. }}
  725. </EventsRequest>
  726. </Fragment>
  727. );
  728. }
  729. }
  730. export default withApi(TriggersChart);
  731. const TransparentLoadingMask = styled(LoadingMask)<{visible: boolean}>`
  732. ${p => !p.visible && 'display: none;'};
  733. opacity: 0.4;
  734. z-index: 1;
  735. `;
  736. const ChartPlaceholder = styled(Placeholder)`
  737. /* Height and margin should add up to graph size (200px) */
  738. margin: 0 0 ${space(2)};
  739. height: 184px;
  740. `;
  741. const StyledErrorPanel = styled(ErrorPanel)`
  742. /* Height and margin should with the alert should match up placeholder height of (184px) */
  743. padding: ${space(2)};
  744. height: 119px;
  745. `;
  746. const ChartErrorWrapper = styled('div')`
  747. margin-top: ${space(2)};
  748. `;
  749. export function ErrorChart({isAllowIndexed, isQueryValid, errorMessage, ...props}: any) {
  750. return (
  751. <ChartErrorWrapper {...props}>
  752. <PanelAlert type="error">
  753. {!isAllowIndexed && !isQueryValid
  754. ? t('Your filter conditions contain an unsupported field - please review.')
  755. : typeof errorMessage === 'string'
  756. ? errorMessage
  757. : t('An error occurred while fetching data')}
  758. </PanelAlert>
  759. <StyledErrorPanel>
  760. <IconWarning color="gray500" size="lg" />
  761. </StyledErrorPanel>
  762. </ChartErrorWrapper>
  763. );
  764. }