123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- import {Component} from 'react';
- import type {Theme} from '@emotion/react';
- import {withTheme} from '@emotion/react';
- import type {LineSeriesOption} from 'echarts';
- import isEqual from 'lodash/isEqual';
- import omit from 'lodash/omit';
- import {addErrorMessage} from 'sentry/actionCreators/indicator';
- import type {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 type {Organization, PageFilters, SessionApiResponse} from 'sentry/types';
- import {SessionFieldWithOperation, SessionStatus} from 'sentry/types';
- import type {Series} from 'sentry/types/echarts';
- import {getPeriod} from 'sentry/utils/duration/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
- // ANR is handled by the ProjectSessionsAnrRequest component
- | 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 crash free sessions and users, we need to make a separate request to get the total count in period
- if (this.isCrashFreeRate) {
- requests.push(
- api.requestPromise(this.path, {
- query: {
- ...queryParams,
- field:
- displayMode === DisplayModes.STABILITY_USERS
- ? SessionFieldWithOperation.USERS
- : SessionFieldWithOperation.SESSIONS,
- groupBy: undefined,
- ...(shouldFetchWithPrevious ? {statsPeriod: datetime.period} : {}),
- },
- })
- );
- }
- const [response, totalCountResponse]: SessionApiResponse[] =
- await Promise.all(requests);
- const filteredResponse = filterSessionsInTimeWindow(
- response,
- queryParams.start,
- queryParams.end
- );
- const {timeseriesData, previousTimeseriesData, totalCount} =
- displayMode === DisplayModes.SESSIONS
- ? this.transformSessionCountData(filteredResponse)
- : this.transformData(filteredResponse, totalCountResponse, {
- fetchedWithPrevious: shouldFetchWithPrevious,
- });
- if (this.unmounting) {
- return;
- }
- this.setState({
- reloading: false,
- timeseriesData,
- previousTimeseriesData,
- totalSessions: totalCount,
- });
- onTotalValuesChange(totalCount);
- } 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;
- switch (displayMode) {
- case DisplayModes.STABILITY_USERS:
- return SessionFieldWithOperation.CRASH_FREE_RATE_USERS;
- case DisplayModes.STABILITY:
- return SessionFieldWithOperation.CRASH_FREE_RATE_SESSIONS;
- default:
- return SessionFieldWithOperation.SESSIONS;
- }
- }
- get isCrashFreeRate() {
- return [DisplayModes.STABILITY, DisplayModes.STABILITY_USERS].includes(
- this.props.displayMode
- );
- }
- queryParams({shouldFetchWithPrevious = false}): Record<string, any> {
- const {selection, query, organization} = this.props;
- const {datetime, projects, environments: environment} = selection;
- const baseParams = {
- field: this.field,
- groupBy: this.isCrashFreeRate ? undefined : '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,
- totalCountResponse: 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);
- // 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 crashedSessionsPercent =
- responseData.groups[0]?.series[field].slice(
- fetchedWithPrevious ? dataMiddleIndex : 0
- )[i] * 100 ?? 0;
- return {
- name: interval,
- value: getCrashFreePercent(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 crashedSessionsPercent =
- responseData.groups[0]?.series[field].slice(0, dataMiddleIndex)[i] * 100 ??
- 0;
- return {
- name: responseData.intervals[i + dataMiddleIndex],
- value: getCrashFreePercent(crashedSessionsPercent),
- };
- }),
- } as Series) // TODO(project-detail): Change SeriesDataUnit value to support null
- : null;
- const totalCount =
- totalCountResponse?.groups[0].totals[
- this.props.displayMode === DisplayModes.STABILITY_USERS
- ? SessionFieldWithOperation.USERS
- : SessionFieldWithOperation.SESSIONS
- ];
- return {
- timeseriesData,
- totalCount,
- 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,
- totalCount: 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);
|