samplingBreakdown.tsx 4.9 KB

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