mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
364 lines
11 KiB
TypeScript
364 lines
11 KiB
TypeScript
import {useTheme} from '@emotion/react';
|
|
import styled from '@emotion/styled';
|
|
|
|
import ChartZoom from 'sentry/components/charts/chartZoom';
|
|
import ErrorPanel from 'sentry/components/charts/errorPanel';
|
|
import type {LineChartProps} from 'sentry/components/charts/lineChart';
|
|
import {LineChart} from 'sentry/components/charts/lineChart';
|
|
import TransitionChart from 'sentry/components/charts/transitionChart';
|
|
import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
|
|
import ErrorBoundary from 'sentry/components/errorBoundary';
|
|
import NotAvailable from 'sentry/components/notAvailable';
|
|
import QuestionTooltip from 'sentry/components/questionTooltip';
|
|
import * as SidebarSection from 'sentry/components/sidebarSection';
|
|
import Tag from 'sentry/components/tag';
|
|
import {Tooltip} from 'sentry/components/tooltip';
|
|
import {IconWarning} from 'sentry/icons';
|
|
import {t, tct} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {ReleaseProject, ReleaseWithHealth, SessionApiResponse} from 'sentry/types';
|
|
import {SessionFieldWithOperation} from 'sentry/types';
|
|
import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
|
|
import {getAdoptionSeries, getCount, getCountAtIndex} from 'sentry/utils/sessions';
|
|
import {useLocation} from 'sentry/utils/useLocation';
|
|
import useRouter from 'sentry/utils/useRouter';
|
|
|
|
import {
|
|
ADOPTION_STAGE_LABELS,
|
|
getReleaseBounds,
|
|
getReleaseParams,
|
|
isMobileRelease,
|
|
} from '../../../utils';
|
|
import {generateReleaseMarkLines, releaseMarkLinesLabels} from '../../utils';
|
|
|
|
const sessionsAxisIndex = 0;
|
|
const usersAxisIndex = 1;
|
|
const axisIndexToSessionsField = {
|
|
[sessionsAxisIndex]: SessionFieldWithOperation.SESSIONS,
|
|
[usersAxisIndex]: SessionFieldWithOperation.USERS,
|
|
};
|
|
|
|
type Props = {
|
|
allSessions: SessionApiResponse | null;
|
|
environment: string[];
|
|
errored: boolean;
|
|
loading: boolean;
|
|
project: ReleaseProject;
|
|
release: ReleaseWithHealth;
|
|
releaseSessions: SessionApiResponse | null;
|
|
reloading: boolean;
|
|
};
|
|
|
|
function ReleaseAdoption({
|
|
release,
|
|
project,
|
|
environment,
|
|
releaseSessions,
|
|
allSessions,
|
|
loading,
|
|
reloading,
|
|
errored,
|
|
}: Props) {
|
|
const location = useLocation();
|
|
const router = useRouter();
|
|
const theme = useTheme();
|
|
|
|
const hasUsers = !!getCount(releaseSessions?.groups, SessionFieldWithOperation.USERS);
|
|
|
|
function getSeries() {
|
|
if (!releaseSessions) {
|
|
return [];
|
|
}
|
|
|
|
const sessionsMarkLines = generateReleaseMarkLines(
|
|
release,
|
|
project,
|
|
theme,
|
|
location,
|
|
{
|
|
hideLabel: true,
|
|
axisIndex: sessionsAxisIndex,
|
|
}
|
|
);
|
|
const sessionSeriesData = getAdoptionSeries(
|
|
releaseSessions.groups,
|
|
allSessions?.groups,
|
|
releaseSessions.intervals,
|
|
SessionFieldWithOperation.SESSIONS
|
|
);
|
|
// echarts doesn't seem to like displaying marklines when there's only one data point.
|
|
// Usually, there is one data point because there is very little sessions data.
|
|
const hasMultipleDataPoints = sessionSeriesData.length > 1;
|
|
const series = [
|
|
...(hasMultipleDataPoints ? sessionsMarkLines : []),
|
|
{
|
|
seriesName: t('Sessions'),
|
|
connectNulls: true,
|
|
yAxisIndex: sessionsAxisIndex,
|
|
xAxisIndex: sessionsAxisIndex,
|
|
data: sessionSeriesData,
|
|
},
|
|
];
|
|
|
|
if (hasUsers) {
|
|
const usersMarkLines = generateReleaseMarkLines(release, project, theme, location, {
|
|
hideLabel: true,
|
|
axisIndex: usersAxisIndex,
|
|
});
|
|
|
|
series.push(...usersMarkLines);
|
|
series.push({
|
|
seriesName: t('Users'),
|
|
connectNulls: true,
|
|
yAxisIndex: usersAxisIndex,
|
|
xAxisIndex: usersAxisIndex,
|
|
data: getAdoptionSeries(
|
|
releaseSessions.groups,
|
|
allSessions?.groups,
|
|
releaseSessions.intervals,
|
|
SessionFieldWithOperation.USERS
|
|
),
|
|
});
|
|
}
|
|
|
|
return series;
|
|
}
|
|
|
|
const colors = theme.charts.getColorPalette(2);
|
|
|
|
const axisLineConfig = {
|
|
scale: true,
|
|
axisLine: {
|
|
show: false,
|
|
},
|
|
axisTick: {
|
|
show: false,
|
|
},
|
|
splitLine: {
|
|
show: false,
|
|
},
|
|
max: 100,
|
|
axisLabel: {
|
|
formatter: (value: number) => `${value}%`,
|
|
color: theme.chartLabel,
|
|
},
|
|
};
|
|
|
|
const chartOptions: Omit<LineChartProps, 'series' | 'ref'> = {
|
|
height: hasUsers ? 280 : 140,
|
|
grid: [
|
|
{
|
|
top: '40px',
|
|
left: '10px',
|
|
right: '10px',
|
|
height: '100px',
|
|
},
|
|
{
|
|
top: '180px',
|
|
left: '10px',
|
|
right: '10px',
|
|
height: '100px',
|
|
},
|
|
],
|
|
axisPointer: {
|
|
// Link each x-axis together.
|
|
link: [{xAxisIndex: [sessionsAxisIndex, usersAxisIndex]}],
|
|
},
|
|
xAxes: Array.from(new Array(2)).map((_i, index) => ({
|
|
gridIndex: index,
|
|
type: 'time' as const,
|
|
show: false,
|
|
})),
|
|
yAxes: [
|
|
{
|
|
gridIndex: sessionsAxisIndex,
|
|
...axisLineConfig,
|
|
},
|
|
{
|
|
gridIndex: usersAxisIndex,
|
|
...axisLineConfig,
|
|
},
|
|
],
|
|
// utc: utc === 'true', //TODO(release-comparison)
|
|
isGroupedByDate: true,
|
|
showTimeInTooltip: true,
|
|
colors: [colors[0], colors[1]] as string[],
|
|
tooltip: {
|
|
trigger: 'axis' as const,
|
|
truncate: 80,
|
|
valueFormatter: (value, label, seriesParams: any) => {
|
|
const {axisIndex, dataIndex} = seriesParams || {};
|
|
const absoluteCount = getCountAtIndex(
|
|
releaseSessions?.groups,
|
|
axisIndexToSessionsField[axisIndex ?? 0],
|
|
dataIndex ?? 0
|
|
);
|
|
|
|
return label && Object.values(releaseMarkLinesLabels).includes(label)
|
|
? ''
|
|
: `<span>${formatAbbreviatedNumber(absoluteCount)} <span style="color: ${
|
|
theme.textColor
|
|
};margin-left: ${space(0.5)}">${value}%</span></span>`;
|
|
},
|
|
filter: (_, seriesParam: any) => {
|
|
const {seriesName, axisIndex} = seriesParam;
|
|
// do not display tooltips for "Users Adopted" marklines
|
|
if (
|
|
axisIndex === usersAxisIndex &&
|
|
Object.values(releaseMarkLinesLabels).includes(seriesName)
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
},
|
|
};
|
|
|
|
const {
|
|
statsPeriod: period,
|
|
start,
|
|
end,
|
|
utc,
|
|
} = getReleaseParams({
|
|
location,
|
|
releaseBounds: getReleaseBounds(release),
|
|
});
|
|
|
|
const adoptionStage = release.adoptionStages?.[project.slug]?.stage;
|
|
const adoptionStageLabel = adoptionStage ? ADOPTION_STAGE_LABELS[adoptionStage] : null;
|
|
const multipleEnvironments = environment.length === 0 || environment.length > 1;
|
|
|
|
return (
|
|
<div>
|
|
{isMobileRelease(project.platform) && (
|
|
<SidebarSection.Wrap>
|
|
<SidebarSection.Title>
|
|
{t('Adoption Stage')}
|
|
{multipleEnvironments && (
|
|
<QuestionTooltip
|
|
position="top"
|
|
title={t(
|
|
'See if a release has low adoption, been adopted by users, or replaced by another release. Select an environment above to view the stage this release is in.'
|
|
)}
|
|
size="sm"
|
|
/>
|
|
)}
|
|
</SidebarSection.Title>
|
|
<SidebarSection.Content>
|
|
{adoptionStageLabel && !multipleEnvironments ? (
|
|
<div>
|
|
<Tooltip title={adoptionStageLabel.tooltipTitle} isHoverable>
|
|
<Tag type={adoptionStageLabel.type}>{adoptionStageLabel.name}</Tag>
|
|
</Tooltip>
|
|
<AdoptionEnvironment>
|
|
{tct(`in [environment]`, {environment})}
|
|
</AdoptionEnvironment>
|
|
</div>
|
|
) : (
|
|
<NotAvailableWrapper>
|
|
<NotAvailable />
|
|
</NotAvailableWrapper>
|
|
)}
|
|
</SidebarSection.Content>
|
|
</SidebarSection.Wrap>
|
|
)}
|
|
<SidebarSection.Wrap>
|
|
<RelativeBox>
|
|
<ErrorBoundary mini>
|
|
{!loading && (
|
|
<ChartLabel top="0px">
|
|
<SidebarSection.Title>
|
|
{t('Sessions Adopted')}
|
|
<TooltipWrapper>
|
|
<QuestionTooltip
|
|
position="top"
|
|
title={t(
|
|
'Adoption compares the sessions of a release with the total sessions for this project.'
|
|
)}
|
|
size="sm"
|
|
/>
|
|
</TooltipWrapper>
|
|
</SidebarSection.Title>
|
|
</ChartLabel>
|
|
)}
|
|
|
|
{!loading && hasUsers && (
|
|
<ChartLabel top="140px">
|
|
<SidebarSection.Title>
|
|
{t('Users Adopted')}
|
|
<TooltipWrapper>
|
|
<QuestionTooltip
|
|
position="top"
|
|
title={t(
|
|
'Adoption compares the users of a release with the total users for this project.'
|
|
)}
|
|
size="sm"
|
|
/>
|
|
</TooltipWrapper>
|
|
</SidebarSection.Title>
|
|
</ChartLabel>
|
|
)}
|
|
|
|
{errored ? (
|
|
<ErrorPanel height="280px">
|
|
<IconWarning color="gray300" size="lg" />
|
|
</ErrorPanel>
|
|
) : (
|
|
<TransitionChart loading={loading} reloading={reloading} height="280px">
|
|
<TransparentLoadingMask visible={reloading} />
|
|
<ChartZoom
|
|
router={router}
|
|
period={period ?? undefined}
|
|
utc={utc === 'true'}
|
|
start={start}
|
|
end={end}
|
|
usePageDate
|
|
xAxisIndex={[sessionsAxisIndex, usersAxisIndex]}
|
|
>
|
|
{zoomRenderProps => (
|
|
<LineChart
|
|
{...chartOptions}
|
|
{...zoomRenderProps}
|
|
series={getSeries()}
|
|
transformSinglePointToLine
|
|
/>
|
|
)}
|
|
</ChartZoom>
|
|
</TransitionChart>
|
|
)}
|
|
</ErrorBoundary>
|
|
</RelativeBox>
|
|
</SidebarSection.Wrap>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const NotAvailableWrapper = styled('div')`
|
|
display: flex;
|
|
align-items: center;
|
|
`;
|
|
|
|
const ChartLabel = styled('div')<{top: string}>`
|
|
position: absolute;
|
|
top: ${p => p.top};
|
|
z-index: 1;
|
|
left: 0;
|
|
right: 0;
|
|
`;
|
|
|
|
const TooltipWrapper = styled('span')`
|
|
margin-left: ${space(0.5)};
|
|
`;
|
|
|
|
const AdoptionEnvironment = styled('span')`
|
|
color: ${p => p.theme.textColor};
|
|
margin-left: ${space(0.5)};
|
|
font-size: ${p => p.theme.fontSizeSmall};
|
|
`;
|
|
|
|
const RelativeBox = styled('div')`
|
|
position: relative;
|
|
`;
|
|
|
|
export default ReleaseAdoption;
|