123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- import {useMemo} from 'react';
- import {useTheme} from '@emotion/react';
- import styled from '@emotion/styled';
- import type {AreaChartProps} from 'sentry/components/charts/areaChart';
- import {AreaChart} from 'sentry/components/charts/areaChart';
- import ChartZoom from 'sentry/components/charts/chartZoom';
- import type {LineChartProps} from 'sentry/components/charts/lineChart';
- import {HeaderTitle} from 'sentry/components/charts/styles';
- import Panel from 'sentry/components/panels/panel';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {PageFilters} from 'sentry/types/core';
- import type {Series} from 'sentry/types/echarts';
- import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts';
- import {aggregateOutputType} from 'sentry/utils/discover/fields';
- import {useProfileEventsStats} from 'sentry/utils/profiling/hooks/useProfileEventsStats';
- // We want p99 to be before p75 because echarts renders the series in order.
- // So if p75 is before p99, p99 will be rendered on top of p75 which will
- // cover it up.
- const SERIES_ORDER = ['count()', 'p99()', 'p95()', 'p75()'] as const;
- interface ProfilesChartProps {
- query: string;
- referrer: string;
- compact?: boolean;
- hideCount?: boolean;
- selection?: PageFilters;
- }
- export function ProfilesChart({
- query,
- referrer,
- selection,
- hideCount,
- compact = false,
- }: ProfilesChartProps) {
- const theme = useTheme();
- const seriesOrder = useMemo(() => {
- if (hideCount) {
- return SERIES_ORDER.filter(s => s !== 'count()');
- }
- return SERIES_ORDER;
- }, [hideCount]);
- const profileStats = useProfileEventsStats({
- dataset: 'profiles',
- query,
- referrer,
- yAxes: seriesOrder,
- });
- const series: Series[] = useMemo(() => {
- if (profileStats.status !== 'success') {
- return [];
- }
- // the timestamps in the response is in seconds but echarts expects
- // a timestamp in milliseconds, so multiply by 1e3 to do the conversion
- const timestamps = profileStats.data.timestamps.map(ts => ts * 1e3);
- const allSeries = profileStats.data.data
- .filter(rawData => seriesOrder.includes(rawData.axis))
- .map(rawData => {
- if (timestamps.length !== rawData.values.length) {
- throw new Error('Invalid stats response');
- }
- if (rawData.axis === 'count()') {
- return {
- data: rawData.values.map((value, i) => ({
- name: timestamps[i]!,
- // the response value contains nulls when no data is
- // available, use 0 to represent it
- value: value ?? 0,
- })),
- seriesName: rawData.axis,
- xAxisIndex: 0,
- yAxisIndex: 0,
- };
- }
- return {
- data: rawData.values.map((value, i) => ({
- name: timestamps[i]!,
- // the response value contains nulls when no data
- // is available, use 0 to represent it
- value: value ?? 0,
- })),
- seriesName: rawData.axis,
- xAxisIndex: 1,
- yAxisIndex: 1,
- };
- });
- allSeries.sort((a, b) => {
- const idxA = seriesOrder.indexOf(a.seriesName as any);
- const idxB = seriesOrder.indexOf(b.seriesName as any);
- return idxA - idxB;
- });
- return allSeries;
- }, [profileStats, seriesOrder]);
- const chartProps: LineChartProps | AreaChartProps = useMemo(() => {
- const baseProps: LineChartProps | AreaChartProps = {
- height: compact ? 150 : 300,
- series,
- grid: [
- {
- top: '32px',
- left: '24px',
- right: '52%',
- bottom: '16px',
- },
- {
- top: '32px',
- left: hideCount ? '24px' : '52%',
- right: '24px',
- bottom: '16px',
- },
- ],
- legend: {
- right: 16,
- top: 12,
- data: seriesOrder.slice(),
- },
- tooltip: {
- valueFormatter: (value, label) =>
- tooltipFormatter(value, aggregateOutputType(label)),
- },
- axisPointer: {
- link: [
- {
- xAxisIndex: [0, 1],
- },
- ],
- },
- xAxes: [
- {
- show: !hideCount,
- gridIndex: 0,
- type: 'time' as const,
- },
- {
- gridIndex: 1,
- type: 'time' as const,
- },
- ],
- yAxes: [
- {
- gridIndex: 0,
- scale: true,
- axisLabel: {
- color: theme.chartLabel,
- formatter(value: number) {
- return axisLabelFormatter(value, 'integer');
- },
- },
- },
- {
- gridIndex: 1,
- scale: true,
- axisLabel: {
- color: theme.chartLabel,
- formatter(value: number) {
- return axisLabelFormatter(value, 'duration');
- },
- },
- },
- ],
- };
- return baseProps;
- }, [compact, hideCount, series, seriesOrder, theme.chartLabel]);
- return (
- <ChartZoom {...selection?.datetime}>
- {zoomRenderProps => (
- <StyledPanel>
- <TitleContainer>
- {!hideCount && (
- <StyledHeaderTitle compact>{t('Profiles by Count')}</StyledHeaderTitle>
- )}
- <StyledHeaderTitle compact>{t('Profiles Duration')}</StyledHeaderTitle>
- </TitleContainer>
- <AreaChart
- {...chartProps}
- isGroupedByDate
- showTimeInTooltip
- {...zoomRenderProps}
- />
- </StyledPanel>
- )}
- </ChartZoom>
- );
- }
- const StyledPanel = styled(Panel)`
- padding-top: ${space(2)};
- `;
- const TitleContainer = styled('div')`
- width: 100%;
- display: flex;
- flex-direction: row;
- `;
- const StyledHeaderTitle = styled(HeaderTitle)<{compact?: boolean}>`
- flex-grow: 1;
- margin-left: ${space(2)};
- font-size: ${p => (p.compact ? p.theme.fontSizeSmall : undefined)};
- `;
|