123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- import * as React from 'react';
- import {withTheme} from '@emotion/react';
- import {Location} from 'history';
- import isEqual from 'lodash/isEqual';
- import meanBy from 'lodash/meanBy';
- import omitBy from 'lodash/omitBy';
- import pick from 'lodash/pick';
- import {fetchTotalCount} from 'app/actionCreators/events';
- import {addErrorMessage} from 'app/actionCreators/indicator';
- import {Client} from 'app/api';
- import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams';
- import {URL_PARAM} from 'app/constants/globalSelectionHeader';
- import {t, tct} from 'app/locale';
- import {GlobalSelection, Organization, SessionApiResponse} from 'app/types';
- import {Series} from 'app/types/echarts';
- import {defined} from 'app/utils';
- import {WebVital} from 'app/utils/discover/fields';
- import {getExactDuration} from 'app/utils/formatters';
- import {Theme} from 'app/utils/theme';
- import {MutableSearch} from 'app/utils/tokenizeSearch';
- import {displayCrashFreePercent, roundDuration} from '../../utils';
- import {EventType, YAxis} from './chart/releaseChartControls';
- import {
- fillChartDataFromSessionsResponse,
- fillCrashFreeChartDataFromSessionsReponse,
- getInterval,
- getReleaseEventView,
- getTotalsFromSessionsResponse,
- initCrashFreeChartData,
- initOtherCrashFreeChartData,
- initOtherSessionDurationChartData,
- initOtherSessionsBreakdownChartData,
- initSessionDurationChartData,
- initSessionsBreakdownChartData,
- } from './chart/utils';
- const omitIgnoredProps = (props: Props) =>
- omitBy(props, (_, key) =>
- ['api', 'orgId', 'projectSlug', 'location', 'children'].includes(key)
- );
- type Data = {
- chartData: Series[];
- chartSummary: React.ReactNode;
- };
- export type ReleaseStatsRequestRenderProps = Data & {
- loading: boolean;
- reloading: boolean;
- errored: boolean;
- };
- type Props = {
- api: Client;
- version: string;
- organization: Organization;
- projectSlug: string;
- selection: GlobalSelection;
- location: Location;
- yAxis: YAxis;
- eventType: EventType;
- vitalType: WebVital;
- children: (renderProps: ReleaseStatsRequestRenderProps) => React.ReactNode;
- hasHealthData: boolean;
- hasDiscover: boolean;
- hasPerformance: boolean;
- defaultStatsPeriod: string;
- theme: Theme;
- };
- type State = {
- reloading: boolean;
- errored: boolean;
- data: Data | null;
- };
- class ReleaseStatsRequest extends React.Component<Props, State> {
- state: State = {
- reloading: false,
- errored: false,
- data: null,
- };
- componentDidMount() {
- this.fetchData();
- }
- componentDidUpdate(prevProps: Props) {
- if (isEqual(omitIgnoredProps(prevProps), omitIgnoredProps(this.props))) {
- return;
- }
- this.fetchData();
- }
- componentWillUnmount() {
- this.unmounting = true;
- }
- private unmounting: boolean = false;
- get path() {
- const {organization} = this.props;
- return `/organizations/${organization.slug}/sessions/`;
- }
- get baseQueryParams() {
- const {version, organization, location, selection, defaultStatsPeriod} = this.props;
- return {
- query: new MutableSearch([`release:"${version}"`]).formatString(),
- interval: getInterval(selection.datetime, {
- highFidelity: organization.features.includes('minute-resolution-sessions'),
- }),
- ...getParams(pick(location.query, Object.values(URL_PARAM)), {
- defaultStatsPeriod,
- }),
- };
- }
- fetchData = async () => {
- let data: Data | null = null;
- const {yAxis, hasHealthData, hasDiscover, hasPerformance} = this.props;
- if (!hasHealthData && !hasDiscover && !hasPerformance) {
- return;
- }
- this.setState(state => ({
- reloading: state.data !== null,
- errored: false,
- }));
- try {
- if (yAxis === YAxis.SESSIONS) {
- data = await this.fetchSessions();
- }
- if (yAxis === YAxis.USERS) {
- data = await this.fetchUsers();
- }
- if (yAxis === YAxis.CRASH_FREE) {
- data = await this.fetchCrashFree();
- }
- if (yAxis === YAxis.SESSION_DURATION) {
- data = await this.fetchSessionDuration();
- }
- if (
- yAxis === YAxis.EVENTS ||
- yAxis === YAxis.FAILED_TRANSACTIONS ||
- yAxis === YAxis.COUNT_DURATION ||
- yAxis === YAxis.COUNT_VITAL
- ) {
- // this is used to get total counts for chart footer summary
- data = await this.fetchEventData();
- }
- } catch (error) {
- addErrorMessage(error.responseJSON?.detail ?? t('Error loading chart data'));
- this.setState({
- errored: true,
- data: null,
- });
- }
- if (!defined(data) && !this.state.errored) {
- // this should not happen
- this.setState({
- errored: true,
- data: null,
- });
- }
- if (this.unmounting) {
- return;
- }
- this.setState({
- reloading: false,
- data,
- });
- };
- async fetchSessions() {
- const {api, version, theme} = this.props;
- const [releaseResponse, otherReleasesResponse]: SessionApiResponse[] =
- await Promise.all([
- api.requestPromise(this.path, {
- query: {
- ...this.baseQueryParams,
- field: 'sum(session)',
- groupBy: 'session.status',
- },
- }),
- api.requestPromise(this.path, {
- query: {
- ...this.baseQueryParams,
- field: 'sum(session)',
- groupBy: 'session.status',
- query: new MutableSearch([`!release:"${version}"`]).formatString(),
- },
- }),
- ]);
- const totalSessions = getTotalsFromSessionsResponse({
- response: releaseResponse,
- field: 'sum(session)',
- });
- const chartData = fillChartDataFromSessionsResponse({
- response: releaseResponse,
- field: 'sum(session)',
- groupBy: 'session.status',
- chartData: initSessionsBreakdownChartData(theme),
- });
- const otherChartData = fillChartDataFromSessionsResponse({
- response: otherReleasesResponse,
- field: 'sum(session)',
- groupBy: 'session.status',
- chartData: initOtherSessionsBreakdownChartData(theme),
- });
- return {
- chartData: [...Object.values(chartData), ...Object.values(otherChartData)],
- chartSummary: totalSessions.toLocaleString(),
- };
- }
- async fetchUsers() {
- const {api, version, theme} = this.props;
- const [releaseResponse, otherReleasesResponse]: SessionApiResponse[] =
- await Promise.all([
- api.requestPromise(this.path, {
- query: {
- ...this.baseQueryParams,
- field: 'count_unique(user)',
- groupBy: 'session.status',
- },
- }),
- api.requestPromise(this.path, {
- query: {
- ...this.baseQueryParams,
- field: 'count_unique(user)',
- groupBy: 'session.status',
- query: new MutableSearch([`!release:"${version}"`]).formatString(),
- },
- }),
- ]);
- const totalUsers = getTotalsFromSessionsResponse({
- response: releaseResponse,
- field: 'count_unique(user)',
- });
- const chartData = fillChartDataFromSessionsResponse({
- response: releaseResponse,
- field: 'count_unique(user)',
- groupBy: 'session.status',
- chartData: initSessionsBreakdownChartData(theme),
- });
- const otherChartData = fillChartDataFromSessionsResponse({
- response: otherReleasesResponse,
- field: 'count_unique(user)',
- groupBy: 'session.status',
- chartData: initOtherSessionsBreakdownChartData(theme),
- });
- return {
- chartData: [...Object.values(chartData), ...Object.values(otherChartData)],
- chartSummary: totalUsers.toLocaleString(),
- };
- }
- async fetchCrashFree() {
- const {api, version} = this.props;
- const [releaseResponse, otherReleasesResponse]: SessionApiResponse[] =
- await Promise.all([
- api.requestPromise(this.path, {
- query: {
- ...this.baseQueryParams,
- field: ['sum(session)', 'count_unique(user)'],
- groupBy: 'session.status',
- },
- }),
- api.requestPromise(this.path, {
- query: {
- ...this.baseQueryParams,
- field: ['sum(session)', 'count_unique(user)'],
- groupBy: 'session.status',
- query: new MutableSearch([`!release:"${version}"`]).formatString(),
- },
- }),
- ]);
- let chartData = fillCrashFreeChartDataFromSessionsReponse({
- response: releaseResponse,
- field: 'sum(session)',
- entity: 'sessions',
- chartData: initCrashFreeChartData(),
- });
- chartData = fillCrashFreeChartDataFromSessionsReponse({
- response: releaseResponse,
- field: 'count_unique(user)',
- entity: 'users',
- chartData,
- });
- let otherChartData = fillCrashFreeChartDataFromSessionsReponse({
- response: otherReleasesResponse,
- field: 'sum(session)',
- entity: 'sessions',
- chartData: initOtherCrashFreeChartData(),
- });
- otherChartData = fillCrashFreeChartDataFromSessionsReponse({
- response: otherReleasesResponse,
- field: 'count_unique(user)',
- entity: 'users',
- chartData: otherChartData,
- });
- // summary is averaging previously rounded values - this might lead to a slightly skewed percentage
- const summary = tct('[usersPercent] users, [sessionsPercent] sessions', {
- usersPercent: displayCrashFreePercent(
- meanBy(
- chartData.users.data.filter(item => defined(item.value)),
- 'value'
- )
- ),
- sessionsPercent: displayCrashFreePercent(
- meanBy(
- chartData.sessions.data.filter(item => defined(item.value)),
- 'value'
- )
- ),
- });
- return {
- chartData: [...Object.values(chartData), ...Object.values(otherChartData)],
- chartSummary: summary,
- };
- }
- async fetchSessionDuration() {
- const {api, version} = this.props;
- const [releaseResponse, otherReleasesResponse]: SessionApiResponse[] =
- await Promise.all([
- api.requestPromise(this.path, {
- query: {
- ...this.baseQueryParams,
- field: 'p50(session.duration)',
- },
- }),
- api.requestPromise(this.path, {
- query: {
- ...this.baseQueryParams,
- field: 'p50(session.duration)',
- query: new MutableSearch([`!release:"${version}"`]).formatString(),
- },
- }),
- ]);
- const totalMedianDuration = getTotalsFromSessionsResponse({
- response: releaseResponse,
- field: 'p50(session.duration)',
- });
- const chartData = fillChartDataFromSessionsResponse({
- response: releaseResponse,
- field: 'p50(session.duration)',
- groupBy: null,
- chartData: initSessionDurationChartData(),
- valueFormatter: duration => roundDuration(duration ? duration / 1000 : 0),
- });
- const otherChartData = fillChartDataFromSessionsResponse({
- response: otherReleasesResponse,
- field: 'p50(session.duration)',
- groupBy: null,
- chartData: initOtherSessionDurationChartData(),
- valueFormatter: duration => roundDuration(duration ? duration / 1000 : 0),
- });
- return {
- chartData: [...Object.values(chartData), ...Object.values(otherChartData)],
- chartSummary: getExactDuration(
- roundDuration(totalMedianDuration ? totalMedianDuration / 1000 : 0)
- ),
- };
- }
- async fetchEventData() {
- const {api, organization, location, yAxis, eventType, vitalType, selection, version} =
- this.props;
- const eventView = getReleaseEventView(
- selection,
- version,
- yAxis,
- eventType,
- vitalType,
- organization,
- true
- );
- const payload = eventView.getEventsAPIPayload(location);
- const eventsCountResponse = await fetchTotalCount(api, organization.slug, payload);
- const chartSummary = eventsCountResponse.toLocaleString();
- return {chartData: [], chartSummary};
- }
- render() {
- const {children} = this.props;
- const {data, reloading, errored} = this.state;
- const loading = data === null;
- return children({
- loading,
- reloading,
- errored,
- chartData: data?.chartData ?? [],
- chartSummary: data?.chartSummary ?? '',
- });
- }
- }
- export default withTheme(ReleaseStatsRequest);
|