Files
2024-02-15 17:01:50 +00:00

366 lines
9.7 KiB
TypeScript

import type {Theme} from '@emotion/react';
import compact from 'lodash/compact';
import moment from 'moment';
import type {DateTimeObject} from 'sentry/components/charts/utils';
import {
getDiffInMinutes,
SIX_HOURS,
SIXTY_DAYS,
THIRTY_DAYS,
TWENTY_FOUR_HOURS,
} from 'sentry/components/charts/utils';
import type {SessionApiResponse, SessionFieldWithOperation} from 'sentry/types';
import {SessionStatus} from 'sentry/types';
import type {SeriesDataUnit} from 'sentry/types/echarts';
import {defined, percent} from 'sentry/utils';
import {getCrashFreePercent, getSessionStatusPercent} from 'sentry/views/releases/utils';
import {sessionTerm} from 'sentry/views/releases/utils/sessionTerm';
/**
* If the time window is less than or equal 10, seconds will be displayed on the graphs
*/
export const MINUTES_THRESHOLD_TO_DISPLAY_SECONDS = 10;
export function getCount(
groups: SessionApiResponse['groups'] = [],
field: SessionFieldWithOperation
) {
return groups.reduce((acc, group) => acc + group.totals[field], 0);
}
export function getCountAtIndex(
groups: SessionApiResponse['groups'] = [],
field: SessionFieldWithOperation,
index: number
) {
return groups.reduce((acc, group) => acc + group.series[field][index], 0);
}
export function getCrashFreeRate(
groups: SessionApiResponse['groups'] = [],
field: SessionFieldWithOperation
) {
const crashedRate = getSessionStatusRate(groups, field, SessionStatus.CRASHED);
return defined(crashedRate) ? getCrashFreePercent(100 - crashedRate) : null;
}
export function getSeriesAverage(
groups: SessionApiResponse['groups'] = [],
field: SessionFieldWithOperation
) {
const totalCount = getCount(groups, field);
const dataPoints = groups.filter(group => !!group.totals[field]).length;
return !defined(totalCount) || dataPoints === null || totalCount === 0
? null
: totalCount / dataPoints;
}
export function getSeriesSum(
groups: SessionApiResponse['groups'] = [],
field: SessionFieldWithOperation,
intervals: SessionApiResponse['intervals'] = []
) {
const dataPointsSums: number[] = Array(intervals.length).fill(0);
const groupSeries = groups.map(group => group.series[field]);
groupSeries.forEach(series => {
series.forEach((dataPoint, idx) => (dataPointsSums[idx] += dataPoint));
});
return dataPointsSums;
}
export function getSessionStatusRate(
groups: SessionApiResponse['groups'] = [],
field: SessionFieldWithOperation,
status: SessionStatus
) {
const totalCount = getCount(groups, field);
const crashedCount = getCount(
groups.filter(({by}) => by['session.status'] === status),
field
);
return !defined(totalCount) || totalCount === 0
? null
: percent(crashedCount ?? 0, totalCount ?? 0);
}
export function getCrashFreeRateSeries(
groups: SessionApiResponse['groups'] = [],
intervals: SessionApiResponse['intervals'] = [],
field: SessionFieldWithOperation
): SeriesDataUnit[] {
return compact(
intervals.map((interval, i) => {
const intervalTotalSessions = groups.reduce(
(acc, group) => acc + (group.series[field]?.[i] ?? 0),
0
);
const intervalCrashedSessions =
groups.find(group => group.by['session.status'] === SessionStatus.CRASHED)
?.series[field]?.[i] ?? 0;
const crashedSessionsPercent = percent(
intervalCrashedSessions,
intervalTotalSessions
);
if (intervalTotalSessions === 0) {
return null;
}
return {
name: interval,
value: getCrashFreePercent(100 - crashedSessionsPercent),
};
})
);
}
export function getSessionStatusRateSeries(
groups: SessionApiResponse['groups'] = [],
intervals: SessionApiResponse['intervals'] = [],
field: SessionFieldWithOperation,
status: SessionStatus
): SeriesDataUnit[] {
return compact(
intervals.map((interval, i) => {
const intervalTotalSessions = groups.reduce(
(acc, group) => acc + group.series[field][i],
0
);
const intervalStatusSessions =
groups.find(group => group.by['session.status'] === status)?.series[field][i] ??
0;
const statusSessionsPercent = percent(
intervalStatusSessions,
intervalTotalSessions
);
if (intervalTotalSessions === 0) {
return null;
}
return {
name: interval,
value: getSessionStatusPercent(statusSessionsPercent),
};
})
);
}
export function getAdoptionSeries(
releaseGroups: SessionApiResponse['groups'] = [],
allGroups: SessionApiResponse['groups'] = [],
intervals: SessionApiResponse['intervals'] = [],
field: SessionFieldWithOperation
): SeriesDataUnit[] {
return intervals.map((interval, i) => {
const intervalReleaseSessions = releaseGroups.reduce(
(acc, group) => acc + (group.series[field]?.[i] ?? 0),
0
);
const intervalTotalSessions = allGroups.reduce(
(acc, group) => acc + (group.series[field]?.[i] ?? 0),
0
);
const intervalAdoption = percent(intervalReleaseSessions, intervalTotalSessions);
return {
name: interval,
value: Math.round(intervalAdoption),
};
});
}
export function getCountSeries(
field: SessionFieldWithOperation,
group?: SessionApiResponse['groups'][0],
intervals: SessionApiResponse['intervals'] = []
): SeriesDataUnit[] {
return intervals.map((interval, index) => ({
name: interval,
value: group?.series[field][index] ?? 0,
}));
}
export function initSessionsChart(theme: Theme) {
const colors = theme.charts.getColorPalette(14);
return {
[SessionStatus.HEALTHY]: {
seriesName: sessionTerm.healthy,
data: [],
color: theme.green300,
areaStyle: {
color: theme.green300,
opacity: 1,
},
lineStyle: {
opacity: 0,
width: 0.4,
},
},
[SessionStatus.ERRORED]: {
seriesName: sessionTerm.errored,
data: [],
color: colors[12],
areaStyle: {
color: colors[12],
opacity: 1,
},
lineStyle: {
opacity: 0,
width: 0.4,
},
},
[SessionStatus.ABNORMAL]: {
seriesName: sessionTerm.abnormal,
data: [],
color: colors[15],
areaStyle: {
color: colors[15],
opacity: 1,
},
lineStyle: {
opacity: 0,
width: 0.4,
},
},
[SessionStatus.CRASHED]: {
seriesName: sessionTerm.crashed,
data: [],
color: theme.red300,
areaStyle: {
color: theme.red300,
opacity: 1,
},
lineStyle: {
opacity: 0,
width: 0.4,
},
},
};
}
type GetSessionsIntervalOptions = {
dailyInterval?: boolean;
highFidelity?: boolean;
};
export function getSessionsInterval(
datetimeObj: DateTimeObject,
{highFidelity, dailyInterval}: GetSessionsIntervalOptions = {}
) {
const diffInMinutes = getDiffInMinutes(datetimeObj);
if (moment(datetimeObj.start).isSameOrBefore(moment().subtract(30, 'days'))) {
// we cannot use sub-hour session resolution on buckets older than 30 days
highFidelity = false;
}
if (dailyInterval === true && diffInMinutes > TWENTY_FOUR_HOURS) {
return '1d';
}
if (diffInMinutes >= SIXTY_DAYS) {
return '1d';
}
if (diffInMinutes >= THIRTY_DAYS) {
return '4h';
}
if (diffInMinutes >= SIX_HOURS) {
return '1h';
}
// limit on backend for sub-hour session resolution is set to six hours
if (highFidelity) {
if (diffInMinutes <= MINUTES_THRESHOLD_TO_DISPLAY_SECONDS) {
// This only works for metrics-based session stats.
// Backend will silently replace with '1m' for session-based stats.
return '10s';
}
if (diffInMinutes <= 30) {
return '1m';
}
return '5m';
}
return '1h';
}
// Sessions API can only round intervals to the closest hour - this is especially problematic when using sub-hour resolution.
// We filter out results that are out of bounds on frontend and recalculate totals.
export function filterSessionsInTimeWindow(
sessions: SessionApiResponse,
start?: string,
end?: string
) {
if (!start || !end) {
return sessions;
}
const filteredIndexes: number[] = [];
const intervals = sessions.intervals.filter((interval, index) => {
const isBetween = moment
.utc(interval)
.isBetween(moment.utc(start), moment.utc(end), undefined, '[]');
if (isBetween) {
filteredIndexes.push(index);
}
return isBetween;
});
const groups = sessions.groups.map(group => {
const series: Record<string, number[]> = {};
const totals: Record<string, number> = {};
Object.keys(group.series).forEach(field => {
totals[field] = 0;
series[field] = group.series[field].filter((value, index) => {
const isBetween = filteredIndexes.includes(index);
if (isBetween) {
totals[field] = (totals[field] ?? 0) + value;
}
return isBetween;
});
if (field.startsWith('p50')) {
// Calculate the mean of the current field.
const base = series[field] ?? [];
totals[field] = base.reduce((acc, curr) => acc + curr, 0) / base.length;
}
if (field.startsWith('count_unique')) {
// E.g. users
// We cannot sum here because users would not be unique anymore.
// User can be repeated and part of multiple buckets in series but it's still that one user so totals would be wrong.
// This operation is not 100% correct, because we are filtering series in time window but the total is for unfiltered series (it's the closest thing we can do right now)
totals[field] = group.totals[field];
}
});
return {...group, series, totals};
});
return {
start: intervals[0],
end: intervals[intervals.length - 1],
query: sessions.query,
intervals,
groups,
};
}