Skip to content

feat(insights): add issues to issue-based charts on session health #88499

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion static/app/components/stream/group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ const Wrapper = styled(PanelItem)<{
`};
`;

const GroupSummary = styled('div')<{canSelect: boolean}>`
export const GroupSummary = styled('div')<{canSelect: boolean}>`
overflow: hidden;
margin-left: ${p => space(p.canSelect ? 1 : 2)};
margin-right: ${space(4)};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,6 @@ const ChartContainer = styled('div')<{height?: string | number}>`
p.height ? (typeof p.height === 'string' ? p.height : `${p.height}px`) : '220px'};
`;

const ModalChartContainer = styled('div')`
export const ModalChartContainer = styled('div')`
height: 360px;
`;
148 changes: 148 additions & 0 deletions static/app/views/insights/sessions/charts/chartWithIssues.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import {Fragment} from 'react';
import styled from '@emotion/styled';

import {openInsightChartModal} from 'sentry/actionCreators/modal';
import {Button} from 'sentry/components/core/button';
import EventOrGroupExtraDetails from 'sentry/components/eventOrGroupExtraDetails';
import EventOrGroupHeader from 'sentry/components/eventOrGroupHeader';
import Panel from 'sentry/components/panels/panel';
import {GroupSummary} from 'sentry/components/stream/group';
import {IconExpand} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Project} from 'sentry/types/project';
import usePageFilters from 'sentry/utils/usePageFilters';
import {useReleaseStats} from 'sentry/utils/useReleaseStats';
import type {LegendSelection} from 'sentry/views/dashboards/widgets/common/types';
import type {Plottable} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/plottable';
import {TimeSeriesWidgetVisualization} from 'sentry/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization';
import {Widget} from 'sentry/views/dashboards/widgets/widget/widget';
import type {DiscoverSeries} from 'sentry/views/insights/common/queries/useDiscoverSeries';
import {ModalChartContainer} from 'sentry/views/insights/pages/backend/laravel/styles';
import {WidgetVisualizationStates} from 'sentry/views/insights/pages/backend/laravel/widgetVisualizationStates';
import useRecentIssues from 'sentry/views/insights/sessions/queries/useRecentIssues';
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';

export default function ChartWithIssues({
project,
series,
plottables,
title,
description,
isPending,
error,
legendSelection,
}: {
description: string;
error: Error | null;
isPending: boolean;
plottables: Plottable[];
project: Project;
series: DiscoverSeries[];
title: string;
legendSelection?: LegendSelection | undefined;
}) {
const {recentIssues, isPending: isPendingRecentIssues} = useRecentIssues({
projectId: project.id,
});
const pageFilters = usePageFilters();

const {releases: releasesWithDate} = useReleaseStats(pageFilters.selection);
const releases =
releasesWithDate?.map(({date, version}) => ({
timestamp: date,
version,
})) ?? [];

const hasData = series?.length;
const isLoading = isPending || isPendingRecentIssues;

if (isLoading) {
return (
<Widget
height={SESSION_HEALTH_CHART_HEIGHT}
Visualization={<TimeSeriesWidgetVisualization.LoadingPlaceholder />}
/>
);
}

const visualization = (
<WidgetVisualizationStates
isEmpty={!hasData}
isLoading={isLoading}
error={error}
VisualizationType={TimeSeriesWidgetVisualization}
visualizationProps={{
legendSelection,
plottables,
}}
/>
);

const footer = hasData && recentIssues && (
<FooterIssues>
{recentIssues.map((group, index) => (
<GroupWrapper canSelect key={group.id}>
<EventOrGroupHeader index={index} data={group} source={'session-health'} />
<EventOrGroupExtraDetails data={group} showLifetime={false} />
</GroupWrapper>
))}
</FooterIssues>
);

return (
<Widget
Title={<Widget.WidgetTitle title={title} />}
height={SESSION_HEALTH_CHART_HEIGHT}
Visualization={visualization}
Actions={
<Widget.WidgetToolbar>
<Widget.WidgetDescription description={description} />
<Button
size="xs"
aria-label={t('Open Full-Screen View')}
borderless
icon={<IconExpand />}
onClick={() => {
openInsightChartModal({
title,
children: (
<Fragment>
<ModalChartContainer>
<TimeSeriesWidgetVisualization
releases={releases ?? []}
plottables={plottables}
legendSelection={legendSelection}
/>
</ModalChartContainer>
<ModalFooterWrapper>{footer}</ModalFooterWrapper>
</Fragment>
),
});
}}
/>
</Widget.WidgetToolbar>
}
noFooterPadding
Footer={footer}
/>
);
}

const FooterIssues = styled('div')`
display: flex;
flex-direction: column;
`;

const GroupWrapper = styled(GroupSummary)`
border-top: 1px solid ${p => p.theme.border};
padding: ${space(1)} ${space(0.5)} ${space(1.5)} ${space(0.5)};

&:first-child {
border-top: none;
}
`;

const ModalFooterWrapper = styled(Panel)`
margin-top: 50px;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {t, tct} from 'sentry/locale';
import {formatSeriesName} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatSeriesName';
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
import useCrashFreeSessions from 'sentry/views/insights/sessions/queries/useCrashFreeSessions';
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';

export default function CrashFreeSessionsChart() {
const {series, releases, isPending, error} = useCrashFreeSessions();
Expand All @@ -17,6 +18,7 @@ export default function CrashFreeSessionsChart() {
return (
<InsightsLineChartWidget
title={t('Crash Free Sessions')}
height={SESSION_HEALTH_CHART_HEIGHT}
description={tct(
'The percent of sessions terminating without a crash. See [link:session status].',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {t, tct} from 'sentry/locale';
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
import useErrorFreeSessions from 'sentry/views/insights/sessions/queries/useErrorFreeSessions';
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';

export default function ErrorFreeSessionsChart() {
const {series, isPending, error} = useErrorFreeSessions();
Expand All @@ -13,6 +14,7 @@ export default function ErrorFreeSessionsChart() {
return (
<InsightsLineChartWidget
title={t('Error Free Sessions')}
height={SESSION_HEALTH_CHART_HEIGHT}
description={tct(
'The percent of sessions terminating without a single error occurring. See [link:session status].',
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,45 @@
import {useTheme} from '@emotion/react';

import {t} from 'sentry/locale';
import {InsightsBarChartWidget} from 'sentry/views/insights/common/components/insightsBarChartWidget';
import type {Project} from 'sentry/types/project';
import {Bars} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/bars';
import {convertSeriesToTimeseries} from 'sentry/views/insights/common/utils/convertSeriesToTimeseries';
import ChartWithIssues from 'sentry/views/insights/sessions/charts/chartWithIssues';
import useNewAndResolvedIssues from 'sentry/views/insights/sessions/queries/useNewAndResolvedIssues';

export default function NewAndResolvedIssueChart({type}: {type: 'issue' | 'feedback'}) {
export default function NewAndResolvedIssueChart({
type,
project,
}: {
project: Project;
type: 'issue' | 'feedback';
}) {
const {series, isPending, error} = useNewAndResolvedIssues({type});
const theme = useTheme();

const aliases = {
new_issues_count: `new_${type}s`,
resolved_issues_count: `resolved_${type}s`,
};

const colorPalette = theme.chart.getColorPalette(series.length - 2);
const title = type === 'issue' ? t('Issues') : t('User Feedback');
const plottables = series.map(
(ts, index) =>
new Bars(convertSeriesToTimeseries(ts), {
alias: aliases[ts.seriesName as keyof typeof aliases],
color: colorPalette[index],
})
);

return (
<InsightsBarChartWidget
title={type === 'issue' ? t('Issues') : t('User Feedback')}
description={t('New and resolved %s counts over time.', type)}
aliases={aliases}
<ChartWithIssues
project={project}
series={series}
isLoading={isPending}
title={title}
description={t('New and resolved %s counts over time.', type)}
plottables={plottables}
isPending={isPending}
error={error}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
import {useTheme} from '@emotion/react';

import {t} from 'sentry/locale';
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
import type {Project} from 'sentry/types/project';
import {Line} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/line';
import {convertSeriesToTimeseries} from 'sentry/views/insights/common/utils/convertSeriesToTimeseries';
import ChartWithIssues from 'sentry/views/insights/sessions/charts/chartWithIssues';
import useReleaseNewIssues from 'sentry/views/insights/sessions/queries/useReleaseNewIssues';

export default function ReleaseNewIssuesChart() {
export default function ReleaseNewIssuesChart({project}: {project: Project}) {
const {series, isPending, error} = useReleaseNewIssues();
const theme = useTheme();

const colorPalette = theme.chart.getColorPalette(series.length - 2);
const plottables = series.map(
(ts, index) =>
new Line(convertSeriesToTimeseries(ts), {
alias: ts.seriesName,
color: colorPalette[index],
})
);

return (
<InsightsLineChartWidget
<ChartWithIssues
project={project}
series={series}
title={t('New Issues by Release')}
description={t('New issue counts over time, grouped by release.')}
series={series}
isLoading={isPending}
isPending={isPending}
error={error}
legendSelection={{
// disable the 'other' series by default since its large values can cause the other lines to be insignificant
other: false,
}}
error={error}
plottables={plottables}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {t} from 'sentry/locale';
import {formatSeriesName} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatSeriesName';
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
import useReleaseSessionCounts from 'sentry/views/insights/sessions/queries/useReleaseSessionCounts';
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';

export default function ReleaseSessionCountChart() {
const {series, releases, isPending, error} = useReleaseSessionCounts();
Expand All @@ -14,6 +15,7 @@ export default function ReleaseSessionCountChart() {
return (
<InsightsLineChartWidget
title={t('Total Sessions by Release')}
height={SESSION_HEALTH_CHART_HEIGHT}
description={t(
'The total number of sessions per release. The 5 most recent releases are shown.'
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {t} from 'sentry/locale';
import {formatSeriesName} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatSeriesName';
import {InsightsAreaChartWidget} from 'sentry/views/insights/common/components/insightsAreaChartWidget';
import useReleaseSessionPercentage from 'sentry/views/insights/sessions/queries/useReleaseSessionPercentage';
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';

export default function ReleaseSessionPercentageChart() {
const {series, releases, isPending, error} = useReleaseSessionPercentage();
Expand All @@ -14,6 +15,7 @@ export default function ReleaseSessionPercentageChart() {
return (
<InsightsAreaChartWidget
title={t('Release Adoption')}
height={SESSION_HEALTH_CHART_HEIGHT}
description={t(
'The percentage of total sessions that each release accounted for. The 5 most recent releases are shown.'
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {t, tct} from 'sentry/locale';
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
import useSessionHealthBreakdown from 'sentry/views/insights/sessions/queries/useSessionHealthBreakdown';
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';

export default function SessionHealthCountChart() {
const {series, isPending, error} = useSessionHealthBreakdown({type: 'count'});
Expand All @@ -15,6 +16,7 @@ export default function SessionHealthCountChart() {

return (
<InsightsLineChartWidget
height={SESSION_HEALTH_CHART_HEIGHT}
title={t('Session Counts')}
description={tct(
'The count of sessions with each health status. See [link:session status].',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {t, tct} from 'sentry/locale';
import {InsightsAreaChartWidget} from 'sentry/views/insights/common/components/insightsAreaChartWidget';
import useSessionHealthBreakdown from 'sentry/views/insights/sessions/queries/useSessionHealthBreakdown';
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';

export default function SessionHealthRateChart() {
const {series, isPending, error} = useSessionHealthBreakdown({type: 'rate'});
Expand All @@ -16,6 +17,7 @@ export default function SessionHealthRateChart() {
return (
<InsightsAreaChartWidget
title={t('Session Health')}
height={SESSION_HEALTH_CHART_HEIGHT}
description={tct(
'The percent of sessions with each health status. See [link:session status].',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {t, tct} from 'sentry/locale';
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
import useUserHealthBreakdown from 'sentry/views/insights/sessions/queries/useUserHealthBreakdown';
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';

export default function UserHealthCountChart() {
const {series, isPending, error} = useUserHealthBreakdown({type: 'count'});
Expand All @@ -15,6 +16,7 @@ export default function UserHealthCountChart() {

return (
<InsightsLineChartWidget
height={SESSION_HEALTH_CHART_HEIGHT}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like every chart has the same height 👍

this'll make it easy to put any chart in any position per #88502
also, we'll get a merge conflict from editing similar lines, but it'll be easy to resolve.
the resolution will be about the same in all cases:

      height={SESSION_HEALTH_CHART_HEIGHT}
      title={CHART_TITLES.zzzzz}
      interactiveTitle={() => (
        <ChartSelectionTitle title={CHART_TITLES.zzzzz} />
      )}

title={t('User Counts')}
description={tct(
'Breakdown of total [linkUsers:users], grouped by [linkStatus:health status].',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ExternalLink from 'sentry/components/links/externalLink';
import {t, tct} from 'sentry/locale';
import {InsightsAreaChartWidget} from 'sentry/views/insights/common/components/insightsAreaChartWidget';
import useUserHealthBreakdown from 'sentry/views/insights/sessions/queries/useUserHealthBreakdown';
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';

export default function UserHealthRateChart() {
const {series, isPending, error} = useUserHealthBreakdown({type: 'rate'});
Expand All @@ -16,6 +17,7 @@ export default function UserHealthRateChart() {
return (
<InsightsAreaChartWidget
title={t('User Health')}
height={SESSION_HEALTH_CHART_HEIGHT}
description={tct(
'The percent of [linkUsers:users] with each [linkStatus:health status].',
{
Expand Down
Loading
Loading