Skip to content

Commit 5633d73

Browse files
feat(insights): add issues to issue-based charts on session health
1 parent 846e3d0 commit 5633d73

16 files changed

+259
-24
lines changed

static/app/components/stream/group.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@ const Wrapper = styled(PanelItem)<{
827827
`};
828828
`;
829829

830-
const GroupSummary = styled('div')<{canSelect: boolean; hasNewLayout: boolean}>`
830+
export const GroupSummary = styled('div')<{canSelect: boolean; hasNewLayout: boolean}>`
831831
overflow: hidden;
832832
margin-left: ${p => space(p.canSelect ? 1 : 2)};
833833
margin-right: ${space(1)};

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

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

179-
const ModalChartContainer = styled('div')`
179+
export const ModalChartContainer = styled('div')`
180180
height: 360px;
181181
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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+
25+
export default function ChartWithIssues({
26+
project,
27+
series,
28+
plottables,
29+
title,
30+
description,
31+
isPending,
32+
error,
33+
legendSelection,
34+
}: {
35+
description: string;
36+
error: Error | null;
37+
isPending: boolean;
38+
plottables: Plottable[];
39+
project: Project;
40+
series: DiscoverSeries[];
41+
title: string;
42+
legendSelection?: LegendSelection | undefined;
43+
}) {
44+
const {recentIssues, isPending: isPendingRecentIssues} = useRecentIssues({
45+
projectId: project.id,
46+
});
47+
const pageFilters = usePageFilters();
48+
49+
const {releases: releasesWithDate} = useReleaseStats(pageFilters.selection);
50+
const releases =
51+
releasesWithDate?.map(({date, version}) => ({
52+
timestamp: date,
53+
version,
54+
})) ?? [];
55+
56+
const hasData = series?.length;
57+
const isLoading = isPending || isPendingRecentIssues;
58+
59+
if (isLoading) {
60+
return (
61+
<Widget
62+
height={400}
63+
Visualization={<TimeSeriesWidgetVisualization.LoadingPlaceholder />}
64+
/>
65+
);
66+
}
67+
68+
const visualization = (
69+
<WidgetVisualizationStates
70+
isEmpty={!hasData}
71+
isLoading={isLoading}
72+
error={error}
73+
VisualizationType={TimeSeriesWidgetVisualization}
74+
visualizationProps={{
75+
legendSelection,
76+
plottables,
77+
}}
78+
/>
79+
);
80+
81+
const footer = hasData && recentIssues && (
82+
<FooterIssues>
83+
{recentIssues.map((group, index) => (
84+
<GroupWrapper key={group.id}>
85+
<GroupSummary canSelect hasNewLayout>
86+
<EventOrGroupHeader index={index} data={group} source={'session-health'} />
87+
<EventOrGroupExtraDetails data={group} showLifetime={false} />
88+
</GroupSummary>
89+
</GroupWrapper>
90+
))}
91+
</FooterIssues>
92+
);
93+
94+
return (
95+
<Widget
96+
Title={<Widget.WidgetTitle title={title} />}
97+
height={400}
98+
Visualization={visualization}
99+
Actions={
100+
<Widget.WidgetToolbar>
101+
<Widget.WidgetDescription description={description} />
102+
<Button
103+
size="xs"
104+
aria-label={t('Open Full-Screen View')}
105+
borderless
106+
icon={<IconExpand />}
107+
onClick={() => {
108+
openInsightChartModal({
109+
title,
110+
children: (
111+
<Fragment>
112+
<ModalChartContainer>
113+
<TimeSeriesWidgetVisualization
114+
releases={releases ?? []}
115+
plottables={plottables}
116+
legendSelection={legendSelection}
117+
/>
118+
</ModalChartContainer>
119+
<FooterWrapper>{footer}</FooterWrapper>
120+
</Fragment>
121+
),
122+
});
123+
}}
124+
/>
125+
</Widget.WidgetToolbar>
126+
}
127+
noFooterPadding
128+
Footer={footer}
129+
/>
130+
);
131+
}
132+
133+
const FooterIssues = styled('div')`
134+
display: flex;
135+
flex-direction: column;
136+
`;
137+
138+
const GroupWrapper = styled('div')`
139+
border-top: 1px solid ${p => p.theme.border};
140+
padding: ${space(1)} ${space(0.5)} ${space(1.5)} ${space(0.5)};
141+
142+
&:first-child {
143+
border-top: none;
144+
}
145+
`;
146+
147+
const FooterWrapper = styled(Panel)`
148+
margin-top: 50px;
149+
`;

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

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default function CrashFreeSessionsChart() {
1717
return (
1818
<InsightsLineChartWidget
1919
title={t('Crash Free Sessions')}
20+
height={400}
2021
description={tct(
2122
'The percent of sessions terminating without a crash. See [link:session status].',
2223
{

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

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default function ErrorFreeSessionsChart() {
1313
return (
1414
<InsightsLineChartWidget
1515
title={t('Error Free Sessions')}
16+
height={400}
1617
description={tct(
1718
'The percent of sessions terminating without a single error occurring. See [link:session status].',
1819
{

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

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default function ReleaseSessionCountChart() {
1414
return (
1515
<InsightsLineChartWidget
1616
title={t('Total Sessions by Release')}
17+
height={400}
1718
description={t(
1819
'The total number of sessions per release. The 5 most recent releases are shown.'
1920
)}

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

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default function ReleaseSessionPercentageChart() {
1414
return (
1515
<InsightsAreaChartWidget
1616
title={t('Release Adoption')}
17+
height={400}
1718
description={t(
1819
'The percentage of total sessions that each release accounted for. The 5 most recent releases are shown.'
1920
)}

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

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default function SessionHealthCountChart() {
1515

1616
return (
1717
<InsightsLineChartWidget
18+
height={400}
1819
title={t('Session Counts')}
1920
description={tct(
2021
'The count of sessions with each health status. See [link:session status].',

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

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default function SessionHealthRateChart() {
1616
return (
1717
<InsightsAreaChartWidget
1818
title={t('Session Health')}
19+
height={400}
1920
description={tct(
2021
'The percent of sessions with each health status. See [link:session status].',
2122
{

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

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default function UserHealthCountChart() {
1515

1616
return (
1717
<InsightsLineChartWidget
18+
height={400}
1819
title={t('User Counts')}
1920
description={tct(
2021
'Breakdown of total [linkUsers:users], grouped by [linkStatus:health status].',

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

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default function UserHealthRateChart() {
1616
return (
1717
<InsightsAreaChartWidget
1818
title={t('User Health')}
19+
height={400}
1920
description={tct(
2021
'The percent of [linkUsers:users] with each [linkStatus:health status].',
2122
{

static/app/views/insights/sessions/queries/useProjectHasSessions.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ export default function useProjectHasSessions() {
88
const projectIds = selection.projects;
99

1010
const projects = projectIds.length
11-
? projectIds.map(projectId => {
12-
return allProjects.find(p => p.id === projectId.toString());
13-
})
11+
? projectIds
12+
.map(projectId => allProjects.find(p => p.id === projectId.toString()))
13+
.filter(project => project !== undefined)
1414
: allProjects;
1515

16-
const hasSessionData = projects.some(p => p?.hasSessions);
16+
const hasSessionData = projects.some(p => p.hasSessions);
1717

18-
return hasSessionData;
18+
return {projects, hasSessionData};
1919
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type {Group} from 'sentry/types/group';
2+
import {useApiQuery} from 'sentry/utils/queryClient';
3+
import {useLocation} from 'sentry/utils/useLocation';
4+
import useOrganization from 'sentry/utils/useOrganization';
5+
6+
export default function useRecentIssues({projectId}: {projectId: string}) {
7+
const organization = useOrganization();
8+
const location = useLocation();
9+
10+
const locationQuery = {
11+
...location,
12+
query: {
13+
...location.query,
14+
query: undefined,
15+
width: undefined,
16+
cursor: undefined,
17+
},
18+
};
19+
20+
// hardcode 14d since the API does not support all statsPeriods
21+
const {data: recentIssues, isPending} = useApiQuery<Group[]>(
22+
[
23+
`/projects/${organization.slug}/${projectId}/issues/`,
24+
{query: {...locationQuery.query, statsPeriod: '14d', limit: 3}},
25+
],
26+
{staleTime: 0}
27+
);
28+
29+
return {recentIssues, isPending};
30+
}

0 commit comments

Comments
 (0)