spanSamplesPanelContainer.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import {Fragment, useCallback, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import debounce from 'lodash/debounce';
  4. import SearchBar from 'sentry/components/events/searchBar';
  5. import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  6. import Link from 'sentry/components/links/link';
  7. import {SpanSearchQueryBuilder} from 'sentry/components/performance/spanSearchQueryBuilder';
  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 {useNavigate} from 'sentry/utils/useNavigate';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. import usePageFilters from 'sentry/utils/usePageFilters';
  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 navigate = useNavigate();
  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 {data: 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, isPending} = 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. navigate(
  112. {
  113. pathname: location.pathname,
  114. query: {
  115. ...location.query,
  116. ...(searchQueryKey && {[searchQueryKey]: newSearchQuery}),
  117. },
  118. },
  119. {replace: true}
  120. );
  121. };
  122. return (
  123. <Fragment>
  124. <PaddedTitle>
  125. {release && (
  126. <SectionTitle>
  127. <Tooltip title={release}>
  128. <Link
  129. to={{
  130. pathname: normalizeUrl(
  131. `/organizations/${organization?.slug}/releases/${encodeURIComponent(
  132. release
  133. )}/`
  134. ),
  135. }}
  136. >
  137. {formatVersionAndCenterTruncate(release)}
  138. </Link>
  139. </Tooltip>
  140. </SectionTitle>
  141. )}
  142. </PaddedTitle>
  143. <StyledReadoutRibbon>
  144. <MetricReadout
  145. title={DataTitles.avg}
  146. value={spanMetrics?.[`avg(${SPAN_SELF_TIME})`]}
  147. unit={DurationUnit.MILLISECOND}
  148. isLoading={isPending}
  149. />
  150. <MetricReadout
  151. title={DataTitles.count}
  152. value={spanMetrics?.['count()'] ?? 0}
  153. unit="count"
  154. isLoading={isPending}
  155. />
  156. </StyledReadoutRibbon>
  157. <DurationChart
  158. spanSearch={spanSearch}
  159. additionalFilters={additionalFilters}
  160. groupId={groupId}
  161. transactionName={transactionName}
  162. transactionMethod={transactionMethod}
  163. onClickSample={span => {
  164. navigate(
  165. generateLinkToEventInTraceView({
  166. eventId: span['transaction.id'],
  167. projectSlug: span.project,
  168. spanId: span.span_id,
  169. location,
  170. organization,
  171. traceSlug: span.trace,
  172. timestamp: span.timestamp,
  173. source: TraceViewSources.APP_STARTS_MODULE,
  174. })
  175. );
  176. }}
  177. onMouseOverSample={sample => debounceSetHighlightedSpanId(sample.span_id)}
  178. onMouseLeaveSample={() => debounceSetHighlightedSpanId(undefined)}
  179. highlightedSpanId={highlightedSpanId}
  180. release={release}
  181. platform={isProjectCrossPlatform ? selectedPlatform : undefined}
  182. />
  183. <StyledSearchBar>
  184. {organization.features.includes('search-query-builder-performance') ? (
  185. <SpanSearchQueryBuilder
  186. searchSource={`${moduleName}-sample-panel`}
  187. initialQuery={searchQuery ?? ''}
  188. onSearch={handleSearch}
  189. placeholder={t('Search for span attributes')}
  190. projects={selection.projects}
  191. />
  192. ) : (
  193. <SearchBar
  194. searchSource={`${moduleName}-sample-panel`}
  195. query={searchQuery}
  196. onSearch={handleSearch}
  197. placeholder={t('Search for span attributes')}
  198. organization={organization}
  199. supportedTags={supportedTags}
  200. dataset={DiscoverDatasets.SPANS_INDEXED}
  201. projectIds={selection.projects}
  202. />
  203. )}
  204. </StyledSearchBar>
  205. <SampleTable
  206. referrer={TraceViewSources.APP_STARTS_MODULE}
  207. spanSearch={spanSearch}
  208. additionalFilters={additionalFilters}
  209. highlightedSpanId={highlightedSpanId}
  210. transactionMethod={transactionMethod}
  211. onMouseLeaveSample={() => setHighlightedSpanId(undefined)}
  212. onMouseOverSample={sample => setHighlightedSpanId(sample.span_id)}
  213. groupId={groupId}
  214. transactionName={transactionName}
  215. moduleName={moduleName}
  216. release={release}
  217. columnOrder={[
  218. {
  219. key: 'span_id',
  220. name: t('Span ID'),
  221. width: COL_WIDTH_UNDEFINED,
  222. },
  223. {
  224. key: 'profile_id',
  225. name: t('Profile'),
  226. width: COL_WIDTH_UNDEFINED,
  227. },
  228. {
  229. key: 'avg_comparison',
  230. name: t('Compared to Average'),
  231. width: COL_WIDTH_UNDEFINED,
  232. },
  233. ]}
  234. />
  235. </Fragment>
  236. );
  237. }
  238. const StyledReadoutRibbon = styled(ReadoutRibbon)`
  239. margin-bottom: ${space(2)};
  240. `;
  241. const SectionTitle = styled('div')`
  242. ${p => p.theme.text.cardTitle}
  243. `;
  244. const PaddedTitle = styled('div')`
  245. margin-bottom: ${space(1)};
  246. `;
  247. const StyledSearchBar = styled('div')`
  248. margin: ${space(2)} 0;
  249. `;