samplingBreakdown.tsx 5.0 KB

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