mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
529 lines
17 KiB
TypeScript
529 lines
17 KiB
TypeScript
import {Component, Fragment} from 'react';
|
|
import type {InjectedRouter} from 'react-router';
|
|
import {browserHistory} from 'react-router';
|
|
import type {Theme} from '@emotion/react';
|
|
import {withTheme} from '@emotion/react';
|
|
import type {Location} from 'history';
|
|
|
|
import type {Client} from 'sentry/api';
|
|
import {BarChart} from 'sentry/components/charts/barChart';
|
|
import LoadingPanel from 'sentry/components/charts/loadingPanel';
|
|
import OptionSelector from 'sentry/components/charts/optionSelector';
|
|
import {
|
|
ChartContainer,
|
|
ChartControls,
|
|
InlineContainer,
|
|
SectionHeading,
|
|
SectionValue,
|
|
} from 'sentry/components/charts/styles';
|
|
import {
|
|
getDiffInMinutes,
|
|
ONE_HOUR,
|
|
ONE_WEEK,
|
|
TWENTY_FOUR_HOURS,
|
|
TWO_WEEKS,
|
|
} from 'sentry/components/charts/utils';
|
|
import Panel from 'sentry/components/panels/panel';
|
|
import Placeholder from 'sentry/components/placeholder';
|
|
import {CHART_PALETTE} from 'sentry/constants/chartPalette';
|
|
import {NOT_AVAILABLE_MESSAGES} from 'sentry/constants/notAvailableMessages';
|
|
import {t} from 'sentry/locale';
|
|
import type {Organization, Project, SelectValue} from 'sentry/types';
|
|
import {defined} from 'sentry/utils';
|
|
import {trackAnalytics} from 'sentry/utils/analytics';
|
|
import {decodeScalar} from 'sentry/utils/queryString';
|
|
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
|
|
import withApi from 'sentry/utils/withApi';
|
|
import {isPlatformANRCompatible} from 'sentry/views/projectDetail/utils';
|
|
import {
|
|
getSessionTermDescription,
|
|
SessionTerm,
|
|
} from 'sentry/views/releases/utils/sessionTerm';
|
|
|
|
import {getTermHelp, PerformanceTerm} from '../performance/data';
|
|
|
|
import ProjectBaseEventsChart from './charts/projectBaseEventsChart';
|
|
import ProjectBaseSessionsChart from './charts/projectBaseSessionsChart';
|
|
import ProjectErrorsBasicChart from './charts/projectErrorsBasicChart';
|
|
|
|
export enum DisplayModes {
|
|
APDEX = 'apdex',
|
|
FAILURE_RATE = 'failure_rate',
|
|
TPM = 'tpm',
|
|
ERRORS = 'errors',
|
|
TRANSACTIONS = 'transactions',
|
|
STABILITY = 'crash_free',
|
|
STABILITY_USERS = 'crash_free_users',
|
|
ANR_RATE = 'anr_rate',
|
|
FOREGROUND_ANR_RATE = 'foreground_anr_rate',
|
|
SESSIONS = 'sessions',
|
|
}
|
|
|
|
type Props = {
|
|
api: Client;
|
|
chartId: string;
|
|
chartIndex: number;
|
|
hasSessions: boolean | null;
|
|
hasTransactions: boolean;
|
|
location: Location;
|
|
organization: Organization;
|
|
router: InjectedRouter;
|
|
theme: Theme;
|
|
visibleCharts: string[];
|
|
project?: Project;
|
|
projectId?: string;
|
|
query?: string;
|
|
};
|
|
|
|
type State = {
|
|
totalValues: number | null;
|
|
};
|
|
|
|
class ProjectCharts extends Component<Props, State> {
|
|
state: State = {
|
|
totalValues: null,
|
|
};
|
|
|
|
get defaultDisplayModes() {
|
|
const {hasSessions, hasTransactions, project} = this.props;
|
|
|
|
if (!hasSessions && !hasTransactions) {
|
|
return [DisplayModes.ERRORS];
|
|
}
|
|
|
|
if (hasSessions && !hasTransactions) {
|
|
if (isPlatformANRCompatible(project?.platform)) {
|
|
return [DisplayModes.STABILITY, DisplayModes.ANR_RATE];
|
|
}
|
|
return [DisplayModes.STABILITY, DisplayModes.ERRORS];
|
|
}
|
|
|
|
if (!hasSessions && hasTransactions) {
|
|
return [DisplayModes.FAILURE_RATE, DisplayModes.APDEX];
|
|
}
|
|
|
|
if (isPlatformANRCompatible(project?.platform)) {
|
|
return [DisplayModes.STABILITY, DisplayModes.ANR_RATE];
|
|
}
|
|
|
|
return [DisplayModes.STABILITY, DisplayModes.APDEX];
|
|
}
|
|
|
|
get otherActiveDisplayModes() {
|
|
const {location, visibleCharts, chartId} = this.props;
|
|
|
|
return visibleCharts
|
|
.filter(visibleChartId => visibleChartId !== chartId)
|
|
.map(urlKey => {
|
|
return decodeScalar(
|
|
location.query[urlKey],
|
|
this.defaultDisplayModes[visibleCharts.findIndex(value => value === urlKey)]
|
|
);
|
|
});
|
|
}
|
|
|
|
get displayMode() {
|
|
const {location, chartId, chartIndex} = this.props;
|
|
const displayMode =
|
|
decodeScalar(location.query[chartId]) || this.defaultDisplayModes[chartIndex];
|
|
|
|
if (!Object.values(DisplayModes).includes(displayMode as DisplayModes)) {
|
|
return this.defaultDisplayModes[chartIndex];
|
|
}
|
|
|
|
return displayMode;
|
|
}
|
|
|
|
get displayModes(): SelectValue<string>[] {
|
|
const {organization, hasSessions, hasTransactions, project} = this.props;
|
|
const hasPerformance = organization.features.includes('performance-view');
|
|
const noPerformanceTooltip = NOT_AVAILABLE_MESSAGES.performance;
|
|
const noHealthTooltip = NOT_AVAILABLE_MESSAGES.releaseHealth;
|
|
|
|
const options = [
|
|
{
|
|
value: DisplayModes.STABILITY,
|
|
label: t('Crash Free Sessions'),
|
|
disabled:
|
|
this.otherActiveDisplayModes.includes(DisplayModes.STABILITY) || !hasSessions,
|
|
tooltip: !hasSessions ? noHealthTooltip : undefined,
|
|
},
|
|
{
|
|
value: DisplayModes.STABILITY_USERS,
|
|
label: t('Crash Free Users'),
|
|
disabled:
|
|
this.otherActiveDisplayModes.includes(DisplayModes.STABILITY_USERS) ||
|
|
!hasSessions,
|
|
tooltip: !hasSessions ? noHealthTooltip : undefined,
|
|
},
|
|
{
|
|
value: DisplayModes.APDEX,
|
|
label: t('Apdex'),
|
|
disabled:
|
|
this.otherActiveDisplayModes.includes(DisplayModes.APDEX) ||
|
|
!hasPerformance ||
|
|
!hasTransactions,
|
|
tooltip:
|
|
hasPerformance && hasTransactions
|
|
? getTermHelp(organization, PerformanceTerm.APDEX)
|
|
: noPerformanceTooltip,
|
|
},
|
|
{
|
|
value: DisplayModes.FAILURE_RATE,
|
|
label: t('Failure Rate'),
|
|
disabled:
|
|
this.otherActiveDisplayModes.includes(DisplayModes.FAILURE_RATE) ||
|
|
!hasPerformance ||
|
|
!hasTransactions,
|
|
tooltip:
|
|
hasPerformance && hasTransactions
|
|
? getTermHelp(organization, PerformanceTerm.FAILURE_RATE)
|
|
: noPerformanceTooltip,
|
|
},
|
|
{
|
|
value: DisplayModes.TPM,
|
|
label: t('Transactions Per Minute'),
|
|
disabled:
|
|
this.otherActiveDisplayModes.includes(DisplayModes.TPM) ||
|
|
!hasPerformance ||
|
|
!hasTransactions,
|
|
tooltip:
|
|
hasPerformance && hasTransactions
|
|
? getTermHelp(organization, PerformanceTerm.TPM)
|
|
: noPerformanceTooltip,
|
|
},
|
|
{
|
|
value: DisplayModes.ERRORS,
|
|
label: t('Number of Errors'),
|
|
disabled: this.otherActiveDisplayModes.includes(DisplayModes.ERRORS),
|
|
},
|
|
{
|
|
value: DisplayModes.SESSIONS,
|
|
label: t('Number of Sessions'),
|
|
disabled:
|
|
this.otherActiveDisplayModes.includes(DisplayModes.SESSIONS) || !hasSessions,
|
|
tooltip: !hasSessions ? noHealthTooltip : undefined,
|
|
},
|
|
{
|
|
value: DisplayModes.TRANSACTIONS,
|
|
label: t('Number of Transactions'),
|
|
disabled:
|
|
this.otherActiveDisplayModes.includes(DisplayModes.TRANSACTIONS) ||
|
|
!hasPerformance ||
|
|
!hasTransactions,
|
|
tooltip: hasPerformance && hasTransactions ? undefined : noPerformanceTooltip,
|
|
},
|
|
];
|
|
|
|
if (isPlatformANRCompatible(project?.platform)) {
|
|
return [
|
|
{
|
|
value: DisplayModes.ANR_RATE,
|
|
label: t('ANR Rate'),
|
|
disabled:
|
|
this.otherActiveDisplayModes.includes(DisplayModes.ANR_RATE) || !hasSessions,
|
|
tooltip: !hasSessions ? noHealthTooltip : undefined,
|
|
},
|
|
{
|
|
value: DisplayModes.FOREGROUND_ANR_RATE,
|
|
label: t('Foreground ANR Rate'),
|
|
disabled:
|
|
this.otherActiveDisplayModes.includes(DisplayModes.FOREGROUND_ANR_RATE) ||
|
|
!hasSessions,
|
|
tooltip: !hasSessions ? noHealthTooltip : undefined,
|
|
},
|
|
...options,
|
|
];
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
get summaryHeading() {
|
|
switch (this.displayMode) {
|
|
case DisplayModes.ERRORS:
|
|
return t('Total Errors');
|
|
case DisplayModes.STABILITY:
|
|
case DisplayModes.SESSIONS:
|
|
return t('Total Sessions');
|
|
case DisplayModes.STABILITY_USERS:
|
|
case DisplayModes.ANR_RATE:
|
|
case DisplayModes.FOREGROUND_ANR_RATE:
|
|
return t('Total Users');
|
|
case DisplayModes.APDEX:
|
|
case DisplayModes.FAILURE_RATE:
|
|
case DisplayModes.TPM:
|
|
case DisplayModes.TRANSACTIONS:
|
|
default:
|
|
return t('Total Transactions');
|
|
}
|
|
}
|
|
|
|
get barChartInterval() {
|
|
const {query} = this.props.location;
|
|
|
|
const diffInMinutes = getDiffInMinutes({
|
|
...query,
|
|
period: decodeScalar(query.statsPeriod),
|
|
});
|
|
|
|
if (diffInMinutes >= TWO_WEEKS) {
|
|
return '1d';
|
|
}
|
|
|
|
if (diffInMinutes >= ONE_WEEK) {
|
|
return '12h';
|
|
}
|
|
|
|
if (diffInMinutes > TWENTY_FOUR_HOURS) {
|
|
return '6h';
|
|
}
|
|
|
|
if (diffInMinutes === TWENTY_FOUR_HOURS) {
|
|
return '1h';
|
|
}
|
|
|
|
if (diffInMinutes <= ONE_HOUR) {
|
|
return '1m';
|
|
}
|
|
|
|
return '15m';
|
|
}
|
|
|
|
handleDisplayModeChange = (value: string) => {
|
|
const {location, chartId, chartIndex, organization} = this.props;
|
|
trackAnalytics('project_detail.change_chart', {
|
|
organization,
|
|
metric: value,
|
|
chart_index: chartIndex,
|
|
});
|
|
|
|
browserHistory.push({
|
|
pathname: location.pathname,
|
|
query: {...location.query, [chartId]: value},
|
|
});
|
|
};
|
|
|
|
handleTotalValuesChange = (value: number | null) => {
|
|
if (value !== this.state.totalValues) {
|
|
this.setState({totalValues: value});
|
|
}
|
|
};
|
|
|
|
render() {
|
|
const {
|
|
api,
|
|
router,
|
|
location,
|
|
organization,
|
|
theme,
|
|
projectId,
|
|
hasSessions,
|
|
query,
|
|
project,
|
|
} = this.props;
|
|
const {totalValues} = this.state;
|
|
const hasDiscover = organization.features.includes('discover-basic');
|
|
const displayMode = this.displayMode;
|
|
const hasAnrRateFeature = isPlatformANRCompatible(project?.platform);
|
|
|
|
return (
|
|
<Panel>
|
|
<ChartContainer>
|
|
{!defined(hasSessions) ? (
|
|
<LoadingPanel />
|
|
) : (
|
|
<Fragment>
|
|
{displayMode === DisplayModes.APDEX && (
|
|
<ProjectBaseEventsChart
|
|
title={t('Apdex')}
|
|
help={getTermHelp(organization, PerformanceTerm.APDEX)}
|
|
query={new MutableSearch([
|
|
'event.type:transaction',
|
|
query ?? '',
|
|
]).formatString()}
|
|
yAxis="apdex()"
|
|
field={['apdex()']}
|
|
api={api}
|
|
router={router}
|
|
organization={organization}
|
|
onTotalValuesChange={this.handleTotalValuesChange}
|
|
colors={[CHART_PALETTE[0][0], theme.purple200]}
|
|
/>
|
|
)}
|
|
{displayMode === DisplayModes.FAILURE_RATE && (
|
|
<ProjectBaseEventsChart
|
|
title={t('Failure Rate')}
|
|
help={getTermHelp(organization, PerformanceTerm.FAILURE_RATE)}
|
|
query={new MutableSearch([
|
|
'event.type:transaction',
|
|
query ?? '',
|
|
]).formatString()}
|
|
yAxis="failure_rate()"
|
|
field={[`failure_rate()`]}
|
|
api={api}
|
|
router={router}
|
|
organization={organization}
|
|
onTotalValuesChange={this.handleTotalValuesChange}
|
|
colors={[theme.red300, theme.purple200]}
|
|
/>
|
|
)}
|
|
{displayMode === DisplayModes.TPM && (
|
|
<ProjectBaseEventsChart
|
|
title={t('Transactions Per Minute')}
|
|
help={getTermHelp(organization, PerformanceTerm.TPM)}
|
|
query={new MutableSearch([
|
|
'event.type:transaction',
|
|
query ?? '',
|
|
]).formatString()}
|
|
yAxis="tpm()"
|
|
field={[`tpm()`]}
|
|
api={api}
|
|
router={router}
|
|
organization={organization}
|
|
onTotalValuesChange={this.handleTotalValuesChange}
|
|
colors={[theme.yellow300, theme.purple200]}
|
|
disablePrevious
|
|
/>
|
|
)}
|
|
{displayMode === DisplayModes.ERRORS &&
|
|
(hasDiscover ? (
|
|
<ProjectBaseEventsChart
|
|
title={t('Number of Errors')}
|
|
query={new MutableSearch([
|
|
'!event.type:transaction',
|
|
query ?? '',
|
|
]).formatString()}
|
|
yAxis="count()"
|
|
field={[`count()`]}
|
|
api={api}
|
|
router={router}
|
|
organization={organization}
|
|
onTotalValuesChange={this.handleTotalValuesChange}
|
|
colors={[theme.purple300, theme.purple200]}
|
|
interval={this.barChartInterval}
|
|
chartComponent={BarChart}
|
|
disableReleases
|
|
/>
|
|
) : (
|
|
<ProjectErrorsBasicChart
|
|
organization={organization}
|
|
projectId={projectId}
|
|
location={location}
|
|
onTotalValuesChange={this.handleTotalValuesChange}
|
|
/>
|
|
))}
|
|
{displayMode === DisplayModes.TRANSACTIONS && (
|
|
<ProjectBaseEventsChart
|
|
title={t('Number of Transactions')}
|
|
query={new MutableSearch([
|
|
'event.type:transaction',
|
|
query ?? '',
|
|
]).formatString()}
|
|
yAxis="count()"
|
|
field={[`count()`]}
|
|
api={api}
|
|
router={router}
|
|
organization={organization}
|
|
onTotalValuesChange={this.handleTotalValuesChange}
|
|
colors={[theme.gray200, theme.purple200]}
|
|
interval={this.barChartInterval}
|
|
chartComponent={BarChart}
|
|
disableReleases
|
|
/>
|
|
)}
|
|
{displayMode === DisplayModes.STABILITY && (
|
|
<ProjectBaseSessionsChart
|
|
title={t('Crash Free Sessions')}
|
|
help={getSessionTermDescription(SessionTerm.STABILITY, null)}
|
|
router={router}
|
|
api={api}
|
|
organization={organization}
|
|
onTotalValuesChange={this.handleTotalValuesChange}
|
|
displayMode={displayMode}
|
|
query={query}
|
|
/>
|
|
)}
|
|
{hasAnrRateFeature && displayMode === DisplayModes.ANR_RATE && (
|
|
<ProjectBaseSessionsChart
|
|
title={t('ANR Rate')}
|
|
help={getSessionTermDescription(SessionTerm.ANR_RATE, null)}
|
|
router={router}
|
|
api={api}
|
|
organization={organization}
|
|
onTotalValuesChange={this.handleTotalValuesChange}
|
|
displayMode={displayMode}
|
|
query={query}
|
|
/>
|
|
)}
|
|
{hasAnrRateFeature && displayMode === DisplayModes.FOREGROUND_ANR_RATE && (
|
|
<ProjectBaseSessionsChart
|
|
title={t('Foreground ANR Rate')}
|
|
help={getSessionTermDescription(SessionTerm.FOREGROUND_ANR_RATE, null)}
|
|
router={router}
|
|
api={api}
|
|
organization={organization}
|
|
onTotalValuesChange={this.handleTotalValuesChange}
|
|
displayMode={displayMode}
|
|
query={query}
|
|
/>
|
|
)}
|
|
{displayMode === DisplayModes.STABILITY_USERS && (
|
|
<ProjectBaseSessionsChart
|
|
title={t('Crash Free Users')}
|
|
help={getSessionTermDescription(SessionTerm.CRASH_FREE_USERS, null)}
|
|
router={router}
|
|
api={api}
|
|
organization={organization}
|
|
onTotalValuesChange={this.handleTotalValuesChange}
|
|
displayMode={displayMode}
|
|
query={query}
|
|
/>
|
|
)}
|
|
{displayMode === DisplayModes.SESSIONS && (
|
|
<ProjectBaseSessionsChart
|
|
title={t('Number of Sessions')}
|
|
router={router}
|
|
api={api}
|
|
organization={organization}
|
|
onTotalValuesChange={this.handleTotalValuesChange}
|
|
displayMode={displayMode}
|
|
disablePrevious
|
|
query={query}
|
|
/>
|
|
)}
|
|
</Fragment>
|
|
)}
|
|
</ChartContainer>
|
|
<ChartControls>
|
|
{/* if hasSessions is not yet defined, it means that request is still in progress and we can't decide what default chart to show */}
|
|
{defined(hasSessions) ? (
|
|
<Fragment>
|
|
<InlineContainer>
|
|
<SectionHeading>{this.summaryHeading}</SectionHeading>
|
|
<SectionValue>
|
|
{typeof totalValues === 'number'
|
|
? totalValues.toLocaleString()
|
|
: '\u2014'}
|
|
</SectionValue>
|
|
</InlineContainer>
|
|
<InlineContainer>
|
|
<OptionSelector
|
|
title={t('Display')}
|
|
selected={displayMode}
|
|
options={this.displayModes}
|
|
onChange={this.handleDisplayModeChange}
|
|
/>
|
|
</InlineContainer>
|
|
</Fragment>
|
|
) : (
|
|
<Placeholder height="34px" />
|
|
)}
|
|
</ChartControls>
|
|
</Panel>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default withApi(withTheme(ProjectCharts));
|