profilingScatterChart.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import {useCallback, useMemo} from 'react';
  2. // eslint-disable-next-line no-restricted-imports
  3. import {browserHistory, withRouter, WithRouterProps} from 'react-router';
  4. import {useTheme} from '@emotion/react';
  5. import type {TooltipComponentFormatterCallback} from 'echarts';
  6. import {Location} from 'history';
  7. import moment from 'moment';
  8. import momentTimezone from 'moment-timezone';
  9. import ChartZoom from 'sentry/components/charts/chartZoom';
  10. import OptionSelector from 'sentry/components/charts/optionSelector';
  11. import ScatterChart from 'sentry/components/charts/scatterChart';
  12. import {
  13. ChartContainer,
  14. ChartControls,
  15. InlineContainer,
  16. } from 'sentry/components/charts/styles';
  17. import TransitionChart from 'sentry/components/charts/transitionChart';
  18. import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
  19. import {getSeriesSelection} from 'sentry/components/charts/utils';
  20. import {Panel} from 'sentry/components/panels';
  21. import {t} from 'sentry/locale';
  22. import ConfigStore from 'sentry/stores/configStore';
  23. import {Organization, PageFilters, Project} from 'sentry/types';
  24. import {Series} from 'sentry/types/echarts';
  25. import {Trace} from 'sentry/types/profiling/core';
  26. import {defined} from 'sentry/utils';
  27. import {axisLabelFormatter} from 'sentry/utils/discover/charts';
  28. import {getDuration} from 'sentry/utils/formatters';
  29. import {generateProfileDetailsRoute} from 'sentry/utils/profiling/routes';
  30. import {Theme} from 'sentry/utils/theme';
  31. import useOrganization from 'sentry/utils/useOrganization';
  32. import useProjects from 'sentry/utils/useProjects';
  33. import {COLOR_ENCODINGS, getColorEncodingFromLocation} from '../utils';
  34. interface ProfilingScatterChartProps extends WithRouterProps {
  35. datetime: PageFilters['datetime'];
  36. isLoading: boolean;
  37. traces: Trace[];
  38. }
  39. function ProfilingScatterChart({
  40. router,
  41. location,
  42. datetime,
  43. isLoading,
  44. traces,
  45. }: ProfilingScatterChartProps) {
  46. const organization = useOrganization();
  47. const {projects} = useProjects();
  48. const theme = useTheme();
  49. const colorEncoding = useMemo(() => getColorEncodingFromLocation(location), [location]);
  50. const data: Record<string, Trace[]> = useMemo(() => {
  51. const dataMap = {};
  52. for (const row of traces) {
  53. const seriesName =
  54. colorEncoding === 'version'
  55. ? `${row.version_name} (build ${row.version_code})`
  56. : row[colorEncoding];
  57. if (!dataMap[seriesName]) {
  58. dataMap[seriesName] = [];
  59. }
  60. dataMap[seriesName].push(row);
  61. }
  62. return dataMap;
  63. }, [colorEncoding, traces]);
  64. const series: Series[] = useMemo(() => {
  65. return Object.entries(data).map(([seriesName, seriesData]) => {
  66. return {
  67. seriesName,
  68. data: seriesData.map(row => ({
  69. name: row.timestamp * 1000,
  70. value: row.trace_duration_ms,
  71. })),
  72. };
  73. });
  74. }, [data]);
  75. const chartOptions = useMemo(
  76. () =>
  77. makeScatterChartOptions({
  78. data,
  79. datetime,
  80. location,
  81. organization,
  82. projects,
  83. theme,
  84. }),
  85. [location, datetime, theme, data, organization, projects]
  86. );
  87. const handleColorEncodingChange = useCallback(
  88. value => {
  89. browserHistory.push({
  90. ...location,
  91. query: {
  92. ...location.query,
  93. colorEncoding: value,
  94. },
  95. });
  96. },
  97. [location]
  98. );
  99. return (
  100. <Panel>
  101. <ChartContainer>
  102. <ChartZoom
  103. router={router}
  104. period={datetime.period}
  105. start={datetime.start}
  106. end={datetime.end}
  107. utc={datetime.utc}
  108. >
  109. {zoomRenderProps => {
  110. return (
  111. <TransitionChart loading={isLoading} reloading={isLoading}>
  112. <TransparentLoadingMask visible={isLoading} />
  113. <ScatterChart series={series} {...chartOptions} {...zoomRenderProps} />
  114. </TransitionChart>
  115. );
  116. }}
  117. </ChartZoom>
  118. </ChartContainer>
  119. <ChartControls>
  120. <InlineContainer>
  121. <OptionSelector
  122. title={t('Group By')}
  123. selected={colorEncoding}
  124. options={COLOR_ENCODINGS}
  125. onChange={handleColorEncodingChange}
  126. />
  127. </InlineContainer>
  128. </ChartControls>
  129. </Panel>
  130. );
  131. }
  132. function makeScatterChartOptions({
  133. data,
  134. datetime,
  135. location,
  136. organization,
  137. projects,
  138. theme,
  139. }: {
  140. /**
  141. * The data is a mapping from the series name to a list of traces in the series. In particular,
  142. * the order of the traces must match the order of the data in the series in the scatter plot.
  143. */
  144. data: Record<string, Trace[]>;
  145. datetime: PageFilters['datetime'];
  146. location: Location;
  147. organization: Organization;
  148. projects: Project[];
  149. theme: Theme;
  150. }) {
  151. const user = ConfigStore.get('user');
  152. const options = user?.options;
  153. const _tooltipFormatter: TooltipComponentFormatterCallback<any> = seriesParams => {
  154. const dataPoint = data[seriesParams.seriesName]?.[seriesParams.dataIndex];
  155. const project = dataPoint && projects.find(proj => proj.id === dataPoint.project_id);
  156. const entries = [
  157. {label: t('Project'), value: project?.slug},
  158. {label: t('App Version'), value: dataPoint?.version_code},
  159. {
  160. label: t('Duration'),
  161. value: defined(dataPoint?.trace_duration_ms)
  162. ? getDuration(dataPoint?.trace_duration_ms / 1000, 2, true)
  163. : null,
  164. },
  165. {label: t('Transaction'), value: dataPoint?.transaction_name},
  166. {label: t('Device Model'), value: dataPoint?.device_model},
  167. {label: t('Device Classification'), value: dataPoint?.device_classification},
  168. {label: t('Device Manufacturer'), value: dataPoint?.device_manufacturer},
  169. ].map(
  170. ({label, value}) =>
  171. `<div><span class="tooltip-label"><strong>${label}</strong></span> ${
  172. value ?? t('Unknown')
  173. }</div>`
  174. );
  175. const date = defined(dataPoint?.timestamp)
  176. ? momentTimezone
  177. .tz(dataPoint?.timestamp * 1000, options?.timezone ?? '')
  178. .format('lll')
  179. : null;
  180. return [
  181. '<div class="tooltip-series">',
  182. ...entries,
  183. '</div>',
  184. `<div class="tooltip-date">${date}</div>`,
  185. '<div class="tooltip-arrow"></div>',
  186. ].join('');
  187. };
  188. const now = moment.utc();
  189. const end = (defined(datetime.end) ? moment.utc(datetime.end) : now).valueOf();
  190. const start = (
  191. defined(datetime.start)
  192. ? moment.utc(datetime.start)
  193. : now.subtract(
  194. parseInt(datetime.period ?? '14', 10),
  195. datetime.period?.charAt(datetime.period.length - 1) === 'h' ? 'hours' : 'days'
  196. )
  197. ).valueOf();
  198. return {
  199. grid: {
  200. left: '10px',
  201. right: '10px',
  202. top: '40px',
  203. bottom: '0px',
  204. },
  205. tooltip: {
  206. trigger: 'item' as const,
  207. formatter: _tooltipFormatter,
  208. },
  209. xAxis: {
  210. // need to specify a min/max on the date range here
  211. // or echarts will use the min/max from the series
  212. min: start,
  213. max: end,
  214. },
  215. yAxis: {
  216. axisLabel: {
  217. color: theme.chartLabel,
  218. formatter: (value: number) => axisLabelFormatter(value, 'p50()'),
  219. },
  220. },
  221. legend: {
  222. right: 10,
  223. top: 5,
  224. selected: getSeriesSelection(location),
  225. },
  226. onClick: params => {
  227. const dataPoint = data[params.seriesName]?.[params.dataIndex];
  228. if (!defined(dataPoint)) {
  229. return;
  230. }
  231. const project = projects.find(proj => proj.id === dataPoint.project_id);
  232. if (!defined(project)) {
  233. return;
  234. }
  235. browserHistory.push(
  236. generateProfileDetailsRoute({
  237. orgSlug: organization.slug,
  238. projectSlug: project.slug,
  239. profileId: dataPoint.id,
  240. })
  241. );
  242. },
  243. };
  244. }
  245. const ProfilingScatterChartWithRouter = withRouter(ProfilingScatterChart);
  246. export {ProfilingScatterChartWithRouter as ProfilingScatterChart};