spanSamplesPanelContainer.tsx 7.7 KB

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