mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
219 lines
5.9 KiB
TypeScript
219 lines
5.9 KiB
TypeScript
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';
|
|
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';
|
|
import useRouter from 'sentry/utils/useRouter';
|
|
|
|
// 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 router = useRouter();
|
|
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 router={router} {...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)};
|
|
`;
|