Skip to content

Commit 21e3b0f

Browse files
michellewzhangadrian-codecov
authored andcommitted
feat(insights): add issues to issue-based charts on session health (#88499)
- closes #88153 - add 3 issue examples to issue-based charts on session health (issues chart on frontend, issues by release chart on mobile). - each chart shows the 3 most recently seen issues that are unresolved. - adjusted all chart heights to be the same height as the issues charts (`400px`). - the chart will dynamically resize within its `400px` box depending on how many issues are available. on mobile: <img width="805" alt="SCR-20250401-moyn" src="https://github.com/user-attachments/assets/0eb951d6-2bb5-4c85-83fb-2b26cb28b21d" /> on frontend: <img width="1232" alt="SCR-20250401-mjrc" src="https://github.com/user-attachments/assets/3c4ae354-2cdd-4dc2-86df-fbcb45776464" /> loading state: https://github.com/user-attachments/assets/d812d790-f18d-4117-a3f7-5f8b2bd261bd
1 parent 68627d8 commit 21e3b0f

17 files changed

+268
-24
lines changed

static/app/components/stream/group.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,7 @@ const Wrapper = styled(PanelItem)<{
759759
`};
760760
`;
761761

762-
const GroupSummary = styled('div')<{canSelect: boolean}>`
762+
export const GroupSummary = styled('div')<{canSelect: boolean}>`
763763
overflow: hidden;
764764
margin-left: ${p => space(p.canSelect ? 1 : 2)};
765765
margin-right: ${space(4)};

static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,6 @@ const ChartContainer = styled('div')<{height?: string | number}>`
180180
p.height ? (typeof p.height === 'string' ? p.height : `${p.height}px`) : '220px'};
181181
`;
182182

183-
const ModalChartContainer = styled('div')`
183+
export const ModalChartContainer = styled('div')`
184184
height: 360px;
185185
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import {Fragment} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import {openInsightChartModal} from 'sentry/actionCreators/modal';
5+
import {Button} from 'sentry/components/core/button';
6+
import EventOrGroupExtraDetails from 'sentry/components/eventOrGroupExtraDetails';
7+
import EventOrGroupHeader from 'sentry/components/eventOrGroupHeader';
8+
import Panel from 'sentry/components/panels/panel';
9+
import {GroupSummary} from 'sentry/components/stream/group';
10+
import {IconExpand} from 'sentry/icons';
11+
import {t} from 'sentry/locale';
12+
import {space} from 'sentry/styles/space';
13+
import type {Project} from 'sentry/types/project';
14+
import usePageFilters from 'sentry/utils/usePageFilters';
15+
import {useReleaseStats} from 'sentry/utils/useReleaseStats';
16+
import type {LegendSelection} from 'sentry/views/dashboards/widgets/common/types';
17+
import type {Plottable} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/plottable';
18+
import {TimeSeriesWidgetVisualization} from 'sentry/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization';
19+
import {Widget} from 'sentry/views/dashboards/widgets/widget/widget';
20+
import type {DiscoverSeries} from 'sentry/views/insights/common/queries/useDiscoverSeries';
21+
import {ModalChartContainer} from 'sentry/views/insights/pages/backend/laravel/styles';
22+
import {WidgetVisualizationStates} from 'sentry/views/insights/pages/backend/laravel/widgetVisualizationStates';
23+
import useRecentIssues from 'sentry/views/insights/sessions/queries/useRecentIssues';
24+
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';
25+
26+
export default function ChartWithIssues({
27+
project,
28+
series,
29+
plottables,
30+
title,
31+
description,
32+
isPending,
33+
error,
34+
legendSelection,
35+
}: {
36+
description: string;
37+
error: Error | null;
38+
isPending: boolean;
39+
plottables: Plottable[];
40+
project: Project;
41+
series: DiscoverSeries[];
42+
title: string;
43+
legendSelection?: LegendSelection | undefined;
44+
}) {
45+
const {recentIssues, isPending: isPendingRecentIssues} = useRecentIssues({
46+
projectId: project.id,
47+
});
48+
const pageFilters = usePageFilters();
49+
50+
const {releases: releasesWithDate} = useReleaseStats(pageFilters.selection);
51+
const releases =
52+
releasesWithDate?.map(({date, version}) => ({
53+
timestamp: date,
54+
version,
55+
})) ?? [];
56+
57+
const hasData = series?.length;
58+
const isLoading = isPending || isPendingRecentIssues;
59+
60+
if (isLoading) {
61+
return (
62+
<Widget
63+
height={SESSION_HEALTH_CHART_HEIGHT}
64+
Visualization={<TimeSeriesWidgetVisualization.LoadingPlaceholder />}
65+
/>
66+
);
67+
}
68+
69+
const visualization = (
70+
<WidgetVisualizationStates
71+
isEmpty={!hasData}
72+
isLoading={isLoading}
73+
error={error}
74+
VisualizationType={TimeSeriesWidgetVisualization}
75+
visualizationProps={{
76+
legendSelection,
77+
plottables,
78+
}}
79+
/>
80+
);
81+
82+
const footer = hasData && recentIssues && (
83+
<FooterIssues>
84+
{recentIssues.map((group, index) => (
85+
<GroupWrapper canSelect key={group.id}>
86+
<EventOrGroupHeader index={index} data={group} source={'session-health'} />
87+
<EventOrGroupExtraDetails data={group} showLifetime={false} />
88+
</GroupWrapper>
89+
))}
90+
</FooterIssues>
91+
);
92+
93+
return (
94+
<Widget
95+
Title={<Widget.WidgetTitle title={title} />}
96+
height={SESSION_HEALTH_CHART_HEIGHT}
97+
Visualization={visualization}
98+
Actions={
99+
<Widget.WidgetToolbar>
100+
<Widget.WidgetDescription description={description} />
101+
<Button
102+
size="xs"
103+
aria-label={t('Open Full-Screen View')}
104+
borderless
105+
icon={<IconExpand />}
106+
onClick={() => {
107+
openInsightChartModal({
108+
title,
109+
children: (
110+
<Fragment>
111+
<ModalChartContainer>
112+
<TimeSeriesWidgetVisualization
113+
releases={releases ?? []}
114+
plottables={plottables}
115+
legendSelection={legendSelection}
116+
/>
117+
</ModalChartContainer>
118+
<ModalFooterWrapper>{footer}</ModalFooterWrapper>
119+
</Fragment>
120+
),
121+
});
122+
}}
123+
/>
124+
</Widget.WidgetToolbar>
125+
}
126+
noFooterPadding
127+
Footer={footer}
128+
/>
129+
);
130+
}
131+
132+
const FooterIssues = styled('div')`
133+
display: flex;
134+
flex-direction: column;
135+
`;
136+
137+
const GroupWrapper = styled(GroupSummary)`
138+
border-top: 1px solid ${p => p.theme.border};
139+
padding: ${space(1)} ${space(0.5)} ${space(1.5)} ${space(0.5)};
140+
141+
&:first-child {
142+
border-top: none;
143+
}
144+
`;
145+
146+
const ModalFooterWrapper = styled(Panel)`
147+
margin-top: 50px;
148+
`;

static/app/views/insights/sessions/charts/crashFreeSessionsChart.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {t, tct} from 'sentry/locale';
33
import {formatSeriesName} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatSeriesName';
44
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
55
import useCrashFreeSessions from 'sentry/views/insights/sessions/queries/useCrashFreeSessions';
6+
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';
67

78
export default function CrashFreeSessionsChart() {
89
const {series, releases, isPending, error} = useCrashFreeSessions();
@@ -17,6 +18,7 @@ export default function CrashFreeSessionsChart() {
1718
return (
1819
<InsightsLineChartWidget
1920
title={t('Crash Free Sessions')}
21+
height={SESSION_HEALTH_CHART_HEIGHT}
2022
description={tct(
2123
'The percent of sessions terminating without a crash. See [link:session status].',
2224
{

static/app/views/insights/sessions/charts/errorFreeSessionsChart.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import ExternalLink from 'sentry/components/links/externalLink';
22
import {t, tct} from 'sentry/locale';
33
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
44
import useErrorFreeSessions from 'sentry/views/insights/sessions/queries/useErrorFreeSessions';
5+
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';
56

67
export default function ErrorFreeSessionsChart() {
78
const {series, isPending, error} = useErrorFreeSessions();
@@ -13,6 +14,7 @@ export default function ErrorFreeSessionsChart() {
1314
return (
1415
<InsightsLineChartWidget
1516
title={t('Error Free Sessions')}
17+
height={SESSION_HEALTH_CHART_HEIGHT}
1618
description={tct(
1719
'The percent of sessions terminating without a single error occurring. See [link:session status].',
1820
{

static/app/views/insights/sessions/charts/newAndResolvedIssueChart.tsx

+30-7
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,45 @@
1+
import {useTheme} from '@emotion/react';
2+
13
import {t} from 'sentry/locale';
2-
import {InsightsBarChartWidget} from 'sentry/views/insights/common/components/insightsBarChartWidget';
4+
import type {Project} from 'sentry/types/project';
5+
import {Bars} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/bars';
6+
import {convertSeriesToTimeseries} from 'sentry/views/insights/common/utils/convertSeriesToTimeseries';
7+
import ChartWithIssues from 'sentry/views/insights/sessions/charts/chartWithIssues';
38
import useNewAndResolvedIssues from 'sentry/views/insights/sessions/queries/useNewAndResolvedIssues';
49

5-
export default function NewAndResolvedIssueChart({type}: {type: 'issue' | 'feedback'}) {
10+
export default function NewAndResolvedIssueChart({
11+
type,
12+
project,
13+
}: {
14+
project: Project;
15+
type: 'issue' | 'feedback';
16+
}) {
617
const {series, isPending, error} = useNewAndResolvedIssues({type});
18+
const theme = useTheme();
719

820
const aliases = {
921
new_issues_count: `new_${type}s`,
1022
resolved_issues_count: `resolved_${type}s`,
1123
};
1224

25+
const colorPalette = theme.chart.getColorPalette(series.length - 2);
26+
const title = type === 'issue' ? t('Issues') : t('User Feedback');
27+
const plottables = series.map(
28+
(ts, index) =>
29+
new Bars(convertSeriesToTimeseries(ts), {
30+
alias: aliases[ts.seriesName as keyof typeof aliases],
31+
color: colorPalette[index],
32+
})
33+
);
34+
1335
return (
14-
<InsightsBarChartWidget
15-
title={type === 'issue' ? t('Issues') : t('User Feedback')}
16-
description={t('New and resolved %s counts over time.', type)}
17-
aliases={aliases}
36+
<ChartWithIssues
37+
project={project}
1838
series={series}
19-
isLoading={isPending}
39+
title={title}
40+
description={t('New and resolved %s counts over time.', type)}
41+
plottables={plottables}
42+
isPending={isPending}
2043
error={error}
2144
/>
2245
);
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,38 @@
1+
import {useTheme} from '@emotion/react';
2+
13
import {t} from 'sentry/locale';
2-
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
4+
import type {Project} from 'sentry/types/project';
5+
import {Line} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/line';
6+
import {convertSeriesToTimeseries} from 'sentry/views/insights/common/utils/convertSeriesToTimeseries';
7+
import ChartWithIssues from 'sentry/views/insights/sessions/charts/chartWithIssues';
38
import useReleaseNewIssues from 'sentry/views/insights/sessions/queries/useReleaseNewIssues';
49

5-
export default function ReleaseNewIssuesChart() {
10+
export default function ReleaseNewIssuesChart({project}: {project: Project}) {
611
const {series, isPending, error} = useReleaseNewIssues();
12+
const theme = useTheme();
13+
14+
const colorPalette = theme.chart.getColorPalette(series.length - 2);
15+
const plottables = series.map(
16+
(ts, index) =>
17+
new Line(convertSeriesToTimeseries(ts), {
18+
alias: ts.seriesName,
19+
color: colorPalette[index],
20+
})
21+
);
722

823
return (
9-
<InsightsLineChartWidget
24+
<ChartWithIssues
25+
project={project}
26+
series={series}
1027
title={t('New Issues by Release')}
1128
description={t('New issue counts over time, grouped by release.')}
12-
series={series}
13-
isLoading={isPending}
29+
isPending={isPending}
30+
error={error}
1431
legendSelection={{
1532
// disable the 'other' series by default since its large values can cause the other lines to be insignificant
1633
other: false,
1734
}}
18-
error={error}
35+
plottables={plottables}
1936
/>
2037
);
2138
}

static/app/views/insights/sessions/charts/releaseSessionCountChart.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {t} from 'sentry/locale';
22
import {formatSeriesName} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatSeriesName';
33
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
44
import useReleaseSessionCounts from 'sentry/views/insights/sessions/queries/useReleaseSessionCounts';
5+
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';
56

67
export default function ReleaseSessionCountChart() {
78
const {series, releases, isPending, error} = useReleaseSessionCounts();
@@ -14,6 +15,7 @@ export default function ReleaseSessionCountChart() {
1415
return (
1516
<InsightsLineChartWidget
1617
title={t('Total Sessions by Release')}
18+
height={SESSION_HEALTH_CHART_HEIGHT}
1719
description={t(
1820
'The total number of sessions per release. The 5 most recent releases are shown.'
1921
)}

static/app/views/insights/sessions/charts/releaseSessionPercentageChart.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {t} from 'sentry/locale';
22
import {formatSeriesName} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatSeriesName';
33
import {InsightsAreaChartWidget} from 'sentry/views/insights/common/components/insightsAreaChartWidget';
44
import useReleaseSessionPercentage from 'sentry/views/insights/sessions/queries/useReleaseSessionPercentage';
5+
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';
56

67
export default function ReleaseSessionPercentageChart() {
78
const {series, releases, isPending, error} = useReleaseSessionPercentage();
@@ -14,6 +15,7 @@ export default function ReleaseSessionPercentageChart() {
1415
return (
1516
<InsightsAreaChartWidget
1617
title={t('Release Adoption')}
18+
height={SESSION_HEALTH_CHART_HEIGHT}
1719
description={t(
1820
'The percentage of total sessions that each release accounted for. The 5 most recent releases are shown.'
1921
)}

static/app/views/insights/sessions/charts/sessionHealthCountChart.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import ExternalLink from 'sentry/components/links/externalLink';
22
import {t, tct} from 'sentry/locale';
33
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
44
import useSessionHealthBreakdown from 'sentry/views/insights/sessions/queries/useSessionHealthBreakdown';
5+
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';
56

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

1617
return (
1718
<InsightsLineChartWidget
19+
height={SESSION_HEALTH_CHART_HEIGHT}
1820
title={t('Session Counts')}
1921
description={tct(
2022
'The count of sessions with each health status. See [link:session status].',

static/app/views/insights/sessions/charts/sessionHealthRateChart.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import ExternalLink from 'sentry/components/links/externalLink';
22
import {t, tct} from 'sentry/locale';
33
import {InsightsAreaChartWidget} from 'sentry/views/insights/common/components/insightsAreaChartWidget';
44
import useSessionHealthBreakdown from 'sentry/views/insights/sessions/queries/useSessionHealthBreakdown';
5+
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';
56

67
export default function SessionHealthRateChart() {
78
const {series, isPending, error} = useSessionHealthBreakdown({type: 'rate'});
@@ -16,6 +17,7 @@ export default function SessionHealthRateChart() {
1617
return (
1718
<InsightsAreaChartWidget
1819
title={t('Session Health')}
20+
height={SESSION_HEALTH_CHART_HEIGHT}
1921
description={tct(
2022
'The percent of sessions with each health status. See [link:session status].',
2123
{

static/app/views/insights/sessions/charts/userHealthCountChart.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import ExternalLink from 'sentry/components/links/externalLink';
22
import {t, tct} from 'sentry/locale';
33
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
44
import useUserHealthBreakdown from 'sentry/views/insights/sessions/queries/useUserHealthBreakdown';
5+
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';
56

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

1617
return (
1718
<InsightsLineChartWidget
19+
height={SESSION_HEALTH_CHART_HEIGHT}
1820
title={t('User Counts')}
1921
description={tct(
2022
'Breakdown of total [linkUsers:users], grouped by [linkStatus:health status].',

static/app/views/insights/sessions/charts/userHealthRateChart.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import ExternalLink from 'sentry/components/links/externalLink';
22
import {t, tct} from 'sentry/locale';
33
import {InsightsAreaChartWidget} from 'sentry/views/insights/common/components/insightsAreaChartWidget';
44
import useUserHealthBreakdown from 'sentry/views/insights/sessions/queries/useUserHealthBreakdown';
5+
import {SESSION_HEALTH_CHART_HEIGHT} from 'sentry/views/insights/sessions/utils/sessions';
56

67
export default function UserHealthRateChart() {
78
const {series, isPending, error} = useUserHealthBreakdown({type: 'rate'});
@@ -16,6 +17,7 @@ export default function UserHealthRateChart() {
1617
return (
1718
<InsightsAreaChartWidget
1819
title={t('User Health')}
20+
height={SESSION_HEALTH_CHART_HEIGHT}
1921
description={tct(
2022
'The percent of [linkUsers:users] with each [linkStatus:health status].',
2123
{

0 commit comments

Comments
 (0)