Skip to content

Commit 3b5fd2e

Browse files
feat(ui): Add release bubbles to issue details (#86950)
Adds new release bubbles to the issue details graph ![image](https://github.com/user-attachments/assets/2eee7aa2-4971-43af-8031-892aa38d7efe) ref #86946 depends on - #86841 - #86840 --------- Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent e8370df commit 3b5fd2e

File tree

7 files changed

+346
-127
lines changed

7 files changed

+346
-127
lines changed

static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx

+14-8
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,16 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati
114114
} = useReleaseBubbles({
115115
chartRenderer: ({start: trimStart, end: trimEnd}) => {
116116
return (
117-
<TimeSeriesWidgetVisualization
118-
{...props}
119-
disableReleaseNavigation
120-
plottables={props.plottables.map(plottable =>
121-
plottable.constrain(trimStart, trimEnd)
122-
)}
123-
showReleaseAs="line"
124-
/>
117+
<DrawerWidgetWrapper>
118+
<TimeSeriesWidgetVisualization
119+
{...props}
120+
disableReleaseNavigation
121+
plottables={props.plottables.map(plottable =>
122+
plottable.constrain(trimStart, trimEnd)
123+
)}
124+
showReleaseAs="line"
125+
/>
126+
</DrawerWidgetWrapper>
125127
);
126128
},
127129
minTime: earliestTimeStamp ? new Date(earliestTimeStamp).getTime() : undefined,
@@ -545,4 +547,8 @@ const LoadingMask = styled(TransparentLoadingMask)`
545547
background: ${p => p.theme.background};
546548
`;
547549

550+
const DrawerWidgetWrapper = styled('div')`
551+
height: 220px;
552+
`;
553+
548554
TimeSeriesWidgetVisualization.LoadingPlaceholder = LoadingPanel;

static/app/views/issueDetails/streamline/eventGraph.tsx

+142-36
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from 'react';
99
import {useTheme} from '@emotion/react';
1010
import styled from '@emotion/styled';
11-
import {useResizeObserver} from '@react-aria/utils';
11+
import {mergeRefs, useResizeObserver} from '@react-aria/utils';
1212
import Color from 'color';
1313

1414
import {BarChart, type BarChartSeries} from 'sentry/components/charts/barChart';
@@ -22,7 +22,7 @@ import InteractionStateLayer from 'sentry/components/interactionStateLayer';
2222
import Placeholder from 'sentry/components/placeholder';
2323
import {t, tct, tn} from 'sentry/locale';
2424
import {space} from 'sentry/styles/space';
25-
import type {SeriesDataUnit} from 'sentry/types/echarts';
25+
import type {ReactEchartsRef, SeriesDataUnit} from 'sentry/types/echarts';
2626
import type {Event} from 'sentry/types/event';
2727
import type {Group} from 'sentry/types/group';
2828
import type {EventsStats, MultiSeriesEventsStats} from 'sentry/types/organization';
@@ -33,6 +33,7 @@ import {useApiQuery} from 'sentry/utils/queryClient';
3333
import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
3434
import {useLocation} from 'sentry/utils/useLocation';
3535
import useOrganization from 'sentry/utils/useOrganization';
36+
import {useReleaseStats} from 'sentry/utils/useReleaseStats';
3637
import {getBucketSize} from 'sentry/views/dashboards/utils/getBucketSize';
3738
import {useIssueDetails} from 'sentry/views/issueDetails/streamline/context';
3839
import useFlagSeries from 'sentry/views/issueDetails/streamline/hooks/featureFlags/useFlagSeries';
@@ -44,6 +45,7 @@ import {
4445
import {useReleaseMarkLineSeries} from 'sentry/views/issueDetails/streamline/hooks/useReleaseMarkLineSeries';
4546
import {Tab} from 'sentry/views/issueDetails/types';
4647
import {useGroupDetailsRoute} from 'sentry/views/issueDetails/useGroupDetailsRoute';
48+
import {useReleaseBubbles} from 'sentry/views/releases/releaseBubbles/useReleaseBubbles';
4749

4850
const enum EventGraphSeries {
4951
EVENT = 'event',
@@ -54,6 +56,24 @@ interface EventGraphProps {
5456
event: Event | undefined;
5557
group: Group;
5658
className?: string;
59+
/**
60+
* Disables navigation via router when the chart is zoomed. This is so the
61+
* release bubbles can zoom in on the chart when it renders and not trigger
62+
* navigation (which would update the page filters and affect the main
63+
* chart).
64+
*/
65+
disableZoomNavigation?: boolean;
66+
ref?: React.Ref<ReactEchartsRef>;
67+
/**
68+
* Configures showing releases on the chart as bubbles or lines. This is used
69+
* when showing the chart inside of the flyout drawer. Bubbles are shown when
70+
* this prop is anything besides "line".
71+
*/
72+
showReleasesAs?: 'line' | 'bubble';
73+
/**
74+
* Enable/disables showing the event and user summary
75+
*/
76+
showSummary?: boolean;
5777
style?: CSSProperties;
5878
}
5979

@@ -76,7 +96,15 @@ function createSeriesAndCount(stats: EventsStats) {
7696
);
7797
}
7898

79-
export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
99+
export function EventGraph({
100+
group,
101+
event,
102+
disableZoomNavigation = false,
103+
showReleasesAs,
104+
showSummary = true,
105+
ref,
106+
...styleProps
107+
}: EventGraphProps) {
80108
const theme = useTheme();
81109
const organization = useOrganization();
82110
const chartContainerRef = useRef<HTMLDivElement | null>(null);
@@ -116,6 +144,8 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
116144
},
117145
});
118146

147+
const hasReleaseBubblesSeries = organization.features.includes('release-bubbles-ui');
148+
119149
const noQueryEventView = eventView.clone();
120150
noQueryEventView.query = `issue:${group.shortId}`;
121151
noQueryEventView.environment = [];
@@ -200,7 +230,75 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
200230
event,
201231
group,
202232
});
203-
const releaseSeries = useReleaseMarkLineSeries({group});
233+
234+
const [legendSelected, setLegendSelected] = useLocalStorageState(
235+
'issue-details-graph-legend',
236+
{
237+
['Feature Flags']: true,
238+
['Releases']: false,
239+
}
240+
);
241+
242+
const {releases = []} = useReleaseStats(
243+
{
244+
projects: eventView.project,
245+
environments: eventView.environment,
246+
datetime: {
247+
start: eventView.start,
248+
end: eventView.end,
249+
period: eventView.statsPeriod,
250+
},
251+
},
252+
{
253+
staleTime: 0,
254+
}
255+
);
256+
257+
const releaseSeries = useReleaseMarkLineSeries({
258+
group,
259+
releases: hasReleaseBubblesSeries && showReleasesAs !== 'line' ? [] : releases,
260+
});
261+
262+
const {
263+
connectReleaseBubbleChartRef,
264+
releaseBubbleEventHandlers,
265+
releaseBubbleSeries,
266+
releaseBubbleXAxis,
267+
releaseBubbleGrid,
268+
} = useReleaseBubbles({
269+
chartRenderer: ({chartRef}) => {
270+
return (
271+
<EventGraph
272+
ref={chartRef}
273+
group={group}
274+
event={event}
275+
showSummary={false}
276+
showReleasesAs="line"
277+
disableZoomNavigation
278+
{...styleProps}
279+
/>
280+
);
281+
},
282+
legendSelected: legendSelected.Releases,
283+
desiredBuckets: eventSeries.length,
284+
minTime: eventSeries.length && (eventSeries.at(0)?.name as number),
285+
maxTime: eventSeries.length && (eventSeries.at(-1)?.name as number),
286+
releases: hasReleaseBubblesSeries && showReleasesAs !== 'line' ? releases : [],
287+
projects: eventView.project,
288+
environments: eventView.environment,
289+
datetime: {
290+
start: eventView.start,
291+
end: eventView.end,
292+
period: eventView.statsPeriod,
293+
},
294+
});
295+
296+
const handleConnectRef = useCallback(
297+
(e: ReactEchartsRef | null) => {
298+
connectReleaseBubbleChartRef(e);
299+
},
300+
[connectReleaseBubbleChartRef]
301+
);
204302
const flagSeries = useFlagSeries({
205303
query: {
206304
start: eventView.start,
@@ -273,7 +371,7 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
273371
seriesData.push(currentEventSeries as BarChartSeries);
274372
}
275373

276-
if (releaseSeries.markLine) {
374+
if (releaseSeries?.markLine) {
277375
seriesData.push(releaseSeries as BarChartSeries);
278376
}
279377

@@ -298,14 +396,6 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
298396

299397
const bucketSize = eventSeries ? getBucketSize(series) : undefined;
300398

301-
const [legendSelected, setLegendSelected] = useLocalStorageState(
302-
'issue-details-graph-legend',
303-
{
304-
['Feature Flags']: true,
305-
['Releases']: false,
306-
}
307-
);
308-
309399
const legend = Legend({
310400
theme,
311401
orient: 'horizontal',
@@ -363,32 +453,39 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
363453

364454
return (
365455
<GraphWrapper {...styleProps}>
366-
<SummaryContainer>
367-
<GraphButton
368-
onClick={() =>
369-
visibleSeries === EventGraphSeries.USER &&
370-
setVisibleSeries(EventGraphSeries.EVENT)
371-
}
372-
isActive={visibleSeries === EventGraphSeries.EVENT}
373-
disabled={visibleSeries === EventGraphSeries.EVENT}
374-
label={tn('Event', 'Events', eventCount)}
375-
count={String(eventCount)}
376-
/>
377-
<GraphButton
378-
onClick={() =>
379-
visibleSeries === EventGraphSeries.EVENT &&
380-
setVisibleSeries(EventGraphSeries.USER)
381-
}
382-
isActive={visibleSeries === EventGraphSeries.USER}
383-
disabled={visibleSeries === EventGraphSeries.USER}
384-
label={tn('User', 'Users', userCount)}
385-
count={String(userCount)}
386-
/>
387-
</SummaryContainer>
456+
{showSummary ? (
457+
<SummaryContainer>
458+
<GraphButton
459+
onClick={() =>
460+
visibleSeries === EventGraphSeries.USER &&
461+
setVisibleSeries(EventGraphSeries.EVENT)
462+
}
463+
isActive={visibleSeries === EventGraphSeries.EVENT}
464+
disabled={visibleSeries === EventGraphSeries.EVENT}
465+
label={tn('Event', 'Events', eventCount)}
466+
count={String(eventCount)}
467+
/>
468+
<GraphButton
469+
onClick={() =>
470+
visibleSeries === EventGraphSeries.EVENT &&
471+
setVisibleSeries(EventGraphSeries.USER)
472+
}
473+
isActive={visibleSeries === EventGraphSeries.USER}
474+
disabled={visibleSeries === EventGraphSeries.USER}
475+
label={tn('User', 'Users', userCount)}
476+
count={String(userCount)}
477+
/>
478+
</SummaryContainer>
479+
) : (
480+
<div />
481+
)}
388482
<ChartContainer role="figure" ref={chartContainerRef}>
389483
<BarChart
484+
{...releaseBubbleEventHandlers}
485+
ref={mergeRefs(ref, handleConnectRef)}
390486
height={100}
391487
series={series}
488+
additionalSeries={releaseBubbleSeries ? [releaseBubbleSeries] : []}
392489
legend={legend}
393490
onLegendSelectChanged={onLegendSelectChanged}
394491
showTimeInTooltip
@@ -397,6 +494,7 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
397494
right: 8,
398495
top: 20,
399496
bottom: 0,
497+
...releaseBubbleGrid,
400498
}}
401499
tooltip={{
402500
formatAxisLabel: (
@@ -428,7 +526,15 @@ export function EventGraph({group, event, ...styleProps}: EventGraphProps) {
428526
},
429527
},
430528
}}
431-
{...chartZoomProps}
529+
xAxis={{
530+
...releaseBubbleXAxis,
531+
}}
532+
{...(disableZoomNavigation
533+
? {
534+
isGroupedByDate: true,
535+
dataZoom: chartZoomProps.dataZoom,
536+
}
537+
: chartZoomProps)}
432538
/>
433539
</ChartContainer>
434540
</GraphWrapper>

static/app/views/issueDetails/streamline/hooks/useReleaseMarkLineSeries.tsx

+12-24
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,30 @@ import {useTheme} from '@emotion/react';
33
import MarkLine from 'sentry/components/charts/components/markLine';
44
import {t} from 'sentry/locale';
55
import type {Group} from 'sentry/types/group';
6+
import type {ReleaseMetaBasic} from 'sentry/types/release';
67
import {escape} from 'sentry/utils';
78
import {getFormattedDate} from 'sentry/utils/dates';
8-
import {useApiQuery} from 'sentry/utils/queryClient';
99
import {useNavigate} from 'sentry/utils/useNavigate';
1010
import useOrganization from 'sentry/utils/useOrganization';
1111
import {formatVersion} from 'sentry/utils/versions/formatVersion';
1212
import {useIssueDetailsEventView} from 'sentry/views/issueDetails/streamline/hooks/useIssueDetailsDiscoverQuery';
1313
import {makeReleasesPathname} from 'sentry/views/releases/utils/pathnames';
1414

15-
interface ReleaseStat {
16-
date: string;
17-
version: string;
18-
}
19-
20-
export function useReleaseMarkLineSeries({group}: {group: Group}) {
15+
export function useReleaseMarkLineSeries({
16+
group,
17+
releases,
18+
}: {
19+
group: Group;
20+
releases: ReleaseMetaBasic[];
21+
}) {
2122
const navigate = useNavigate();
2223
const theme = useTheme();
2324
const eventView = useIssueDetailsEventView({group});
2425
const organization = useOrganization();
25-
const {data: releases = []} = useApiQuery<ReleaseStat[]>(
26-
[
27-
`/organizations/${organization.slug}/releases/stats/`,
28-
{
29-
query: {
30-
project: eventView.project,
31-
environment: eventView.environment,
32-
start: eventView.start,
33-
end: eventView.end,
34-
statsPeriod: eventView.statsPeriod,
35-
},
36-
},
37-
],
38-
{
39-
staleTime: 0,
40-
}
41-
);
26+
27+
if (!releases.length) {
28+
return null;
29+
}
4230

4331
const markLine = MarkLine({
4432
animation: false,

0 commit comments

Comments
 (0)