123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- import round from 'lodash/round';
- import {
- getDiffInMinutes,
- shouldFetchPreviousPeriod,
- } from 'sentry/components/charts/utils';
- import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
- import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
- import ScoreCard from 'sentry/components/scoreCard';
- import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
- import {IconArrow} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {
- Organization,
- PageFilters,
- SessionApiResponse,
- SessionFieldWithOperation,
- } from 'sentry/types';
- import {defined, percent} from 'sentry/utils';
- import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
- import {getPeriod} from 'sentry/utils/getPeriod';
- import {displayCrashFreePercent, getCrashFreePercent} from 'sentry/views/releases/utils';
- import {
- getSessionTermDescription,
- SessionTerm,
- } from 'sentry/views/releases/utils/sessionTerm';
- import MissingReleasesButtons from '../missingFeatureButtons/missingReleasesButtons';
- type Props = DeprecatedAsyncComponent['props'] & {
- field: SessionFieldWithOperation.SESSIONS | SessionFieldWithOperation.USERS;
- hasSessions: boolean | null;
- isProjectStabilized: boolean;
- organization: Organization;
- selection: PageFilters;
- query?: string;
- };
- type State = DeprecatedAsyncComponent['state'] & {
- currentSessions: SessionApiResponse | null;
- previousSessions: SessionApiResponse | null;
- };
- class ProjectStabilityScoreCard extends DeprecatedAsyncComponent<Props, State> {
- shouldRenderBadRequests = true;
- getDefaultState() {
- return {
- ...super.getDefaultState(),
- currentSessions: null,
- previousSessions: null,
- };
- }
- getEndpoints() {
- const {organization, selection, isProjectStabilized, hasSessions, query, field} =
- this.props;
- if (!isProjectStabilized || !hasSessions) {
- return [];
- }
- const {projects, environments: environment, datetime} = selection;
- const {period} = datetime;
- const commonQuery = {
- environment,
- project: projects[0],
- groupBy: 'session.status',
- interval: getDiffInMinutes(datetime) > 24 * 60 ? '1d' : '1h',
- query,
- field,
- };
- // Unfortunately we can't do something like statsPeriod=28d&interval=14d to get scores for this and previous interval with the single request
- // https://github.com/getsentry/sentry/pull/22770#issuecomment-758595553
- const endpoints: ReturnType<DeprecatedAsyncComponent['getEndpoints']> = [
- [
- 'currentSessions',
- `/organizations/${organization.slug}/sessions/`,
- {
- query: {
- ...commonQuery,
- ...normalizeDateTimeParams(datetime),
- },
- },
- ],
- ];
- if (
- shouldFetchPreviousPeriod({
- start: datetime.start,
- end: datetime.end,
- period: datetime.period,
- })
- ) {
- const doubledPeriod = getPeriod(
- {period, start: undefined, end: undefined},
- {shouldDoublePeriod: true}
- ).statsPeriod;
- endpoints.push([
- 'previousSessions',
- `/organizations/${organization.slug}/sessions/`,
- {
- query: {
- ...commonQuery,
- statsPeriodStart: doubledPeriod,
- statsPeriodEnd: period ?? DEFAULT_STATS_PERIOD,
- },
- },
- ]);
- }
- return endpoints;
- }
- get cardTitle() {
- return this.props.field === SessionFieldWithOperation.SESSIONS
- ? t('Crash Free Sessions')
- : t('Crash Free Users');
- }
- get cardHelp() {
- return getSessionTermDescription(
- this.props.field === SessionFieldWithOperation.SESSIONS
- ? SessionTerm.CRASH_FREE_SESSIONS
- : SessionTerm.CRASH_FREE_USERS,
- null
- );
- }
- get score() {
- const {currentSessions} = this.state;
- return this.calculateCrashFree(currentSessions);
- }
- get trend() {
- const {previousSessions} = this.state;
- const previousScore = this.calculateCrashFree(previousSessions);
- if (!defined(this.score) || !defined(previousScore)) {
- return undefined;
- }
- return round(this.score - previousScore, 3);
- }
- get trendStatus(): React.ComponentProps<typeof ScoreCard>['trendStatus'] {
- if (!this.trend) {
- return undefined;
- }
- return this.trend > 0 ? 'good' : 'bad';
- }
- componentDidUpdate(prevProps: Props) {
- const {selection, isProjectStabilized, hasSessions, query} = this.props;
- if (
- prevProps.selection !== selection ||
- prevProps.hasSessions !== hasSessions ||
- prevProps.isProjectStabilized !== isProjectStabilized ||
- prevProps.query !== query
- ) {
- this.remountComponent();
- }
- }
- calculateCrashFree(data?: SessionApiResponse | null) {
- const {field} = this.props;
- if (!data) {
- return undefined;
- }
- const totalSessions = data.groups.reduce(
- (acc, group) => acc + group.totals[field],
- 0
- );
- const crashedSessions = data.groups.find(
- group => group.by['session.status'] === 'crashed'
- )?.totals[field];
- if (totalSessions === 0 || !defined(totalSessions) || !defined(crashedSessions)) {
- return undefined;
- }
- const crashedSessionsPercent = percent(crashedSessions, totalSessions);
- return getCrashFreePercent(100 - crashedSessionsPercent);
- }
- renderLoading() {
- return this.renderBody();
- }
- renderMissingFeatureCard() {
- const {organization} = this.props;
- return (
- <ScoreCard
- title={this.cardTitle}
- help={this.cardHelp}
- score={<MissingReleasesButtons organization={organization} health />}
- />
- );
- }
- renderScore() {
- const {loading} = this.state;
- if (loading || !defined(this.score)) {
- return '\u2014';
- }
- return displayCrashFreePercent(this.score);
- }
- renderTrend() {
- const {loading} = this.state;
- if (loading || !defined(this.score) || !defined(this.trend)) {
- return null;
- }
- return (
- <div>
- {this.trend >= 0 ? (
- <IconArrow direction="up" size="xs" />
- ) : (
- <IconArrow direction="down" size="xs" />
- )}
- {`${formatAbbreviatedNumber(Math.abs(this.trend))}\u0025`}
- </div>
- );
- }
- renderBody() {
- const {hasSessions} = this.props;
- if (hasSessions === false) {
- return this.renderMissingFeatureCard();
- }
- return (
- <ScoreCard
- title={this.cardTitle}
- help={this.cardHelp}
- score={this.renderScore()}
- trend={this.renderTrend()}
- trendStatus={this.trendStatus}
- />
- );
- }
- }
- export default ProjectStabilityScoreCard;
|