spanSamplesPanelContainer.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import {Fragment, useCallback, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import debounce from 'lodash/debounce';
  4. import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  5. import Link from 'sentry/components/links/link';
  6. import {Tooltip} from 'sentry/components/tooltip';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import type {Project} from 'sentry/types/project';
  10. import {DurationUnit} from 'sentry/utils/discover/fields';
  11. import {decodeScalar} from 'sentry/utils/queryString';
  12. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  13. import {useLocation} from 'sentry/utils/useLocation';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. import useRouter from 'sentry/utils/useRouter';
  16. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  17. import {MetricReadout} from 'sentry/views/performance/metricReadout';
  18. import {
  19. DEFAULT_PLATFORM,
  20. PLATFORM_LOCAL_STORAGE_KEY,
  21. PLATFORM_QUERY_PARAM,
  22. } from 'sentry/views/performance/mobile/screenload/screens/platformSelector';
  23. import {isCrossPlatform} from 'sentry/views/performance/mobile/screenload/screens/utils';
  24. import {useSpanMetrics} from 'sentry/views/starfish/queries/useDiscover';
  25. import {
  26. type ModuleName,
  27. SpanMetricsField,
  28. type SpanMetricsQueryFilters,
  29. } from 'sentry/views/starfish/types';
  30. import {formatVersionAndCenterTruncate} from 'sentry/views/starfish/utils/centerTruncate';
  31. import {DataTitles} from 'sentry/views/starfish/views/spans/types';
  32. import DurationChart from 'sentry/views/starfish/views/spanSummaryPage/sampleList/durationChart';
  33. import SampleTable from 'sentry/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable';
  34. const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsField;
  35. type Props = {
  36. groupId: string;
  37. moduleName: ModuleName;
  38. transactionName: string;
  39. additionalFilters?: Record<string, string>;
  40. project?: Project | null;
  41. release?: string;
  42. sectionSubtitle?: string;
  43. sectionTitle?: string;
  44. spanOp?: string;
  45. transactionMethod?: string;
  46. };
  47. export function SpanSamplesContainer({
  48. groupId,
  49. moduleName,
  50. transactionName,
  51. transactionMethod,
  52. release,
  53. project,
  54. spanOp,
  55. additionalFilters,
  56. }: Props) {
  57. const router = useRouter();
  58. const location = useLocation();
  59. const [highlightedSpanId, setHighlightedSpanId] = useState<string | undefined>(
  60. undefined
  61. );
  62. const organization = useOrganization();
  63. const hasPlatformSelectFeature = organization.features.includes('spans-first-ui');
  64. const platform =
  65. decodeScalar(location.query[PLATFORM_QUERY_PARAM]) ??
  66. localStorage.getItem(PLATFORM_LOCAL_STORAGE_KEY) ??
  67. DEFAULT_PLATFORM;
  68. // eslint-disable-next-line react-hooks/exhaustive-deps
  69. const debounceSetHighlightedSpanId = useCallback(
  70. debounce(id => {
  71. setHighlightedSpanId(id);
  72. }, 10),
  73. []
  74. );
  75. const filters: SpanMetricsQueryFilters = {
  76. 'span.group': groupId,
  77. transaction: transactionName,
  78. };
  79. if (transactionMethod) {
  80. filters['transaction.method'] = transactionMethod;
  81. }
  82. if (release) {
  83. filters.release = release;
  84. }
  85. if (project && isCrossPlatform(project) && hasPlatformSelectFeature) {
  86. filters['os.name'] = platform;
  87. }
  88. if (spanOp) {
  89. filters['span.op'] = spanOp;
  90. }
  91. const {data} = useSpanMetrics(
  92. {
  93. search: MutableSearch.fromQueryObject({...filters, ...additionalFilters}),
  94. fields: [`avg(${SPAN_SELF_TIME})`, 'count()', SPAN_OP],
  95. enabled: Boolean(groupId) && Boolean(transactionName),
  96. },
  97. 'api.starfish.span-summary-panel-samples-table-avg'
  98. );
  99. const spanMetrics = data[0] ?? {};
  100. return (
  101. <Fragment>
  102. <PaddedTitle>
  103. {release && (
  104. <SectionTitle>
  105. <Tooltip title={release}>
  106. <Link
  107. to={{
  108. pathname: normalizeUrl(
  109. `/organizations/${organization?.slug}/releases/${encodeURIComponent(
  110. release
  111. )}/`
  112. ),
  113. }}
  114. >
  115. {formatVersionAndCenterTruncate(release)}
  116. </Link>
  117. </Tooltip>
  118. </SectionTitle>
  119. )}
  120. </PaddedTitle>
  121. <Container>
  122. <MetricReadout
  123. title={DataTitles.avg}
  124. align="left"
  125. value={spanMetrics?.[`avg(${SPAN_SELF_TIME})`]}
  126. unit={DurationUnit.MILLISECOND}
  127. />
  128. <MetricReadout
  129. title={DataTitles.count}
  130. align="left"
  131. value={spanMetrics?.['count()'] ?? 0}
  132. unit="count"
  133. />
  134. </Container>
  135. <DurationChart
  136. query={
  137. additionalFilters
  138. ? Object.entries(additionalFilters).map(([key, value]) => `${key}:${value}`)
  139. : undefined
  140. }
  141. additionalFilters={additionalFilters}
  142. groupId={groupId}
  143. transactionName={transactionName}
  144. transactionMethod={transactionMethod}
  145. onClickSample={span => {
  146. router.push(
  147. `/performance/${span.project}:${span['transaction.id']}/#span-${span.span_id}`
  148. );
  149. }}
  150. onMouseOverSample={sample => debounceSetHighlightedSpanId(sample.span_id)}
  151. onMouseLeaveSample={() => debounceSetHighlightedSpanId(undefined)}
  152. highlightedSpanId={highlightedSpanId}
  153. release={release}
  154. platform={
  155. project && isCrossPlatform(project) && hasPlatformSelectFeature
  156. ? platform
  157. : undefined
  158. }
  159. />
  160. <SampleTable
  161. query={
  162. additionalFilters
  163. ? Object.entries(additionalFilters).map(([key, value]) => `${key}:${value}`)
  164. : undefined
  165. }
  166. additionalFilters={additionalFilters}
  167. highlightedSpanId={highlightedSpanId}
  168. transactionMethod={transactionMethod}
  169. onMouseLeaveSample={() => setHighlightedSpanId(undefined)}
  170. onMouseOverSample={sample => setHighlightedSpanId(sample.span_id)}
  171. groupId={groupId}
  172. transactionName={transactionName}
  173. moduleName={moduleName}
  174. release={release}
  175. columnOrder={[
  176. {
  177. key: 'span_id',
  178. name: t('Span ID'),
  179. width: COL_WIDTH_UNDEFINED,
  180. },
  181. {
  182. key: 'profile_id',
  183. name: t('Profile'),
  184. width: COL_WIDTH_UNDEFINED,
  185. },
  186. {
  187. key: 'avg_comparison',
  188. name: t('Compared to Average'),
  189. width: COL_WIDTH_UNDEFINED,
  190. },
  191. ]}
  192. />
  193. </Fragment>
  194. );
  195. }
  196. const SectionTitle = styled('div')`
  197. ${p => p.theme.text.cardTitle}
  198. `;
  199. const PaddedTitle = styled('div')`
  200. margin-bottom: ${space(1)};
  201. `;
  202. const Container = styled('div')`
  203. display: flex;
  204. `;