mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
528 lines
15 KiB
TypeScript
528 lines
15 KiB
TypeScript
import {Component} from 'react';
|
|
import type {Location} from 'history';
|
|
import isEqual from 'lodash/isEqual';
|
|
import omit from 'lodash/omit';
|
|
import pick from 'lodash/pick';
|
|
import moment from 'moment';
|
|
|
|
import {addErrorMessage} from 'sentry/actionCreators/indicator';
|
|
import type {Client} from 'sentry/api';
|
|
import type {DateTimeObject} from 'sentry/components/charts/utils';
|
|
import {
|
|
getDiffInMinutes,
|
|
ONE_WEEK,
|
|
TWENTY_FOUR_HOURS,
|
|
TWO_WEEKS,
|
|
} from 'sentry/components/charts/utils';
|
|
import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
|
|
import {URL_PARAM} from 'sentry/constants/pageFilters';
|
|
import {t} from 'sentry/locale';
|
|
import type {Organization, PageFilters, SessionApiResponse} from 'sentry/types';
|
|
import {HealthStatsPeriodOption, SessionFieldWithOperation} from 'sentry/types';
|
|
import {defined, percent} from 'sentry/utils';
|
|
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
|
|
import withApi from 'sentry/utils/withApi';
|
|
|
|
import {getCrashFreePercent} from '../utils';
|
|
|
|
import {ReleasesDisplayOption} from './releasesDisplayOptions';
|
|
|
|
function omitIgnoredProps(props: Props) {
|
|
return omit(props, [
|
|
'api',
|
|
'organization',
|
|
'children',
|
|
'selection.datetime.utc',
|
|
'location',
|
|
]);
|
|
}
|
|
|
|
function getInterval(datetimeObj: DateTimeObject) {
|
|
const diffInMinutes = getDiffInMinutes(datetimeObj);
|
|
|
|
if (diffInMinutes >= TWO_WEEKS) {
|
|
return '1d';
|
|
}
|
|
if (diffInMinutes >= ONE_WEEK) {
|
|
return '6h';
|
|
}
|
|
|
|
if (diffInMinutes > TWENTY_FOUR_HOURS) {
|
|
return '4h';
|
|
}
|
|
|
|
// TODO(sessions): sub-hour session resolution is still not possible
|
|
return '1h';
|
|
}
|
|
export function reduceTimeSeriesGroups(
|
|
acc: number[],
|
|
group: SessionApiResponse['groups'][number],
|
|
field: 'count_unique(user)' | 'sum(session)'
|
|
) {
|
|
group.series[field]?.forEach(
|
|
(value, index) => (acc[index] = (acc[index] ?? 0) + value)
|
|
);
|
|
|
|
return acc;
|
|
}
|
|
|
|
export function sessionDisplayToField(display: ReleasesDisplayOption) {
|
|
switch (display) {
|
|
case ReleasesDisplayOption.USERS:
|
|
return SessionFieldWithOperation.USERS;
|
|
case ReleasesDisplayOption.SESSIONS:
|
|
default:
|
|
return SessionFieldWithOperation.SESSIONS;
|
|
}
|
|
}
|
|
|
|
export type ReleasesRequestRenderProps = {
|
|
errored: boolean;
|
|
getHealthData: ReturnType<ReleasesRequest['getHealthData']>;
|
|
isHealthLoading: boolean;
|
|
};
|
|
|
|
type Props = {
|
|
api: Client;
|
|
children: (renderProps: ReleasesRequestRenderProps) => React.ReactNode;
|
|
display: ReleasesDisplayOption[];
|
|
location: Location;
|
|
organization: Organization;
|
|
releases: string[];
|
|
selection: PageFilters;
|
|
defaultStatsPeriod?: string;
|
|
disable?: boolean;
|
|
healthStatsPeriod?: HealthStatsPeriodOption;
|
|
releasesReloading?: boolean;
|
|
};
|
|
type State = {
|
|
errored: boolean;
|
|
loading: boolean;
|
|
statusCountByProjectInPeriod: SessionApiResponse | null;
|
|
statusCountByReleaseInPeriod: SessionApiResponse | null;
|
|
totalCountByProjectIn24h: SessionApiResponse | null;
|
|
totalCountByProjectInPeriod: SessionApiResponse | null;
|
|
totalCountByReleaseIn24h: SessionApiResponse | null;
|
|
totalCountByReleaseInPeriod: SessionApiResponse | null;
|
|
};
|
|
|
|
class ReleasesRequest extends Component<Props, State> {
|
|
state: State = {
|
|
loading: false,
|
|
errored: false,
|
|
statusCountByReleaseInPeriod: null,
|
|
totalCountByReleaseIn24h: null,
|
|
totalCountByProjectIn24h: null,
|
|
statusCountByProjectInPeriod: null,
|
|
totalCountByReleaseInPeriod: null,
|
|
totalCountByProjectInPeriod: null,
|
|
};
|
|
|
|
componentDidMount() {
|
|
this.fetchData();
|
|
}
|
|
|
|
componentDidUpdate(prevProps: Props) {
|
|
if (this.props.releasesReloading) {
|
|
return;
|
|
}
|
|
if (isEqual(omitIgnoredProps(prevProps), omitIgnoredProps(this.props))) {
|
|
return;
|
|
}
|
|
|
|
this.fetchData();
|
|
}
|
|
|
|
get path() {
|
|
const {organization} = this.props;
|
|
|
|
return `/organizations/${organization.slug}/sessions/`;
|
|
}
|
|
|
|
get baseQueryParams() {
|
|
const {location, selection, defaultStatsPeriod, releases} = this.props;
|
|
|
|
return {
|
|
query: new MutableSearch(
|
|
releases.reduce<string[]>((acc, release, index, allReleases) => {
|
|
acc.push(`release:"${release}"`);
|
|
if (index < allReleases.length - 1) {
|
|
acc.push('OR');
|
|
}
|
|
|
|
return acc;
|
|
}, [])
|
|
).formatString(),
|
|
interval: getInterval(selection.datetime),
|
|
...normalizeDateTimeParams(pick(location.query, Object.values(URL_PARAM)), {
|
|
defaultStatsPeriod,
|
|
}),
|
|
};
|
|
}
|
|
|
|
fetchData = async () => {
|
|
const {api, healthStatsPeriod, disable} = this.props;
|
|
|
|
if (disable) {
|
|
return;
|
|
}
|
|
|
|
api.clear();
|
|
this.setState({
|
|
loading: true,
|
|
errored: false,
|
|
statusCountByReleaseInPeriod: null,
|
|
totalCountByReleaseIn24h: null,
|
|
totalCountByProjectIn24h: null,
|
|
});
|
|
|
|
const promises = [
|
|
this.fetchStatusCountByReleaseInPeriod(),
|
|
this.fetchTotalCountByReleaseIn24h(),
|
|
this.fetchTotalCountByProjectIn24h(),
|
|
];
|
|
|
|
if (healthStatsPeriod === HealthStatsPeriodOption.AUTO) {
|
|
promises.push(this.fetchStatusCountByProjectInPeriod());
|
|
promises.push(this.fetchTotalCountByReleaseInPeriod());
|
|
promises.push(this.fetchTotalCountByProjectInPeriod());
|
|
}
|
|
|
|
try {
|
|
const [
|
|
statusCountByReleaseInPeriod,
|
|
totalCountByReleaseIn24h,
|
|
totalCountByProjectIn24h,
|
|
statusCountByProjectInPeriod,
|
|
totalCountByReleaseInPeriod,
|
|
totalCountByProjectInPeriod,
|
|
] = await Promise.all(promises);
|
|
|
|
this.setState({
|
|
loading: false,
|
|
statusCountByReleaseInPeriod,
|
|
totalCountByReleaseIn24h,
|
|
totalCountByProjectIn24h,
|
|
statusCountByProjectInPeriod,
|
|
totalCountByReleaseInPeriod,
|
|
totalCountByProjectInPeriod,
|
|
});
|
|
} catch (error) {
|
|
addErrorMessage(error.responseJSON?.detail ?? t('Error loading health data'));
|
|
this.setState({
|
|
loading: false,
|
|
errored: true,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Used to calculate crash free rate, count histogram (This Release series), and crash count
|
|
*/
|
|
async fetchStatusCountByReleaseInPeriod() {
|
|
const {api, display} = this.props;
|
|
|
|
const response: SessionApiResponse = await api.requestPromise(this.path, {
|
|
query: {
|
|
...this.baseQueryParams,
|
|
field: [
|
|
...new Set([...display.map(d => sessionDisplayToField(d)), 'sum(session)']),
|
|
], // this request needs to be fired for sessions in both display options (because of crash count), removing potential sum(session) duplicated with Set
|
|
groupBy: ['project', 'release', 'session.status'],
|
|
},
|
|
});
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Used to calculate count histogram (Total Project series)
|
|
*/
|
|
async fetchStatusCountByProjectInPeriod() {
|
|
const {api, display} = this.props;
|
|
|
|
const response: SessionApiResponse = await api.requestPromise(this.path, {
|
|
query: {
|
|
...this.baseQueryParams,
|
|
query: undefined,
|
|
field: [
|
|
...new Set([...display.map(d => sessionDisplayToField(d)), 'sum(session)']),
|
|
],
|
|
groupBy: ['project', 'session.status'],
|
|
},
|
|
});
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Used to calculate adoption, and count histogram (This Release series)
|
|
*/
|
|
async fetchTotalCountByReleaseIn24h() {
|
|
const {api, display} = this.props;
|
|
|
|
const response: SessionApiResponse = await api.requestPromise(this.path, {
|
|
query: {
|
|
...this.baseQueryParams,
|
|
field: display.map(d => sessionDisplayToField(d)),
|
|
groupBy: ['project', 'release'],
|
|
interval: '1h',
|
|
statsPeriod: '24h',
|
|
},
|
|
});
|
|
|
|
return response;
|
|
}
|
|
|
|
async fetchTotalCountByReleaseInPeriod() {
|
|
const {api, display} = this.props;
|
|
|
|
const response: SessionApiResponse = await api.requestPromise(this.path, {
|
|
query: {
|
|
...this.baseQueryParams,
|
|
field: display.map(d => sessionDisplayToField(d)),
|
|
groupBy: ['project', 'release'],
|
|
},
|
|
});
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Used to calculate adoption, and count histogram (Total Project series)
|
|
*/
|
|
async fetchTotalCountByProjectIn24h() {
|
|
const {api, display} = this.props;
|
|
|
|
const response: SessionApiResponse = await api.requestPromise(this.path, {
|
|
query: {
|
|
...this.baseQueryParams,
|
|
query: undefined,
|
|
field: display.map(d => sessionDisplayToField(d)),
|
|
groupBy: ['project'],
|
|
interval: '1h',
|
|
statsPeriod: '24h',
|
|
},
|
|
});
|
|
|
|
return response;
|
|
}
|
|
|
|
async fetchTotalCountByProjectInPeriod() {
|
|
const {api, display} = this.props;
|
|
|
|
const response: SessionApiResponse = await api.requestPromise(this.path, {
|
|
query: {
|
|
...this.baseQueryParams,
|
|
query: undefined,
|
|
field: display.map(d => sessionDisplayToField(d)),
|
|
groupBy: ['project'],
|
|
},
|
|
});
|
|
|
|
return response;
|
|
}
|
|
|
|
getHealthData = () => {
|
|
// TODO(sessions): investigate if this needs to be optimized to lower O(n) complexity
|
|
return {
|
|
getCrashCount: this.getCrashCount,
|
|
getCrashFreeRate: this.getCrashFreeRate,
|
|
get24hCountByRelease: this.get24hCountByRelease,
|
|
get24hCountByProject: this.get24hCountByProject,
|
|
getTimeSeries: this.getTimeSeries,
|
|
getAdoption: this.getAdoption,
|
|
};
|
|
};
|
|
|
|
getCrashCount = (version: string, project: number, display: ReleasesDisplayOption) => {
|
|
const {statusCountByReleaseInPeriod} = this.state;
|
|
const field = sessionDisplayToField(display);
|
|
|
|
return statusCountByReleaseInPeriod?.groups.find(
|
|
({by}) =>
|
|
by.release === version &&
|
|
by.project === project &&
|
|
by['session.status'] === 'crashed'
|
|
)?.totals[field];
|
|
};
|
|
|
|
getCrashFreeRate = (
|
|
version: string,
|
|
project: number,
|
|
display: ReleasesDisplayOption
|
|
) => {
|
|
const {statusCountByReleaseInPeriod} = this.state;
|
|
const field = sessionDisplayToField(display);
|
|
|
|
const totalCount = statusCountByReleaseInPeriod?.groups
|
|
.filter(({by}) => by.release === version && by.project === project)
|
|
?.reduce((acc, group) => acc + group.totals[field], 0);
|
|
|
|
const crashedCount = this.getCrashCount(version, project, display);
|
|
|
|
return !defined(totalCount) || totalCount === 0
|
|
? null
|
|
: getCrashFreePercent(100 - percent(crashedCount ?? 0, totalCount ?? 0));
|
|
};
|
|
|
|
get24hCountByRelease = (
|
|
version: string,
|
|
project: number,
|
|
display: ReleasesDisplayOption
|
|
) => {
|
|
const {totalCountByReleaseIn24h} = this.state;
|
|
const field = sessionDisplayToField(display);
|
|
|
|
return totalCountByReleaseIn24h?.groups
|
|
.filter(({by}) => by.release === version && by.project === project)
|
|
?.reduce((acc, group) => acc + group.totals[field], 0);
|
|
};
|
|
|
|
getPeriodCountByRelease = (
|
|
version: string,
|
|
project: number,
|
|
display: ReleasesDisplayOption
|
|
) => {
|
|
const {totalCountByReleaseInPeriod} = this.state;
|
|
const field = sessionDisplayToField(display);
|
|
|
|
return totalCountByReleaseInPeriod?.groups
|
|
.filter(({by}) => by.release === version && by.project === project)
|
|
?.reduce((acc, group) => acc + group.totals[field], 0);
|
|
};
|
|
|
|
get24hCountByProject = (project: number, display: ReleasesDisplayOption) => {
|
|
const {totalCountByProjectIn24h} = this.state;
|
|
const field = sessionDisplayToField(display);
|
|
|
|
return totalCountByProjectIn24h?.groups
|
|
.filter(({by}) => by.project === project)
|
|
?.reduce((acc, group) => acc + group.totals[field], 0);
|
|
};
|
|
|
|
getPeriodCountByProject = (project: number, display: ReleasesDisplayOption) => {
|
|
const {totalCountByProjectInPeriod} = this.state;
|
|
const field = sessionDisplayToField(display);
|
|
|
|
return totalCountByProjectInPeriod?.groups
|
|
.filter(({by}) => by.project === project)
|
|
?.reduce((acc, group) => acc + group.totals[field], 0);
|
|
};
|
|
|
|
getTimeSeries = (version: string, project: number, display: ReleasesDisplayOption) => {
|
|
const {healthStatsPeriod} = this.props;
|
|
if (healthStatsPeriod === HealthStatsPeriodOption.AUTO) {
|
|
return this.getPeriodTimeSeries(version, project, display);
|
|
}
|
|
|
|
return this.get24hTimeSeries(version, project, display);
|
|
};
|
|
|
|
get24hTimeSeries = (
|
|
version: string,
|
|
project: number,
|
|
display: ReleasesDisplayOption
|
|
) => {
|
|
const {totalCountByReleaseIn24h, totalCountByProjectIn24h} = this.state;
|
|
const field = sessionDisplayToField(display);
|
|
|
|
const intervals = totalCountByProjectIn24h?.intervals ?? [];
|
|
|
|
const projectData = totalCountByProjectIn24h?.groups.find(
|
|
({by}) => by.project === project
|
|
)?.series[field];
|
|
|
|
const releaseData = totalCountByReleaseIn24h?.groups.find(
|
|
({by}) => by.project === project && by.release === version
|
|
)?.series[field];
|
|
|
|
return [
|
|
{
|
|
seriesName: t('This Release'),
|
|
data: intervals?.map((interval, index) => ({
|
|
name: moment(interval).valueOf(),
|
|
value: releaseData?.[index] ?? 0,
|
|
})),
|
|
},
|
|
{
|
|
seriesName: t('Total Project'),
|
|
data: intervals?.map((interval, index) => ({
|
|
name: moment(interval).valueOf(),
|
|
value: projectData?.[index] ?? 0,
|
|
})),
|
|
z: 0,
|
|
},
|
|
];
|
|
};
|
|
|
|
getPeriodTimeSeries = (
|
|
version: string,
|
|
project: number,
|
|
display: ReleasesDisplayOption
|
|
) => {
|
|
const {statusCountByReleaseInPeriod, statusCountByProjectInPeriod} = this.state;
|
|
const field = sessionDisplayToField(display);
|
|
|
|
const intervals = statusCountByProjectInPeriod?.intervals ?? [];
|
|
|
|
const projectData = statusCountByProjectInPeriod?.groups
|
|
.filter(({by}) => by.project === project)
|
|
?.reduce((acc, group) => reduceTimeSeriesGroups(acc, group, field), [] as number[]);
|
|
|
|
const releaseData = statusCountByReleaseInPeriod?.groups
|
|
.filter(({by}) => by.project === project && by.release === version)
|
|
?.reduce((acc, group) => reduceTimeSeriesGroups(acc, group, field), [] as number[]);
|
|
|
|
return [
|
|
{
|
|
seriesName: t('This Release'),
|
|
data: intervals?.map((interval, index) => ({
|
|
name: moment(interval).valueOf(),
|
|
value: releaseData?.[index] ?? 0,
|
|
})),
|
|
},
|
|
{
|
|
seriesName: t('Total Project'),
|
|
data: intervals?.map((interval, index) => ({
|
|
name: moment(interval).valueOf(),
|
|
value: projectData?.[index] ?? 0,
|
|
})),
|
|
z: 0,
|
|
},
|
|
];
|
|
};
|
|
|
|
getAdoption = (version: string, project: number, display: ReleasesDisplayOption) => {
|
|
const {healthStatsPeriod} = this.props;
|
|
|
|
const countByRelease = (
|
|
healthStatsPeriod === HealthStatsPeriodOption.AUTO
|
|
? this.getPeriodCountByRelease
|
|
: this.get24hCountByRelease
|
|
)(version, project, display);
|
|
const countByProject = (
|
|
healthStatsPeriod === HealthStatsPeriodOption.AUTO
|
|
? this.getPeriodCountByProject
|
|
: this.get24hCountByProject
|
|
)(project, display);
|
|
|
|
return defined(countByRelease) && defined(countByProject)
|
|
? percent(countByRelease, countByProject)
|
|
: undefined;
|
|
};
|
|
|
|
render() {
|
|
const {loading, errored} = this.state;
|
|
const {children} = this.props;
|
|
|
|
return children({
|
|
isHealthLoading: loading,
|
|
errored,
|
|
getHealthData: this.getHealthData(),
|
|
});
|
|
}
|
|
}
|
|
|
|
export default withApi(ReleasesRequest);
|