samplesContainer.tsx 6.6 KB

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