123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- import {Component} from 'react';
- import {Theme, withTheme} from '@emotion/react';
- import {LineSeriesOption} from 'echarts';
- import isEqual from 'lodash/isEqual';
- import omit from 'lodash/omit';
- import {addErrorMessage} from 'sentry/actionCreators/indicator';
- import {Client} from 'sentry/api';
- import {shouldFetchPreviousPeriod} from 'sentry/components/charts/utils';
- import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
- import {t} from 'sentry/locale';
- import {
- Organization,
- PageFilters,
- SessionApiResponse,
- SessionFieldWithOperation,
- SessionStatus,
- } from 'sentry/types';
- import {Series} from 'sentry/types/echarts';
- import {percent} from 'sentry/utils';
- import {getPeriod} from 'sentry/utils/getPeriod';
- import {
- filterSessionsInTimeWindow,
- getCount,
- getCountSeries,
- getSessionsInterval,
- initSessionsChart,
- } from 'sentry/utils/sessions';
- import {getCrashFreePercent} from 'sentry/views/releases/utils';
- import {DisplayModes} from '../projectCharts';
- const omitIgnoredProps = (props: ProjectSessionsChartRequestProps) =>
- omit(props, ['api', 'organization', 'children', 'selection.datetime.utc']);
- type ProjectSessionsChartRequestRenderProps = {
- errored: boolean;
- loading: boolean;
- previousTimeseriesData: Series | null;
- reloading: boolean;
- timeseriesData: Series[];
- totalSessions: number | null;
- additionalSeries?: LineSeriesOption[];
- };
- export type ProjectSessionsChartRequestProps = {
- api: Client;
- children: (renderProps: ProjectSessionsChartRequestRenderProps) => React.ReactNode;
- displayMode:
- | DisplayModes.SESSIONS
- | DisplayModes.STABILITY
- | DisplayModes.STABILITY_USERS
- | DisplayModes.ANR_RATE
- | DisplayModes.FOREGROUND_ANR_RATE;
- onTotalValuesChange: (value: number | null) => void;
- organization: Organization;
- selection: PageFilters;
- theme: Theme;
- disablePrevious?: boolean;
- query?: string;
- };
- type State = {
- errored: boolean;
- previousTimeseriesData: Series | null;
- reloading: boolean;
- timeseriesData: Series[] | null;
- totalSessions: number | null;
- };
- class ProjectSessionsChartRequest extends Component<
- ProjectSessionsChartRequestProps,
- State
- > {
- state: State = {
- reloading: false,
- errored: false,
- timeseriesData: null,
- previousTimeseriesData: null,
- totalSessions: null,
- };
- componentDidMount() {
- this.fetchData();
- }
- componentDidUpdate(prevProps: ProjectSessionsChartRequestProps) {
- if (!isEqual(omitIgnoredProps(this.props), omitIgnoredProps(prevProps))) {
- this.fetchData();
- }
- }
- componentWillUnmount() {
- this.unmounting = true;
- }
- private unmounting: boolean = false;
- fetchData = async () => {
- const {
- api,
- selection: {datetime},
- onTotalValuesChange,
- displayMode,
- disablePrevious,
- } = this.props;
- const shouldFetchWithPrevious =
- !disablePrevious &&
- shouldFetchPreviousPeriod({
- start: datetime.start,
- end: datetime.end,
- period: datetime.period,
- });
- this.setState(state => ({
- reloading: state.timeseriesData !== null,
- errored: false,
- }));
- try {
- const queryParams = this.queryParams({shouldFetchWithPrevious});
- const requests = [
- api.requestPromise(this.path, {
- query: queryParams,
- }),
- ];
- // for users, we need to make a separate request to get the total count in period
- if (displayMode === DisplayModes.STABILITY_USERS) {
- requests.push(
- api.requestPromise(this.path, {
- query: {
- ...queryParams,
- groupBy: undefined,
- ...(shouldFetchWithPrevious ? {statsPeriod: datetime.period} : {}),
- },
- })
- );
- }
- const [response, responseUsersTotal]: SessionApiResponse[] =
- await Promise.all(requests);
- const filteredResponse = filterSessionsInTimeWindow(
- response,
- queryParams.start,
- queryParams.end
- );
- // totalSessions can't be used when we're talking about users
- // users are a set and counting together buckets or statuses is not correct
- // because one user can be present in multiple buckets/statuses
- const {timeseriesData, previousTimeseriesData, totalSessions} =
- displayMode === DisplayModes.SESSIONS
- ? this.transformSessionCountData(filteredResponse)
- : this.transformData(filteredResponse, {
- fetchedWithPrevious: shouldFetchWithPrevious,
- });
- const totalUsers = responseUsersTotal?.groups[0].totals[this.field];
- const totalNumber =
- displayMode === DisplayModes.STABILITY_USERS ? totalUsers : totalSessions;
- if (this.unmounting) {
- return;
- }
- this.setState({
- reloading: false,
- timeseriesData,
- previousTimeseriesData,
- totalSessions: totalNumber,
- });
- onTotalValuesChange(totalNumber);
- } catch {
- addErrorMessage(t('Error loading chart data'));
- this.setState({
- errored: true,
- reloading: false,
- timeseriesData: null,
- previousTimeseriesData: null,
- totalSessions: null,
- });
- }
- };
- get path() {
- const {organization} = this.props;
- return `/organizations/${organization.slug}/sessions/`;
- }
- get field() {
- const {displayMode} = this.props;
- return displayMode === DisplayModes.STABILITY_USERS
- ? SessionFieldWithOperation.USERS
- : SessionFieldWithOperation.SESSIONS;
- }
- queryParams({shouldFetchWithPrevious = false}): Record<string, any> {
- const {selection, query, organization} = this.props;
- const {datetime, projects, environments: environment} = selection;
- const baseParams = {
- field: this.field,
- groupBy: 'session.status',
- interval: getSessionsInterval(datetime, {
- highFidelity: organization.features.includes('minute-resolution-sessions'),
- }),
- project: projects[0],
- environment,
- query,
- };
- if (!shouldFetchWithPrevious) {
- return {
- ...baseParams,
- ...normalizeDateTimeParams(datetime),
- };
- }
- const {period} = selection.datetime;
- const doubledPeriod = getPeriod(
- {period, start: undefined, end: undefined},
- {shouldDoublePeriod: true}
- ).statsPeriod;
- return {
- ...baseParams,
- statsPeriod: doubledPeriod,
- };
- }
- transformData(responseData: SessionApiResponse, {fetchedWithPrevious = false}) {
- const {theme} = this.props;
- const {field} = this;
- // Take the floor just in case, but data should always be divisible by 2
- const dataMiddleIndex = Math.floor(responseData.intervals.length / 2);
- // calculate the total number of sessions for this period (exclude previous if there)
- const totalSessions = responseData.groups.reduce(
- (acc, group) =>
- acc +
- group.series[field]
- .slice(fetchedWithPrevious ? dataMiddleIndex : 0)
- .reduce((value, groupAcc) => groupAcc + value, 0),
- 0
- );
- const previousPeriodTotalSessions = fetchedWithPrevious
- ? responseData.groups.reduce(
- (acc, group) =>
- acc +
- group.series[field]
- .slice(0, dataMiddleIndex)
- .reduce((value, groupAcc) => groupAcc + value, 0),
- 0
- )
- : 0;
- // TODO(project-details): refactor this to avoid duplication as we add more session charts
- const timeseriesData = [
- {
- seriesName: t('This Period'),
- color: theme.green300,
- data: responseData.intervals
- .slice(fetchedWithPrevious ? dataMiddleIndex : 0)
- .map((interval, i) => {
- const totalIntervalSessions = responseData.groups.reduce(
- (acc, group) =>
- acc +
- group.series[field].slice(fetchedWithPrevious ? dataMiddleIndex : 0)[i],
- 0
- );
- const intervalCrashedSessions =
- responseData.groups
- .find(group => group.by['session.status'] === 'crashed')
- ?.series[field].slice(fetchedWithPrevious ? dataMiddleIndex : 0)[i] ?? 0;
- const crashedSessionsPercent = percent(
- intervalCrashedSessions,
- totalIntervalSessions
- );
- return {
- name: interval,
- value:
- totalSessions === 0 && previousPeriodTotalSessions === 0
- ? 0
- : totalIntervalSessions === 0
- ? null
- : getCrashFreePercent(100 - crashedSessionsPercent),
- };
- }),
- },
- ] as Series[]; // TODO(project-detail): Change SeriesDataUnit value to support null
- const previousTimeseriesData = fetchedWithPrevious
- ? ({
- seriesName: t('Previous Period'),
- data: responseData.intervals.slice(0, dataMiddleIndex).map((_interval, i) => {
- const totalIntervalSessions = responseData.groups.reduce(
- (acc, group) => acc + group.series[field].slice(0, dataMiddleIndex)[i],
- 0
- );
- const intervalCrashedSessions =
- responseData.groups
- .find(group => group.by['session.status'] === 'crashed')
- ?.series[field].slice(0, dataMiddleIndex)[i] ?? 0;
- const crashedSessionsPercent = percent(
- intervalCrashedSessions,
- totalIntervalSessions
- );
- return {
- name: responseData.intervals[i + dataMiddleIndex],
- value:
- totalSessions === 0 && previousPeriodTotalSessions === 0
- ? 0
- : totalIntervalSessions === 0
- ? null
- : getCrashFreePercent(100 - crashedSessionsPercent),
- };
- }),
- } as Series) // TODO(project-detail): Change SeriesDataUnit value to support null
- : null;
- return {
- totalSessions,
- timeseriesData,
- previousTimeseriesData,
- };
- }
- transformSessionCountData(responseData: SessionApiResponse) {
- const {theme} = this.props;
- const sessionsChart = initSessionsChart(theme);
- const {intervals, groups} = responseData;
- const totalSessions = getCount(
- responseData.groups,
- SessionFieldWithOperation.SESSIONS
- );
- const chartData = [
- {
- ...sessionsChart[SessionStatus.HEALTHY],
- data: getCountSeries(
- SessionFieldWithOperation.SESSIONS,
- groups.find(g => g.by['session.status'] === SessionStatus.HEALTHY),
- intervals
- ),
- },
- {
- ...sessionsChart[SessionStatus.ERRORED],
- data: getCountSeries(
- SessionFieldWithOperation.SESSIONS,
- groups.find(g => g.by['session.status'] === SessionStatus.ERRORED),
- intervals
- ),
- },
- {
- ...sessionsChart[SessionStatus.ABNORMAL],
- data: getCountSeries(
- SessionFieldWithOperation.SESSIONS,
- groups.find(g => g.by['session.status'] === SessionStatus.ABNORMAL),
- intervals
- ),
- },
- {
- ...sessionsChart[SessionStatus.CRASHED],
- data: getCountSeries(
- SessionFieldWithOperation.SESSIONS,
- groups.find(g => g.by['session.status'] === SessionStatus.CRASHED),
- intervals
- ),
- },
- ];
- return {
- timeseriesData: chartData,
- previousTimeseriesData: null,
- totalSessions,
- };
- }
- render() {
- const {children} = this.props;
- const {timeseriesData, reloading, errored, totalSessions, previousTimeseriesData} =
- this.state;
- const loading = timeseriesData === null;
- return children({
- loading,
- reloading,
- errored,
- totalSessions,
- previousTimeseriesData,
- timeseriesData: timeseriesData ?? [],
- });
- }
- }
- export default withTheme(ProjectSessionsChartRequest);
|