123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- import compact from 'lodash/compact';
- import mean from 'lodash/mean';
- import moment from 'moment';
- import {
- DateTimeObject,
- getDiffInMinutes,
- SIX_HOURS,
- SIXTY_DAYS,
- THIRTY_DAYS,
- } from 'sentry/components/charts/utils';
- import {SessionApiResponse, SessionFieldWithOperation, SessionStatus} from 'sentry/types';
- import {SeriesDataUnit} from 'sentry/types/echarts';
- import {defined, percent} from 'sentry/utils';
- import {Theme} from 'sentry/utils/theme';
- 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 getSessionP50Series(
- groups: SessionApiResponse['groups'] = [],
- intervals: SessionApiResponse['intervals'] = [],
- field: SessionFieldWithOperation,
- valueFormatter?: (value: number) => number
- ): SeriesDataUnit[] {
- return compact(
- intervals.map((interval, i) => {
- const meanValue = mean(
- groups.map(group => group.series[field][i]).filter(v => !!v)
- );
- if (!meanValue) {
- return null;
- }
- return {
- name: interval,
- value:
- typeof valueFormatter === 'function' ? valueFormatter(meanValue) : meanValue,
- };
- })
- );
- }
- 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 = {
- highFidelity?: boolean;
- };
- export function getSessionsInterval(
- datetimeObj: DateTimeObject,
- {highFidelity}: 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 (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 = {};
- const totals = {};
- 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')) {
- totals[field] = mean(series[field]);
- }
- 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,
- };
- }
|