usageStatsPerMin.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import styled from '@emotion/styled';
  2. import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
  3. import {t} from 'sentry/locale';
  4. import {DataCategoryInfo, Organization, Outcome} from 'sentry/types';
  5. import {UsageSeries} from './types';
  6. import {formatUsageWithUnits, getFormatUsageOptions} from './utils';
  7. type Props = {
  8. dataCategory: DataCategoryInfo['plural'];
  9. organization: Organization;
  10. projectIds: number[];
  11. } & DeprecatedAsyncComponent['props'];
  12. type State = {
  13. orgStats: UsageSeries | undefined;
  14. } & DeprecatedAsyncComponent['state'];
  15. /**
  16. * Making 1 extra API call to display this number isn't very efficient.
  17. * The other approach would be to fetch the data in UsageStatsOrg with 1min
  18. * interval and roll it up on the frontend, but that (1) adds unnecessary
  19. * complexity as it's gnarly to fetch + rollup 90 days of 1min intervals,
  20. * (3) API resultset has a limit of 1000, so 90 days of 1min would not work.
  21. *
  22. * We're going with this approach for simplicity sake. By keeping the range
  23. * as small as possible, this call is quite fast.
  24. */
  25. class UsageStatsPerMin extends DeprecatedAsyncComponent<Props, State> {
  26. componentDidUpdate(prevProps: Props) {
  27. const {projectIds} = this.props;
  28. if (prevProps.projectIds !== projectIds) {
  29. this.reloadData();
  30. }
  31. }
  32. getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
  33. return [['orgStats', this.endpointPath, {query: this.endpointQuery}]];
  34. }
  35. get endpointPath() {
  36. const {organization} = this.props;
  37. return `/organizations/${organization.slug}/stats_v2/`;
  38. }
  39. get endpointQuery() {
  40. const {projectIds} = this.props;
  41. return {
  42. statsPeriod: '5m', // Any value <1h will return current hour's data
  43. interval: '1m',
  44. groupBy: ['category', 'outcome'],
  45. project: projectIds,
  46. field: ['sum(quantity)'],
  47. };
  48. }
  49. get minuteData(): string | undefined {
  50. const {dataCategory} = this.props;
  51. const {loading, error, orgStats} = this.state;
  52. if (loading || error || !orgStats || orgStats.intervals.length === 0) {
  53. return undefined;
  54. }
  55. // The last minute in the series is still "in progress"
  56. // Read data from 2nd last element for the latest complete minute
  57. const {intervals, groups} = orgStats;
  58. const lastMin = Math.max(intervals.length - 2, 0);
  59. const eventsLastMin = groups.reduce((count, group) => {
  60. const {outcome, category} = group.by;
  61. // HACK: The backend enum are singular, but the frontend enums are plural
  62. if (!dataCategory.includes(`${category}`) || outcome !== Outcome.ACCEPTED) {
  63. return count;
  64. }
  65. count += group.series['sum(quantity)'][lastMin];
  66. return count;
  67. }, 0);
  68. return formatUsageWithUnits(
  69. eventsLastMin,
  70. dataCategory,
  71. getFormatUsageOptions(dataCategory)
  72. );
  73. }
  74. renderComponent() {
  75. if (!this.minuteData) {
  76. return null;
  77. }
  78. return (
  79. <Wrapper>
  80. {this.minuteData} {t('in last min')}
  81. </Wrapper>
  82. );
  83. }
  84. }
  85. export default UsageStatsPerMin;
  86. const Wrapper = styled('div')`
  87. display: inline-block;
  88. color: ${p => p.theme.success};
  89. font-size: ${p => p.theme.fontSizeMedium};
  90. `;