Skip to content

Commit b047a33

Browse files
authored
feat(issue): Session percent in issue stream (#26126)
Feature flagged to percent-issue-display Adds a new dropdown to the issue stream which allows displaying % issue data. Uses the expand=sessions from #26115 Events as % is disabled for multi projects and when the selected project has no session data
1 parent f5ed679 commit b047a33

File tree

6 files changed

+175
-2
lines changed

6 files changed

+175
-2
lines changed

static/app/components/stream/group.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ import {queryToObj} from 'app/utils/stream';
4141
import withGlobalSelection from 'app/utils/withGlobalSelection';
4242
import withOrganization from 'app/utils/withOrganization';
4343
import {TimePeriodType} from 'app/views/alerts/rules/details/constants';
44-
import {getTabs, isForReviewQuery, Query} from 'app/views/issueList/utils';
44+
import {
45+
getTabs,
46+
isForReviewQuery,
47+
IssueDisplayOptions,
48+
Query,
49+
} from 'app/views/issueList/utils';
4550

4651
const DiscoveryExclusionFields: string[] = [
4752
'query',
@@ -59,13 +64,15 @@ const DiscoveryExclusionFields: string[] = [
5964
];
6065

6166
export const DEFAULT_STREAM_GROUP_STATS_PERIOD = '24h';
67+
const DEFAULT_DISPLAY = IssueDisplayOptions.EVENTS;
6268

6369
const defaultProps = {
6470
statsPeriod: DEFAULT_STREAM_GROUP_STATS_PERIOD,
6571
canSelect: true,
6672
withChart: true,
6773
useFilteredStats: false,
6874
useTintRow: true,
75+
display: DEFAULT_DISPLAY,
6976
};
7077

7178
type Props = {
@@ -79,6 +86,7 @@ type Props = {
7986
showInboxTime?: boolean;
8087
index?: number;
8188
customStatsPeriod?: TimePeriodType;
89+
display?: IssueDisplayOptions;
8290
// TODO(ts): higher order functions break defaultprops export types
8391
} & Partial<typeof defaultProps>;
8492

@@ -349,6 +357,7 @@ class StreamGroup extends React.Component<Props, State> {
349357
useFilteredStats,
350358
useTintRow,
351359
customStatsPeriod,
360+
display,
352361
} = this.props;
353362

354363
const {period, start, end} = selection.datetime || {};
@@ -373,6 +382,13 @@ class StreamGroup extends React.Component<Props, State> {
373382
const hasInbox = organization.features.includes('inbox');
374383
const unresolved = data.status === 'unresolved' ? true : false;
375384

385+
const showSessions = display === IssueDisplayOptions.SESSIONS;
386+
// calculate a percentage count based on session data if the user has selected sessions display
387+
const primaryPercent =
388+
showSessions &&
389+
data.sessionCount &&
390+
(Number(primaryCount) / Number(data.sessionCount)) * 100;
391+
376392
return (
377393
<Wrapper
378394
data-test-id="group"
@@ -443,7 +459,8 @@ class StreamGroup extends React.Component<Props, State> {
443459
>
444460
<span {...getActorProps({})}>
445461
<div className="dropdown-actor-title">
446-
<PrimaryCount value={primaryCount} />
462+
<PrimaryCount value={primaryPercent || primaryCount} />
463+
{primaryPercent && '%'}
447464
{secondaryCount !== undefined && useFilteredStats && (
448465
<SecondaryCount value={secondaryCount} />
449466
)}

static/app/types/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,7 @@ type GroupFiltered = {
991991
export type GroupStats = GroupFiltered & {
992992
lifetime?: GroupFiltered;
993993
filtered: GroupFiltered | null;
994+
sessionCount?: string | null;
994995
id: string;
995996
};
996997

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import DropdownControl, {DropdownItem} from 'app/components/dropdownControl';
5+
import Tooltip from 'app/components/tooltip';
6+
import {t} from 'app/locale';
7+
import space from 'app/styles/space';
8+
import {getDisplayLabel, IssueDisplayOptions} from 'app/views/issueList/utils';
9+
10+
type Props = {
11+
onDisplayChange: (display: string) => void;
12+
display: IssueDisplayOptions;
13+
hasSessions: boolean;
14+
hasMultipleProjectsSelected: boolean;
15+
};
16+
17+
const IssueListDisplayOptions = ({
18+
onDisplayChange,
19+
display,
20+
hasSessions,
21+
hasMultipleProjectsSelected,
22+
}: Props) => {
23+
const getMenuItem = (key: IssueDisplayOptions): React.ReactNode => {
24+
let tooltipText: string | undefined;
25+
let disabled = false;
26+
if (key === IssueDisplayOptions.SESSIONS) {
27+
if (hasMultipleProjectsSelected) {
28+
tooltipText = t(
29+
'Select a project to view events as a % of sessions. This helps you get a better picture of how these errors affect your users.'
30+
);
31+
disabled = true;
32+
} else if (!hasSessions) {
33+
tooltipText = t('The selected project does not have session data');
34+
disabled = true;
35+
}
36+
}
37+
38+
return (
39+
<DropdownItem
40+
onSelect={onDisplayChange}
41+
eventKey={key}
42+
isActive={key === display}
43+
disabled={disabled}
44+
>
45+
<StyledTooltip
46+
containerDisplayMode="block"
47+
position="top"
48+
delay={500}
49+
title={tooltipText}
50+
disabled={!tooltipText}
51+
>
52+
{getDisplayLabel(key)}
53+
</StyledTooltip>
54+
</DropdownItem>
55+
);
56+
};
57+
58+
return (
59+
<StyledDropdownControl
60+
buttonProps={{prefix: t('Display')}}
61+
label={getDisplayLabel(display)}
62+
>
63+
<React.Fragment>
64+
{getMenuItem(IssueDisplayOptions.EVENTS)}
65+
{getMenuItem(IssueDisplayOptions.SESSIONS)}
66+
</React.Fragment>
67+
</StyledDropdownControl>
68+
);
69+
};
70+
71+
const StyledDropdownControl = styled(DropdownControl)`
72+
margin-right: ${space(1)};
73+
`;
74+
75+
const StyledTooltip = styled(Tooltip)`
76+
width: 100%;
77+
`;
78+
79+
export default IssueListDisplayOptions;

static/app/views/issueList/filters.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import {ClassNames} from '@emotion/react';
33
import styled from '@emotion/styled';
44

5+
import Feature from 'app/components/acl/feature';
56
import GuideAnchor from 'app/components/assistant/guideAnchor';
67
import PageHeading from 'app/components/pageHeading';
78
import QueryCount from 'app/components/queryCount';
@@ -11,23 +12,29 @@ import space from 'app/styles/space';
1112
import {Organization, SavedSearch} from 'app/types';
1213
import {trackAnalyticsEvent} from 'app/utils/analytics';
1314

15+
import IssueListDisplayOptions from './displayOptions';
1416
import SavedSearchSelector from './savedSearchSelector';
1517
import IssueListSearchBar from './searchBar';
1618
import IssueListSortOptions from './sortOptions';
1719
import {TagValueLoader} from './types';
20+
import {IssueDisplayOptions} from './utils';
1821

1922
type IssueListSearchBarProps = React.ComponentProps<typeof IssueListSearchBar>;
2023

2124
type Props = {
2225
organization: Organization;
2326
savedSearchList: SavedSearch[];
2427
savedSearch: SavedSearch;
28+
display: IssueDisplayOptions;
2529
sort: string;
2630
query: string;
2731
isSearchDisabled: boolean;
2832
queryCount: number;
2933
queryMaxCount: number;
34+
hasSessions: boolean;
35+
selectedProjects: number[];
3036

37+
onDisplayChange: (display: string) => void;
3138
onSortChange: (sort: string) => void;
3239
onSearch: (query: string) => void;
3340
onSidebarToggle: (event: React.MouseEvent) => void;
@@ -64,11 +71,15 @@ class IssueListFilters extends React.Component<Props> {
6471
savedSearchList,
6572
isSearchDisabled,
6673
sort,
74+
display,
75+
hasSessions,
76+
selectedProjects,
6777

6878
onSidebarToggle,
6979
onSearch,
7080
onSavedSearchDelete,
7181
onSortChange,
82+
onDisplayChange,
7283
tagValueLoader,
7384
tags,
7485
isInbox,
@@ -85,6 +96,14 @@ class IssueListFilters extends React.Component<Props> {
8596

8697
<SearchContainer isInbox={isInbox}>
8798
<IssueListSortOptions sort={sort} query={query} onSelect={onSortChange} />
99+
<Feature features={['issue-percent-display']} organization={organization}>
100+
<IssueListDisplayOptions
101+
onDisplayChange={onDisplayChange}
102+
display={display}
103+
hasSessions={hasSessions}
104+
hasMultipleProjectsSelected={selectedProjects.length !== 1}
105+
/>
106+
</Feature>
88107

89108
<SearchSelectorContainer isInbox={isInbox}>
90109
{!isInbox && (

static/app/views/issueList/overview.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
getTabs,
6969
getTabsWithCounts,
7070
isForReviewQuery,
71+
IssueDisplayOptions,
7172
IssueSortOptions,
7273
Query,
7374
QueryCounts,
@@ -76,6 +77,7 @@ import {
7677

7778
const MAX_ITEMS = 25;
7879
const DEFAULT_SORT = IssueSortOptions.DATE;
80+
const DEFAULT_DISPLAY = IssueDisplayOptions.EVENTS;
7981
// the default period for the graph in each issue row
8082
const DEFAULT_GRAPH_STATS_PERIOD = '24h';
8183
// the allowed period choices for graph in each issue row
@@ -118,6 +120,8 @@ type State = {
118120
issuesLoading: boolean;
119121
tagsLoading: boolean;
120122
memberList: ReturnType<typeof indexMembersByProject>;
123+
// Will be set to true if there is valid session data from issue-stats api call
124+
hasSessions: boolean;
121125
query?: string;
122126
};
123127

@@ -130,6 +134,7 @@ type EndpointParams = Partial<GlobalSelection['datetime']> & {
130134
groupStatsPeriod?: string;
131135
cursor?: string;
132136
page?: number | string;
137+
display?: string;
133138
};
134139

135140
type CountsEndpointParams = Omit<EndpointParams, 'cursor' | 'page' | 'query'> & {
@@ -138,6 +143,7 @@ type CountsEndpointParams = Omit<EndpointParams, 'cursor' | 'page' | 'query'> &
138143

139144
type StatEndpointParams = Omit<EndpointParams, 'cursor' | 'page'> & {
140145
groups: string[];
146+
expand?: string | string[];
141147
};
142148

143149
class IssueListOverview extends React.Component<Props, State> {
@@ -165,6 +171,7 @@ class IssueListOverview extends React.Component<Props, State> {
165171
issuesLoading: true,
166172
tagsLoading: true,
167173
memberList: {},
174+
hasSessions: false,
168175
};
169176
}
170177

@@ -311,6 +318,21 @@ class IssueListOverview extends React.Component<Props, State> {
311318
return DEFAULT_SORT;
312319
}
313320

321+
getDisplay(): IssueDisplayOptions {
322+
const {organization, location} = this.props;
323+
324+
if (organization.features.includes('issue-percent-display')) {
325+
if (
326+
location.query.display &&
327+
Object.values(IssueDisplayOptions).includes(location.query.display)
328+
) {
329+
return location.query.display as IssueDisplayOptions;
330+
}
331+
}
332+
333+
return DEFAULT_DISPLAY;
334+
}
335+
314336
getGroupStatsPeriod(): string {
315337
let currentPeriod: string;
316338
if (typeof this.props.location.query?.groupStatsPeriod === 'string') {
@@ -399,6 +421,9 @@ class IssueListOverview extends React.Component<Props, State> {
399421
if (!requestParams.statsPeriod && !requestParams.start) {
400422
requestParams.statsPeriod = DEFAULT_STATS_PERIOD;
401423
}
424+
if (this.props.organization.features.includes('issue-percent-display')) {
425+
requestParams.expand = 'sessions';
426+
}
402427

403428
this._lastStatsRequest = this.props.api.request(this.getGroupStatsEndpoint(), {
404429
method: 'GET',
@@ -409,6 +434,13 @@ class IssueListOverview extends React.Component<Props, State> {
409434
}
410435

411436
GroupActions.populateStats(groups, data);
437+
const hasSessions =
438+
data.filter(groupStats => !groupStats.sessionCount).length === 0;
439+
if (hasSessions !== this.state.hasSessions) {
440+
this.setState({
441+
hasSessions,
442+
});
443+
}
412444
},
413445
error: err => {
414446
this.setState({
@@ -694,6 +726,10 @@ class IssueListOverview extends React.Component<Props, State> {
694726
this.transitionTo({sort});
695727
};
696728

729+
onDisplayChange = (display: string) => {
730+
this.transitionTo({display});
731+
};
732+
697733
onCursorChange = (cursor: string | undefined, _path, query, pageDiff: number) => {
698734
const queryPageInt = parseInt(query.page, 10);
699735
let nextPage: number | undefined = isNaN(queryPageInt)
@@ -827,6 +863,7 @@ class IssueListOverview extends React.Component<Props, State> {
827863
displayReprocessingLayout={displayReprocessingLayout}
828864
useFilteredStats
829865
showInboxTime={showInboxTime}
866+
display={this.getDisplay()}
830867
/>
831868
);
832869
});
@@ -952,6 +989,7 @@ class IssueListOverview extends React.Component<Props, State> {
952989
groupIds,
953990
queryMaxCount,
954991
itemsRemoved,
992+
hasSessions,
955993
} = this.state;
956994
const {
957995
organization,
@@ -1039,8 +1077,10 @@ class IssueListOverview extends React.Component<Props, State> {
10391077
query={query}
10401078
savedSearch={savedSearch}
10411079
sort={this.getSort()}
1080+
display={this.getDisplay()}
10421081
queryCount={queryCount}
10431082
queryMaxCount={queryMaxCount}
1083+
onDisplayChange={this.onDisplayChange}
10441084
onSortChange={this.onSortChange}
10451085
onSearch={this.onSearch}
10461086
onSavedSearchSelect={this.onSavedSearchSelect}
@@ -1051,6 +1091,8 @@ class IssueListOverview extends React.Component<Props, State> {
10511091
tagValueLoader={this.tagValueLoader}
10521092
tags={tags}
10531093
isInbox={hasFeature}
1094+
hasSessions={hasSessions}
1095+
selectedProjects={selection.projects}
10541096
/>
10551097

10561098
<Panel>

static/app/views/issueList/utils.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,18 @@ export function getSortLabel(key: string) {
138138
return t('Last Seen');
139139
}
140140
}
141+
142+
export enum IssueDisplayOptions {
143+
EVENTS = 'events',
144+
SESSIONS = 'sessions',
145+
}
146+
147+
export function getDisplayLabel(key: IssueDisplayOptions) {
148+
switch (key) {
149+
case IssueDisplayOptions.SESSIONS:
150+
return t('Events as %');
151+
case IssueDisplayOptions.EVENTS:
152+
default:
153+
return t('Event Count');
154+
}
155+
}

0 commit comments

Comments
 (0)