mapSeriesToChart.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. import * as Sentry from '@sentry/react';
  2. import startCase from 'lodash/startCase';
  3. import moment from 'moment-timezone';
  4. import type {TooltipSubLabel} from 'sentry/components/charts/components/tooltip';
  5. import type {DataCategoryInfo, IntervalPeriod} from 'sentry/types/core';
  6. import {Outcome} from 'sentry/types/core';
  7. import {getDateFromMoment} from './usageChart/utils';
  8. import {getReasonGroupName} from './getReasonGroupName';
  9. import type {UsageSeries, UsageStat} from './types';
  10. import type {ChartStats} from './usageChart';
  11. import {SeriesTypes} from './usageChart';
  12. import {formatUsageWithUnits, getFormatUsageOptions} from './utils';
  13. export function mapSeriesToChart({
  14. orgStats,
  15. dataCategory,
  16. chartDateUtc,
  17. endpointQuery,
  18. chartDateInterval,
  19. }: {
  20. chartDateInterval: IntervalPeriod;
  21. chartDateUtc: boolean;
  22. dataCategory: DataCategoryInfo['plural'];
  23. endpointQuery: Record<string, unknown>;
  24. orgStats?: UsageSeries;
  25. }): {
  26. cardStats: {
  27. accepted?: string;
  28. accepted_stored?: string;
  29. filtered?: string;
  30. invalid?: string;
  31. rateLimited?: string;
  32. total?: string;
  33. };
  34. chartStats: ChartStats;
  35. chartSubLabels: TooltipSubLabel[];
  36. dataError?: Error;
  37. } {
  38. const cardStats = {
  39. total: undefined,
  40. accepted: undefined,
  41. accepted_stored: undefined,
  42. filtered: undefined,
  43. invalid: undefined,
  44. rateLimited: undefined,
  45. };
  46. const chartStats: ChartStats = {
  47. accepted: [],
  48. accepted_stored: [],
  49. filtered: [],
  50. rateLimited: [],
  51. invalid: [],
  52. clientDiscard: [],
  53. projected: [],
  54. };
  55. let chartSubLabels: TooltipSubLabel[] = [];
  56. if (!orgStats) {
  57. return {cardStats, chartStats, chartSubLabels};
  58. }
  59. try {
  60. const usageStats: UsageStat[] = orgStats.intervals.map(interval => {
  61. const dateTime = moment(interval);
  62. return {
  63. date: getDateFromMoment(dateTime, chartDateInterval, chartDateUtc),
  64. total: 0,
  65. accepted: 0,
  66. accepted_stored: 0,
  67. filtered: 0,
  68. rateLimited: 0,
  69. invalid: 0,
  70. clientDiscard: 0,
  71. };
  72. });
  73. // Tally totals for card data
  74. const count = {
  75. total: 0,
  76. [Outcome.ACCEPTED]: 0,
  77. [Outcome.FILTERED]: 0,
  78. [Outcome.INVALID]: 0,
  79. [Outcome.RATE_LIMITED]: 0, // Combined with dropped later
  80. [Outcome.CLIENT_DISCARD]: 0,
  81. [Outcome.CARDINALITY_LIMITED]: 0, // Combined with dropped later
  82. [Outcome.ABUSE]: 0, // Combined with dropped later
  83. };
  84. let countAcceptedStored = 0;
  85. orgStats.groups.forEach(group => {
  86. const {outcome, category} = group.by;
  87. // For spans, we additionally query for `span_indexed` data
  88. // to get the `accepted_stored` count
  89. if (category !== 'span_indexed') {
  90. if (outcome !== Outcome.CLIENT_DISCARD) {
  91. count.total += group.totals['sum(quantity)'];
  92. }
  93. count[outcome] += group.totals['sum(quantity)'];
  94. } else {
  95. if (outcome === Outcome.ACCEPTED) {
  96. countAcceptedStored += group.totals['sum(quantity)'];
  97. }
  98. }
  99. if (category === 'span_indexed' && outcome !== Outcome.ACCEPTED) {
  100. // we need `span_indexed` data for `accepted_stored` only
  101. return;
  102. }
  103. group.series['sum(quantity)'].forEach((stat, i) => {
  104. const dataObject = {name: orgStats.intervals[i], value: stat};
  105. const strigfiedReason = String(group.by.reason ?? '');
  106. const reason = getReasonGroupName(outcome, strigfiedReason);
  107. // Function to handle chart sub-label updates
  108. const updateChartSubLabels = (
  109. parentLabel: SeriesTypes,
  110. label = startCase(reason.replace(/-|_/g, ' '))
  111. ) => {
  112. const existingSubLabel = chartSubLabels.find(
  113. subLabel => subLabel.label === label && subLabel.parentLabel === parentLabel
  114. );
  115. if (existingSubLabel) {
  116. // Check if the existing sub-label's data length matches the intervals length
  117. if (existingSubLabel.data.length === group.series['sum(quantity)'].length) {
  118. // Update the value of the current interval
  119. existingSubLabel.data[i].value += stat;
  120. } else {
  121. // Add a new data object if the length does not match
  122. existingSubLabel.data.push(dataObject);
  123. }
  124. } else {
  125. chartSubLabels.push({
  126. parentLabel,
  127. label,
  128. data: [dataObject],
  129. });
  130. }
  131. };
  132. // Add accepted indexed spans as sub-label to accepted
  133. if (category === 'span_indexed') {
  134. if (outcome === Outcome.ACCEPTED) {
  135. usageStats[i].accepted_stored += stat;
  136. updateChartSubLabels(SeriesTypes.ACCEPTED, 'Stored');
  137. return;
  138. }
  139. }
  140. switch (outcome) {
  141. case Outcome.FILTERED:
  142. usageStats[i].filtered += stat;
  143. updateChartSubLabels(SeriesTypes.FILTERED);
  144. break;
  145. case Outcome.ACCEPTED:
  146. usageStats[i].accepted += stat;
  147. break;
  148. case Outcome.CARDINALITY_LIMITED:
  149. case Outcome.RATE_LIMITED:
  150. case Outcome.ABUSE:
  151. usageStats[i].rateLimited += stat;
  152. updateChartSubLabels(SeriesTypes.RATE_LIMITED);
  153. break;
  154. case Outcome.CLIENT_DISCARD:
  155. usageStats[i].clientDiscard += stat;
  156. updateChartSubLabels(SeriesTypes.CLIENT_DISCARD);
  157. break;
  158. case Outcome.INVALID:
  159. usageStats[i].invalid += stat;
  160. updateChartSubLabels(SeriesTypes.INVALID);
  161. break;
  162. default:
  163. break;
  164. }
  165. });
  166. });
  167. // Combine rate limited counts
  168. count[Outcome.RATE_LIMITED] +=
  169. count[Outcome.ABUSE] + count[Outcome.CARDINALITY_LIMITED];
  170. const isSampled =
  171. dataCategory === 'spans' &&
  172. countAcceptedStored > 0 &&
  173. countAcceptedStored !== count[Outcome.ACCEPTED];
  174. usageStats.forEach(stat => {
  175. stat.total = [
  176. stat.accepted,
  177. stat.filtered,
  178. stat.rateLimited,
  179. stat.invalid,
  180. stat.clientDiscard,
  181. ].reduce((acc, val) => acc + val, 0);
  182. // Chart Data
  183. const chartData = [
  184. {
  185. key: 'accepted',
  186. value: stat.accepted,
  187. },
  188. ...(isSampled ? [{key: 'accepted_stored', value: stat.accepted_stored}] : []),
  189. {key: 'filtered', value: stat.filtered},
  190. {key: 'rateLimited', value: stat.rateLimited},
  191. {key: 'invalid', value: stat.invalid},
  192. {key: 'clientDiscard', value: stat.clientDiscard},
  193. ];
  194. chartData.forEach(data => {
  195. (chartStats[data.key] as any[]).push({value: [stat.date, data.value]});
  196. });
  197. });
  198. if (!isSampled) {
  199. chartSubLabels = chartSubLabels.filter(
  200. subLabel => subLabel.parentLabel !== SeriesTypes.ACCEPTED
  201. );
  202. }
  203. return {
  204. cardStats: {
  205. total: formatUsageWithUnits(
  206. count.total,
  207. dataCategory,
  208. getFormatUsageOptions(dataCategory)
  209. ),
  210. accepted: formatUsageWithUnits(
  211. count[Outcome.ACCEPTED],
  212. dataCategory,
  213. getFormatUsageOptions(dataCategory)
  214. ),
  215. accepted_stored: isSampled
  216. ? formatUsageWithUnits(
  217. countAcceptedStored,
  218. dataCategory,
  219. getFormatUsageOptions(dataCategory)
  220. )
  221. : undefined,
  222. filtered: formatUsageWithUnits(
  223. count[Outcome.FILTERED],
  224. dataCategory,
  225. getFormatUsageOptions(dataCategory)
  226. ),
  227. invalid: formatUsageWithUnits(
  228. count[Outcome.INVALID],
  229. dataCategory,
  230. getFormatUsageOptions(dataCategory)
  231. ),
  232. rateLimited: formatUsageWithUnits(
  233. count[Outcome.RATE_LIMITED],
  234. dataCategory,
  235. getFormatUsageOptions(dataCategory)
  236. ),
  237. },
  238. chartStats,
  239. chartSubLabels,
  240. };
  241. } catch (err) {
  242. Sentry.withScope(scope => {
  243. scope.setContext('query', endpointQuery);
  244. scope.setContext('body', {...orgStats});
  245. Sentry.captureException(err);
  246. });
  247. return {
  248. cardStats,
  249. chartStats,
  250. chartSubLabels,
  251. dataError: new Error('Failed to parse stats data'),
  252. };
  253. }
  254. }