samplingBreakdown.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import {css} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import {PlatformIcon} from 'platformicons';
  4. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  5. import {Tooltip} from 'sentry/components/tooltip';
  6. import {CHART_PALETTE} from 'sentry/constants/chartPalette';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
  10. import {formatNumberWithDynamicDecimalPoints} from 'sentry/utils/number/formatNumberWithDynamicDecimalPoints';
  11. import type {ProjectSampleCount} from 'sentry/views/settings/dynamicSampling/utils/useProjectSampleCounts';
  12. const ITEMS_TO_SHOW = 5;
  13. const palette = CHART_PALETTE[ITEMS_TO_SHOW - 1];
  14. interface Props extends React.HTMLAttributes<HTMLDivElement> {
  15. sampleCounts: ProjectSampleCount[];
  16. sampleRates: Record<string, number>;
  17. }
  18. function OthersBadge() {
  19. return (
  20. <div
  21. css={css`
  22. display: flex;
  23. align-items: center;
  24. gap: ${space(0.75)};
  25. `}
  26. >
  27. <PlatformIcon
  28. css={css`
  29. width: 16px;
  30. height: 16px;
  31. `}
  32. platform="other"
  33. />
  34. {t('other projects')}
  35. </div>
  36. );
  37. }
  38. export function SamplingBreakdown({sampleCounts, sampleRates, ...props}: Props) {
  39. const spansWithSampleRates = sampleCounts
  40. ?.map(item => {
  41. const sampledSpans = Math.floor(item.count * (sampleRates[item.project.id] ?? 1));
  42. return {
  43. project: item.project,
  44. sampledSpans,
  45. };
  46. })
  47. .toSorted((a, b) => b.sampledSpans - a.sampledSpans);
  48. const hasOthers = spansWithSampleRates.length > ITEMS_TO_SHOW;
  49. const topItems = hasOthers
  50. ? spansWithSampleRates.slice(0, ITEMS_TO_SHOW - 1)
  51. : spansWithSampleRates.slice(0, ITEMS_TO_SHOW);
  52. const otherSpanCount = spansWithSampleRates
  53. .slice(ITEMS_TO_SHOW - 1)
  54. .reduce((acc, item) => acc + item.sampledSpans, 0);
  55. const total = spansWithSampleRates.reduce((acc, item) => acc + item.sampledSpans, 0);
  56. const getSpanPercent = spanCount => (total === 0 ? 100 : (spanCount / total) * 100);
  57. const otherPercent = getSpanPercent(otherSpanCount);
  58. return (
  59. <div {...props}>
  60. <Heading>
  61. {t('Breakdown of stored spans')}
  62. <SubText>{t('Total: %s', formatAbbreviatedNumber(total))}</SubText>
  63. </Heading>
  64. <Breakdown>
  65. {topItems.map((item, index) => {
  66. return (
  67. <Tooltip
  68. key={item.project.id}
  69. overlayStyle={{maxWidth: 'none'}}
  70. title={
  71. <LegendItem key={item.project.id}>
  72. <ProjectBadge disableLink avatarSize={16} project={item.project} />
  73. {`${formatNumberWithDynamicDecimalPoints(getSpanPercent(item.sampledSpans))}%`}
  74. <SubText>{formatAbbreviatedNumber(item.sampledSpans)}</SubText>
  75. </LegendItem>
  76. }
  77. skipWrapper
  78. >
  79. <div
  80. style={{
  81. width: `${getSpanPercent(item.sampledSpans)}%`,
  82. backgroundColor: palette[index],
  83. }}
  84. />
  85. </Tooltip>
  86. );
  87. })}
  88. {hasOthers && (
  89. <Tooltip
  90. overlayStyle={{maxWidth: 'none'}}
  91. title={
  92. <LegendItem>
  93. <OthersBadge />
  94. {`${formatNumberWithDynamicDecimalPoints(otherPercent)}%`}
  95. <SubText>{formatAbbreviatedNumber(total)}</SubText>
  96. </LegendItem>
  97. }
  98. skipWrapper
  99. >
  100. <div
  101. style={{
  102. width: `${otherPercent}%`,
  103. backgroundColor: palette[palette.length - 1],
  104. }}
  105. />
  106. </Tooltip>
  107. )}
  108. </Breakdown>
  109. <Legend>
  110. {topItems.map(item => {
  111. return (
  112. <LegendItem key={item.project.id}>
  113. <ProjectBadge avatarSize={16} project={item.project} />
  114. {`${formatNumberWithDynamicDecimalPoints(getSpanPercent(item.sampledSpans))}%`}
  115. </LegendItem>
  116. );
  117. })}
  118. {hasOthers && (
  119. <LegendItem>
  120. <OthersBadge />
  121. {`${formatNumberWithDynamicDecimalPoints(otherPercent)}%`}
  122. </LegendItem>
  123. )}
  124. </Legend>
  125. </div>
  126. );
  127. }
  128. const Heading = styled('h6')`
  129. margin-bottom: ${space(1)};
  130. font-size: ${p => p.theme.fontSizeMedium};
  131. display: flex;
  132. justify-content: space-between;
  133. `;
  134. const Breakdown = styled('div')`
  135. display: flex;
  136. height: ${space(2)};
  137. width: 100%;
  138. border-radius: ${p => p.theme.borderRadius};
  139. overflow: hidden;
  140. `;
  141. const Legend = styled('div')`
  142. display: flex;
  143. flex-wrap: wrap;
  144. margin-top: ${space(1)};
  145. gap: ${space(1.5)};
  146. `;
  147. const LegendItem = styled('div')`
  148. display: flex;
  149. align-items: center;
  150. gap: ${space(0.75)};
  151. `;
  152. const SubText = styled('span')`
  153. color: ${p => p.theme.gray300};
  154. white-space: nowrap;
  155. `;