spanSamplesPanelContainer.tsx 7.4 KB

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