spanSamplesPanelContainer.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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 {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 {DiscoverDatasets} from 'sentry/utils/discover/types';
  12. import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
  13. import {decodeScalar} from 'sentry/utils/queryString';
  14. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  15. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  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 {MetricReadout} from 'sentry/views/insights/common/components/metricReadout';
  21. import {ReadoutRibbon} from 'sentry/views/insights/common/components/ribbon';
  22. import {useSpanMetrics} from 'sentry/views/insights/common/queries/useDiscover';
  23. import {formatVersionAndCenterTruncate} from 'sentry/views/insights/common/utils/centerTruncate';
  24. import {DataTitles} from 'sentry/views/insights/common/views/spans/types';
  25. import DurationChart from 'sentry/views/insights/common/views/spanSummaryPage/sampleList/durationChart';
  26. import SampleTable from 'sentry/views/insights/common/views/spanSummaryPage/sampleList/sampleTable/sampleTable';
  27. import useCrossPlatformProject from 'sentry/views/insights/mobile/common/queries/useCrossPlatformProject';
  28. import {
  29. type ModuleName,
  30. SpanMetricsField,
  31. type SpanMetricsQueryFilters,
  32. } from 'sentry/views/insights/types';
  33. import {useSpanFieldSupportedTags} from 'sentry/views/performance/utils/useSpanFieldSupportedTags';
  34. import {TraceViewSources} from '../../../../performance/newTraceDetails/traceMetadataHeader';
  35. const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsField;
  36. type Props = {
  37. groupId: string;
  38. moduleName: ModuleName;
  39. transactionName: string;
  40. additionalFilters?: Record<string, string>;
  41. release?: string;
  42. searchQueryKey?: string;
  43. sectionSubtitle?: string;
  44. sectionTitle?: string;
  45. spanOp?: string;
  46. transactionMethod?: string;
  47. };
  48. export function SpanSamplesContainer({
  49. groupId,
  50. moduleName,
  51. transactionName,
  52. transactionMethod,
  53. release,
  54. searchQueryKey,
  55. spanOp,
  56. additionalFilters,
  57. }: Props) {
  58. const location = useLocation();
  59. const router = useRouter();
  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 supportedTags = useSpanFieldSupportedTags();
  67. const searchQuery =
  68. searchQueryKey !== undefined
  69. ? decodeScalar(location.query[searchQueryKey])
  70. : undefined;
  71. // eslint-disable-next-line react-hooks/exhaustive-deps
  72. const debounceSetHighlightedSpanId = useCallback(
  73. debounce(id => {
  74. setHighlightedSpanId(id);
  75. }, 10),
  76. []
  77. );
  78. const spanSearch = new MutableSearch(searchQuery ?? '');
  79. if (additionalFilters) {
  80. Object.entries(additionalFilters).forEach(([key, value]) => {
  81. spanSearch.addFilterValue(key, value);
  82. });
  83. }
  84. const filters: SpanMetricsQueryFilters = {
  85. 'span.group': groupId,
  86. transaction: transactionName,
  87. };
  88. if (transactionMethod) {
  89. filters['transaction.method'] = transactionMethod;
  90. }
  91. if (release) {
  92. filters.release = release;
  93. }
  94. if (isProjectCrossPlatform) {
  95. filters['os.name'] = selectedPlatform;
  96. }
  97. if (spanOp) {
  98. filters['span.op'] = spanOp;
  99. }
  100. const {data, isPending} = useSpanMetrics(
  101. {
  102. search: MutableSearch.fromQueryObject({...filters, ...additionalFilters}),
  103. fields: [`avg(${SPAN_SELF_TIME})`, 'count()', SPAN_OP],
  104. enabled: Boolean(groupId) && Boolean(transactionName),
  105. },
  106. 'api.starfish.span-summary-panel-samples-table-avg'
  107. );
  108. const spanMetrics = data[0] ?? {};
  109. const handleSearch = (newSearchQuery: string) => {
  110. router.replace({
  111. pathname: location.pathname,
  112. query: {
  113. ...location.query,
  114. ...(searchQueryKey && {[searchQueryKey]: newSearchQuery}),
  115. },
  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. router.push(
  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. searchSource={`${moduleName}-sample-panel`}
  181. query={searchQuery}
  182. onSearch={handleSearch}
  183. placeholder={t('Search for span attributes')}
  184. organization={organization}
  185. supportedTags={supportedTags}
  186. dataset={DiscoverDatasets.SPANS_INDEXED}
  187. projectIds={selection.projects}
  188. />
  189. <SampleTable
  190. referrer={TraceViewSources.APP_STARTS_MODULE}
  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 StyledReadoutRibbon = styled(ReadoutRibbon)`
  223. margin-bottom: ${space(2)};
  224. `;
  225. const SectionTitle = styled('div')`
  226. ${p => p.theme.text.cardTitle}
  227. `;
  228. const PaddedTitle = styled('div')`
  229. margin-bottom: ${space(1)};
  230. `;
  231. const StyledSearchBar = styled(SearchBar)`
  232. margin-top: ${space(2)};
  233. `;