usageStatsPerMin.tsx 3.2 KB

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