spanSamplesPanelContainer.tsx 8.1 KB

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