samplingBreakdown.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  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: any, b: any) => 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: any, item: any) => acc + item.sampledSpans, 0);
  57. const total = spansWithSampleRates.reduce(
  58. (acc: any, item: any) => acc + item.sampledSpans,
  59. 0
  60. );
  61. const getSpanRate = (spanCount: any) => (total === 0 ? 0 : spanCount / total);
  62. const otherRate = getSpanRate(otherSpanCount);
  63. return (
  64. <div {...props}>
  65. <Heading>
  66. {t('Breakdown of stored spans originating in these projects')}
  67. <SubText>{t('Total: %s', formatAbbreviatedNumber(total))}</SubText>
  68. </Heading>
  69. <Breakdown>
  70. {topItems.map((item: any, index: any) => {
  71. const itemPercent = getSpanRate(item.sampledSpans);
  72. return (
  73. <Tooltip
  74. key={item.project.id}
  75. overlayStyle={{maxWidth: 'none'}}
  76. title={
  77. <LegendItem key={item.project.id}>
  78. <ProjectBadge disableLink avatarSize={16} project={item.project} />
  79. {formatPercent(itemPercent, {addSymbol: true})}
  80. <SubText>{formatAbbreviatedNumber(item.sampledSpans)}</SubText>
  81. </LegendItem>
  82. }
  83. skipWrapper
  84. >
  85. <div
  86. style={{
  87. width: `${itemPercent * 100}%`,
  88. backgroundColor: palette[index],
  89. }}
  90. />
  91. </Tooltip>
  92. );
  93. })}
  94. {hasOthers && (
  95. <Tooltip
  96. overlayStyle={{maxWidth: 'none'}}
  97. title={
  98. <LegendItem>
  99. <OthersBadge />
  100. {formatPercent(otherRate, {addSymbol: true})}
  101. <SubText>{formatAbbreviatedNumber(total)}</SubText>
  102. </LegendItem>
  103. }
  104. skipWrapper
  105. >
  106. <div
  107. style={{
  108. width: `${otherRate * 100}%`,
  109. backgroundColor: palette[palette.length - 1],
  110. }}
  111. />
  112. </Tooltip>
  113. )}
  114. </Breakdown>
  115. <Legend>
  116. {topItems.map((item: any) => {
  117. const itemPercent = getSpanRate(item.sampledSpans);
  118. return (
  119. <LegendItem key={item.project.id}>
  120. <ProjectBadge avatarSize={16} project={item.project} />
  121. {formatPercent(itemPercent, {addSymbol: true})}
  122. </LegendItem>
  123. );
  124. })}
  125. {hasOthers && (
  126. <LegendItem>
  127. <OthersBadge />
  128. {formatPercent(otherRate, {addSymbol: true})}
  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. display: flex;
  139. justify-content: space-between;
  140. `;
  141. const Breakdown = styled('div')`
  142. display: flex;
  143. height: ${space(2)};
  144. width: 100%;
  145. border-radius: ${p => p.theme.borderRadius};
  146. overflow: hidden;
  147. `;
  148. const Legend = styled('div')`
  149. display: flex;
  150. flex-wrap: wrap;
  151. margin-top: ${space(1)};
  152. gap: ${space(1.5)};
  153. `;
  154. const LegendItem = styled('div')`
  155. display: flex;
  156. align-items: center;
  157. gap: ${space(0.75)};
  158. `;
  159. const SubText = styled('span')`
  160. color: ${p => p.theme.gray300};
  161. white-space: nowrap;
  162. `;