usageStatsPerMin.tsx 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import styled from '@emotion/styled';
  2. import {t} from 'sentry/locale';
  3. import type {DataCategoryInfo} from 'sentry/types/core';
  4. import {Outcome} from 'sentry/types/core';
  5. import type {Organization} from 'sentry/types/organization';
  6. import {useApiQuery} from 'sentry/utils/queryClient';
  7. import type {UsageSeries} from './types';
  8. import {formatUsageWithUnits, getFormatUsageOptions} from './utils';
  9. type Props = {
  10. dataCategory: DataCategoryInfo['plural'];
  11. dataCategoryApiName: DataCategoryInfo['apiName'];
  12. organization: Organization;
  13. projectIds: number[];
  14. };
  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. function UsageStatsPerMin({
  26. organization,
  27. projectIds,
  28. dataCategory,
  29. dataCategoryApiName,
  30. }: Props) {
  31. const {
  32. data: orgStats,
  33. isPending,
  34. isError,
  35. } = useApiQuery<UsageSeries>(
  36. [
  37. `/organizations/${organization.slug}/stats_v2/`,
  38. {
  39. query: {
  40. statsPeriod: '5m', // Any value <1h will return current hour's data
  41. interval: '1m',
  42. groupBy: ['category', 'outcome'],
  43. project: projectIds,
  44. field: ['sum(quantity)'],
  45. },
  46. },
  47. ],
  48. {
  49. staleTime: 0,
  50. }
  51. );
  52. if (isPending || isError || !orgStats || orgStats.intervals.length === 0) {
  53. return null;
  54. }
  55. const minuteData = (): string | undefined => {
  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 {category, outcome} = group.by;
  62. if (dataCategoryApiName === 'span_indexed') {
  63. if (category !== 'span_indexed' || outcome !== Outcome.ACCEPTED) {
  64. return count;
  65. }
  66. } else {
  67. // HACK: The backend enum are singular, but the frontend enums are plural
  68. if (!dataCategory.includes(`${category}`) || outcome !== Outcome.ACCEPTED) {
  69. return count;
  70. }
  71. }
  72. count += group.series['sum(quantity)'][lastMin];
  73. return count;
  74. }, 0);
  75. return formatUsageWithUnits(
  76. eventsLastMin,
  77. dataCategory,
  78. getFormatUsageOptions(dataCategory)
  79. );
  80. };
  81. return (
  82. <Wrapper>
  83. {minuteData()} {t('in last min')}
  84. </Wrapper>
  85. );
  86. }
  87. export default UsageStatsPerMin;
  88. const Wrapper = styled('div')`
  89. display: inline-block;
  90. color: ${p => p.theme.success};
  91. font-size: ${p => p.theme.fontSizeMedium};
  92. `;