spanSamplesPanelContainer.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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 {SpanSearchQueryBuilder} from 'sentry/components/performance/spanSearchQueryBuilder';
  7. import {Tooltip} from 'sentry/components/tooltip';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {DurationUnit} from 'sentry/utils/discover/fields';
  11. import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
  12. import {decodeScalar} from 'sentry/utils/queryString';
  13. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  14. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  15. import {useLocation} from 'sentry/utils/useLocation';
  16. import {useNavigate} from 'sentry/utils/useNavigate';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import usePageFilters from 'sentry/utils/usePageFilters';
  19. import {MetricReadout} from 'sentry/views/insights/common/components/metricReadout';
  20. import {ReadoutRibbon} from 'sentry/views/insights/common/components/ribbon';
  21. import {useSpanMetrics} from 'sentry/views/insights/common/queries/useDiscover';
  22. import {formatVersionAndCenterTruncate} from 'sentry/views/insights/common/utils/centerTruncate';
  23. import {DataTitles} from 'sentry/views/insights/common/views/spans/types';
  24. import DurationChart from 'sentry/views/insights/common/views/spanSummaryPage/sampleList/durationChart';
  25. import SampleTable from 'sentry/views/insights/common/views/spanSummaryPage/sampleList/sampleTable/sampleTable';
  26. import useCrossPlatformProject from 'sentry/views/insights/mobile/common/queries/useCrossPlatformProject';
  27. import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters';
  28. import {
  29. type ModuleName,
  30. SpanMetricsField,
  31. type SpanMetricsQueryFilters,
  32. } from 'sentry/views/insights/types';
  33. import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs';
  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 navigate = useNavigate();
  59. const {view} = useDomainViewFilters();
  60. const [highlightedSpanId, setHighlightedSpanId] = useState<string | undefined>(
  61. undefined
  62. );
  63. const {selectedPlatform, isProjectCrossPlatform} = useCrossPlatformProject();
  64. const organization = useOrganization();
  65. const {selection} = usePageFilters();
  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, isPending} = 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. navigate(
  110. {
  111. pathname: location.pathname,
  112. query: {
  113. ...location.query,
  114. ...(searchQueryKey && {[searchQueryKey]: newSearchQuery}),
  115. },
  116. },
  117. {replace: true}
  118. );
  119. };
  120. return (
  121. <Fragment>
  122. <PaddedTitle>
  123. {release && (
  124. <SectionTitle>
  125. <Tooltip title={release}>
  126. <Link
  127. to={{
  128. pathname: normalizeUrl(
  129. `/organizations/${organization?.slug}/releases/${encodeURIComponent(
  130. release
  131. )}/`
  132. ),
  133. }}
  134. >
  135. {formatVersionAndCenterTruncate(release)}
  136. </Link>
  137. </Tooltip>
  138. </SectionTitle>
  139. )}
  140. </PaddedTitle>
  141. <StyledReadoutRibbon>
  142. <MetricReadout
  143. title={DataTitles.avg}
  144. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  145. value={spanMetrics?.[`avg(${SPAN_SELF_TIME})`]}
  146. unit={DurationUnit.MILLISECOND}
  147. isLoading={isPending}
  148. />
  149. <MetricReadout
  150. title={DataTitles.count}
  151. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  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. view,
  174. source: TraceViewSources.APP_STARTS_MODULE,
  175. })
  176. );
  177. }}
  178. onMouseOverSample={sample => debounceSetHighlightedSpanId(sample.span_id)}
  179. onMouseLeaveSample={() => debounceSetHighlightedSpanId(undefined)}
  180. highlightedSpanId={highlightedSpanId}
  181. release={release}
  182. platform={isProjectCrossPlatform ? selectedPlatform : undefined}
  183. />
  184. <StyledSearchBar>
  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. </StyledSearchBar>
  193. <SampleTable
  194. referrer={TraceViewSources.APP_STARTS_MODULE}
  195. spanSearch={spanSearch}
  196. additionalFilters={additionalFilters}
  197. highlightedSpanId={highlightedSpanId}
  198. transactionMethod={transactionMethod}
  199. onMouseLeaveSample={() => setHighlightedSpanId(undefined)}
  200. onMouseOverSample={sample => setHighlightedSpanId(sample.span_id)}
  201. groupId={groupId}
  202. transactionName={transactionName}
  203. moduleName={moduleName}
  204. release={release}
  205. columnOrder={[
  206. {
  207. key: 'span_id',
  208. name: t('Span ID'),
  209. width: COL_WIDTH_UNDEFINED,
  210. },
  211. {
  212. key: 'profile_id',
  213. name: t('Profile'),
  214. width: COL_WIDTH_UNDEFINED,
  215. },
  216. {
  217. key: 'avg_comparison',
  218. name: t('Compared to Average'),
  219. width: COL_WIDTH_UNDEFINED,
  220. },
  221. ]}
  222. />
  223. </Fragment>
  224. );
  225. }
  226. const StyledReadoutRibbon = styled(ReadoutRibbon)`
  227. margin-bottom: ${space(2)};
  228. `;
  229. const SectionTitle = styled('div')`
  230. ${p => p.theme.text.cardTitle}
  231. `;
  232. const PaddedTitle = styled('div')`
  233. margin-bottom: ${space(1)};
  234. `;
  235. const StyledSearchBar = styled('div')`
  236. margin: ${space(2)} 0;
  237. `;