mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
205 lines
5.4 KiB
TypeScript
205 lines
5.4 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 {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 ProfileSummaryChartProps {
|
|
query: string;
|
|
referrer: string;
|
|
hideCount?: boolean;
|
|
selection?: PageFilters;
|
|
}
|
|
|
|
export function ProfilesSummaryChart({
|
|
query,
|
|
referrer,
|
|
selection,
|
|
hideCount,
|
|
}: ProfileSummaryChartProps) {
|
|
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: AreaChartProps = useMemo(() => {
|
|
const baseProps: AreaChartProps = {
|
|
height: 150,
|
|
series,
|
|
grid: [
|
|
{
|
|
top: '16px',
|
|
left: '16px',
|
|
right: '8px',
|
|
bottom: '16px',
|
|
},
|
|
{
|
|
top: '16px',
|
|
left: '8px',
|
|
right: '16px',
|
|
bottom: '8px',
|
|
},
|
|
],
|
|
legend: {
|
|
right: 16,
|
|
top: 0,
|
|
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;
|
|
}, [hideCount, series, seriesOrder, theme.chartLabel]);
|
|
|
|
return (
|
|
<ProfilesChartContainer>
|
|
<ProfilesChartTitle>{t('Durations')}</ProfilesChartTitle>
|
|
<ChartZoom router={router} {...selection?.datetime}>
|
|
{zoomRenderProps => (
|
|
<AreaChart
|
|
{...chartProps}
|
|
isGroupedByDate
|
|
showTimeInTooltip
|
|
{...zoomRenderProps}
|
|
/>
|
|
)}
|
|
</ChartZoom>
|
|
</ProfilesChartContainer>
|
|
);
|
|
}
|
|
|
|
const ProfilesChartTitle = styled('div')`
|
|
font-size: ${p => p.theme.fontSizeSmall};
|
|
color: ${p => p.theme.textColor};
|
|
font-weight: 600;
|
|
padding: ${space(0.25)} ${space(1)};
|
|
`;
|
|
|
|
const ProfilesChartContainer = styled('div')`
|
|
background-color: ${p => p.theme.background};
|
|
border-bottom: 1px solid ${p => p.theme.border};
|
|
`;
|