durationPercentileChart.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import * as React from 'react';
  2. import {withTheme} from '@emotion/react';
  3. import {Location} from 'history';
  4. import isEqual from 'lodash/isEqual';
  5. import pick from 'lodash/pick';
  6. import AsyncComponent from 'app/components/asyncComponent';
  7. import AreaChart from 'app/components/charts/areaChart';
  8. import ErrorPanel from 'app/components/charts/errorPanel';
  9. import LoadingPanel from 'app/components/charts/loadingPanel';
  10. import {HeaderTitleLegend} from 'app/components/charts/styles';
  11. import QuestionTooltip from 'app/components/questionTooltip';
  12. import {IconWarning} from 'app/icons';
  13. import {t, tct} from 'app/locale';
  14. import {OrganizationSummary} from 'app/types';
  15. import {defined} from 'app/utils';
  16. import {axisLabelFormatter} from 'app/utils/discover/charts';
  17. import EventView from 'app/utils/discover/eventView';
  18. import {getDuration} from 'app/utils/formatters';
  19. import {Theme} from 'app/utils/theme';
  20. import {filterToColour, filterToField, SpanOperationBreakdownFilter} from './filter';
  21. const QUERY_KEYS = [
  22. 'environment',
  23. 'project',
  24. 'query',
  25. 'start',
  26. 'end',
  27. 'statsPeriod',
  28. ] as const;
  29. type ViewProps = Pick<EventView, typeof QUERY_KEYS[number]>;
  30. type ApiResult = {
  31. [bucket: string]: number;
  32. };
  33. type Props = AsyncComponent['props'] &
  34. ViewProps & {
  35. organization: OrganizationSummary;
  36. location: Location;
  37. currentFilter: SpanOperationBreakdownFilter;
  38. };
  39. type State = AsyncComponent['state'] & {
  40. chartData: {data: ApiResult[]} | null;
  41. };
  42. /**
  43. * Fetch and render a bar chart that shows event volume
  44. * for each duration bucket. We always render 15 buckets of
  45. * equal widths based on the endpoints min + max durations.
  46. *
  47. * This graph visualizes how many transactions were recorded
  48. * at each duration bucket, showing the modality of the transaction.
  49. */
  50. class DurationPercentileChart extends AsyncComponent<Props, State> {
  51. generateFields = () => {
  52. const {currentFilter} = this.props;
  53. if (currentFilter === SpanOperationBreakdownFilter.None) {
  54. return [
  55. 'percentile(transaction.duration, 0.10)',
  56. 'percentile(transaction.duration, 0.25)',
  57. 'percentile(transaction.duration, 0.50)',
  58. 'percentile(transaction.duration, 0.75)',
  59. 'percentile(transaction.duration, 0.90)',
  60. 'percentile(transaction.duration, 0.95)',
  61. 'percentile(transaction.duration, 0.99)',
  62. 'percentile(transaction.duration, 0.995)',
  63. 'percentile(transaction.duration, 0.999)',
  64. 'p100()',
  65. ];
  66. }
  67. const field = filterToField(currentFilter);
  68. return [
  69. `percentile(${field}, 0.10)`,
  70. `percentile(${field}, 0.25)`,
  71. `percentile(${field}, 0.50)`,
  72. `percentile(${field}, 0.75)`,
  73. `percentile(${field}, 0.90)`,
  74. `percentile(${field}, 0.95)`,
  75. `percentile(${field}, 0.99)`,
  76. `percentile(${field}, 0.995)`,
  77. `percentile(${field}, 0.999)`,
  78. `p100(${field})`,
  79. ];
  80. };
  81. getEndpoints = (): ReturnType<AsyncComponent['getEndpoints']> => {
  82. const {
  83. organization,
  84. query,
  85. start,
  86. end,
  87. statsPeriod,
  88. environment,
  89. project,
  90. location,
  91. } = this.props;
  92. const eventView = EventView.fromSavedQuery({
  93. id: '',
  94. name: '',
  95. version: 2,
  96. fields: this.generateFields(),
  97. orderby: '',
  98. projects: project,
  99. range: statsPeriod,
  100. query,
  101. environment,
  102. start,
  103. end,
  104. });
  105. const apiPayload = eventView.getEventsAPIPayload(location);
  106. apiPayload.referrer = 'api.performance.durationpercentilechart';
  107. return [
  108. ['chartData', `/organizations/${organization.slug}/eventsv2/`, {query: apiPayload}],
  109. ];
  110. };
  111. componentDidUpdate(prevProps: Props) {
  112. if (this.shouldRefetchData(prevProps)) {
  113. this.fetchData();
  114. }
  115. }
  116. shouldRefetchData(prevProps: Props) {
  117. if (this.state.loading) {
  118. return false;
  119. }
  120. return !isEqual(pick(prevProps, QUERY_KEYS), pick(this.props, QUERY_KEYS));
  121. }
  122. renderLoading() {
  123. return <LoadingPanel data-test-id="histogram-loading" />;
  124. }
  125. renderError() {
  126. // Don't call super as we don't really need issues for this.
  127. return (
  128. <ErrorPanel>
  129. <IconWarning color="gray300" size="lg" />
  130. </ErrorPanel>
  131. );
  132. }
  133. renderBody() {
  134. const {currentFilter} = this.props;
  135. const {chartData} = this.state;
  136. if (!defined(chartData)) {
  137. return null;
  138. }
  139. const colors = (theme: Theme) =>
  140. currentFilter === SpanOperationBreakdownFilter.None
  141. ? theme.charts.getColorPalette(1)
  142. : [filterToColour(currentFilter)];
  143. return <StyledAreaChart series={transformData(chartData.data)} colors={colors} />;
  144. }
  145. render() {
  146. const {currentFilter} = this.props;
  147. const headerTitle =
  148. currentFilter === SpanOperationBreakdownFilter.None
  149. ? t('Duration Percentiles')
  150. : tct('Span Operation Percentiles - [operationName]', {
  151. operationName: currentFilter,
  152. });
  153. return (
  154. <React.Fragment>
  155. <HeaderTitleLegend>
  156. {headerTitle}
  157. <QuestionTooltip
  158. position="top"
  159. size="sm"
  160. title={t(
  161. `Compare the duration at each percentile. Compare with Latency Histogram to see transaction volume at duration intervals.`
  162. )}
  163. />
  164. </HeaderTitleLegend>
  165. {this.renderComponent()}
  166. </React.Fragment>
  167. );
  168. }
  169. }
  170. type ChartProps = React.ComponentPropsWithoutRef<typeof AreaChart> & {theme: Theme};
  171. const StyledAreaChart = withTheme(({theme, ...props}: ChartProps) => (
  172. <AreaChart
  173. grid={{left: '10px', right: '10px', top: '40px', bottom: '0px'}}
  174. xAxis={{
  175. type: 'category' as const,
  176. truncate: true,
  177. axisLabel: {
  178. showMinLabel: true,
  179. showMaxLabel: true,
  180. },
  181. axisTick: {
  182. interval: 0,
  183. alignWithLabel: true,
  184. },
  185. }}
  186. yAxis={{
  187. type: 'value' as const,
  188. axisLabel: {
  189. color: theme.chartLabel,
  190. // Use p50() to force time formatting.
  191. formatter: (value: number) => axisLabelFormatter(value, 'p50()'),
  192. },
  193. }}
  194. tooltip={{valueFormatter: value => getDuration(value / 1000, 2)}}
  195. {...props}
  196. />
  197. ));
  198. const VALUE_EXTRACT_PATTERN = /(\d+)$/;
  199. /**
  200. * Convert a discover response into a barchart compatible series
  201. */
  202. function transformData(data: ApiResult[]) {
  203. const extractedData = Object.keys(data[0])
  204. .map((key: string) => {
  205. const nameMatch = VALUE_EXTRACT_PATTERN.exec(key);
  206. if (!nameMatch) {
  207. return [-1, -1];
  208. }
  209. let nameValue = Number(nameMatch[1]);
  210. if (nameValue > 100) {
  211. nameValue /= 10;
  212. }
  213. return [nameValue, data[0][key]];
  214. })
  215. .filter(i => i[0] > 0);
  216. extractedData.sort((a, b) => {
  217. if (a[0] > b[0]) {
  218. return 1;
  219. }
  220. if (a[0] < b[0]) {
  221. return -1;
  222. }
  223. return 0;
  224. });
  225. return [
  226. {
  227. seriesName: t('Duration'),
  228. data: extractedData.map(i => ({value: i[1], name: `${i[0].toLocaleString()}%`})),
  229. },
  230. ];
  231. }
  232. export default DurationPercentileChart;