spanSamplesPanelContainer.tsx 7.8 KB

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