spansTab.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. import {useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import Alert from 'sentry/components/alert';
  5. import {Button} from 'sentry/components/button';
  6. import {getDiffInMinutes} from 'sentry/components/charts/utils';
  7. import * as Layout from 'sentry/components/layouts/thirds';
  8. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  9. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  10. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  11. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  12. import {
  13. EAPSpanSearchQueryBuilder,
  14. SpanSearchQueryBuilder,
  15. } from 'sentry/components/performance/spanSearchQueryBuilder';
  16. import {IconChevron} from 'sentry/icons/iconChevron';
  17. import {t} from 'sentry/locale';
  18. import {space} from 'sentry/styles/space';
  19. import type {PageFilters} from 'sentry/types/core';
  20. import {defined} from 'sentry/utils';
  21. import {dedupeArray} from 'sentry/utils/dedupeArray';
  22. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  23. import {
  24. type AggregationKey,
  25. ALLOWED_EXPLORE_VISUALIZE_AGGREGATES,
  26. } from 'sentry/utils/fields';
  27. import useOrganization from 'sentry/utils/useOrganization';
  28. import usePageFilters from 'sentry/utils/usePageFilters';
  29. import {ExploreCharts} from 'sentry/views/explore/charts';
  30. import {
  31. PageParamsProvider,
  32. useExploreDataset,
  33. useExploreMode,
  34. useExploreQuery,
  35. useExploreVisualizes,
  36. useSetExploreQuery,
  37. } from 'sentry/views/explore/contexts/pageParamsContext';
  38. import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
  39. import {
  40. SpanTagsProvider,
  41. useSpanTags,
  42. } from 'sentry/views/explore/contexts/spanTagsContext';
  43. import {useAnalytics} from 'sentry/views/explore/hooks/useAnalytics';
  44. import {useExploreAggregatesTable} from 'sentry/views/explore/hooks/useExploreAggregatesTable';
  45. import {useExploreSpansTable} from 'sentry/views/explore/hooks/useExploreSpansTable';
  46. import {useExploreTimeseries} from 'sentry/views/explore/hooks/useExploreTimeseries';
  47. import {useExploreTracesTable} from 'sentry/views/explore/hooks/useExploreTracesTable';
  48. import {Tab, useTab} from 'sentry/views/explore/hooks/useTab';
  49. import {ExploreTables} from 'sentry/views/explore/tables';
  50. import {ExploreToolbar} from 'sentry/views/explore/toolbar';
  51. import {
  52. combineConfidenceForSeries,
  53. type DefaultPeriod,
  54. type MaxPickableDays,
  55. } from 'sentry/views/explore/utils';
  56. export type SpanTabProps = {
  57. defaultPeriod: DefaultPeriod;
  58. maxPickableDays: MaxPickableDays;
  59. relativeOptions: Record<string, React.ReactNode>;
  60. };
  61. export function SpansTabContentImpl({
  62. defaultPeriod,
  63. maxPickableDays,
  64. relativeOptions,
  65. }: SpanTabProps) {
  66. const organization = useOrganization();
  67. const {selection} = usePageFilters();
  68. const dataset = useExploreDataset();
  69. const mode = useExploreMode();
  70. const visualizes = useExploreVisualizes();
  71. const [samplesTab, setSamplesTab] = useTab();
  72. const numberTags = useSpanTags('number');
  73. const stringTags = useSpanTags('string');
  74. const query = useExploreQuery();
  75. const setQuery = useSetExploreQuery();
  76. const toolbarExtras = organization?.features?.includes('visibility-explore-dataset')
  77. ? ['dataset toggle' as const]
  78. : [];
  79. const queryType: 'aggregate' | 'samples' | 'traces' =
  80. mode === Mode.AGGREGATE
  81. ? 'aggregate'
  82. : samplesTab === Tab.TRACE
  83. ? 'traces'
  84. : 'samples';
  85. const limit = 25;
  86. const isAllowedSelection = useMemo(
  87. () => checkIsAllowedSelection(selection, maxPickableDays),
  88. [selection, maxPickableDays]
  89. );
  90. const aggregatesTableResult = useExploreAggregatesTable({
  91. query,
  92. limit,
  93. enabled: isAllowedSelection && queryType === 'aggregate',
  94. });
  95. const spansTableResult = useExploreSpansTable({
  96. query,
  97. limit,
  98. enabled: isAllowedSelection && queryType === 'samples',
  99. });
  100. const tracesTableResult = useExploreTracesTable({
  101. query,
  102. limit,
  103. enabled: isAllowedSelection && queryType === 'traces',
  104. });
  105. const {timeseriesResult, canUsePreviousResults} = useExploreTimeseries({
  106. query,
  107. enabled: isAllowedSelection,
  108. });
  109. const confidences = useMemo(
  110. () =>
  111. visualizes.map(visualize => {
  112. const dedupedYAxes = dedupeArray(visualize.yAxes);
  113. const series = dedupedYAxes
  114. .flatMap(yAxis => timeseriesResult.data[yAxis])
  115. .filter(defined);
  116. return combineConfidenceForSeries(series);
  117. }),
  118. [timeseriesResult.data, visualizes]
  119. );
  120. const tableError =
  121. queryType === 'aggregate'
  122. ? aggregatesTableResult.result.error?.message ?? ''
  123. : queryType === 'traces'
  124. ? tracesTableResult.result.error?.message ?? ''
  125. : spansTableResult.result.error?.message ?? '';
  126. const chartError = timeseriesResult.error?.message ?? '';
  127. const [expanded, setExpanded] = useState(true);
  128. useAnalytics({
  129. queryType,
  130. aggregatesTableResult,
  131. spansTableResult,
  132. tracesTableResult,
  133. timeseriesResult,
  134. });
  135. return (
  136. <Body withToolbar={expanded}>
  137. <TopSection>
  138. <StyledPageFilterBar condensed>
  139. <ProjectPageFilter />
  140. <EnvironmentPageFilter />
  141. <DatePageFilter
  142. defaultPeriod={defaultPeriod}
  143. maxPickableDays={maxPickableDays}
  144. relativeOptions={({arbitraryOptions}) => ({
  145. ...arbitraryOptions,
  146. ...relativeOptions,
  147. })}
  148. />
  149. </StyledPageFilterBar>
  150. {dataset === DiscoverDatasets.SPANS_INDEXED ? (
  151. <SpanSearchQueryBuilder
  152. projects={selection.projects}
  153. initialQuery={query}
  154. onSearch={setQuery}
  155. searchSource="explore"
  156. />
  157. ) : (
  158. <EAPSpanSearchQueryBuilder
  159. projects={selection.projects}
  160. initialQuery={query}
  161. onSearch={setQuery}
  162. searchSource="explore"
  163. getFilterTokenWarning={
  164. mode === Mode.SAMPLES
  165. ? key => {
  166. if (
  167. ALLOWED_EXPLORE_VISUALIZE_AGGREGATES.includes(key as AggregationKey)
  168. ) {
  169. return t(
  170. "This key won't affect the results because samples mode does not support aggregate functions"
  171. );
  172. }
  173. return undefined;
  174. }
  175. : undefined
  176. }
  177. supportedAggregates={
  178. mode === Mode.SAMPLES ? [] : ALLOWED_EXPLORE_VISUALIZE_AGGREGATES
  179. }
  180. numberTags={numberTags}
  181. stringTags={stringTags}
  182. />
  183. )}
  184. </TopSection>
  185. <SideSection>
  186. <ExploreToolbar width={300} extras={toolbarExtras} />
  187. </SideSection>
  188. <section>
  189. {(tableError || chartError) && (
  190. <Alert type="error" showIcon>
  191. {tableError || chartError}
  192. </Alert>
  193. )}
  194. <MainContent>
  195. <ExploreCharts
  196. canUsePreviousResults={canUsePreviousResults}
  197. confidences={confidences}
  198. isAllowedSelection={isAllowedSelection}
  199. query={query}
  200. timeseriesResult={timeseriesResult}
  201. />
  202. <ExploreTables
  203. aggregatesTableResult={aggregatesTableResult}
  204. spansTableResult={spansTableResult}
  205. tracesTableResult={tracesTableResult}
  206. confidences={confidences}
  207. samplesTab={samplesTab}
  208. setSamplesTab={setSamplesTab}
  209. />
  210. <Toggle withToolbar={expanded}>
  211. <StyledButton
  212. aria-label={expanded ? t('Collapse sidebar') : t('Expande sidebar')}
  213. size="xs"
  214. icon={<IconDoubleChevron direction={expanded ? 'left' : 'right'} />}
  215. onClick={() => setExpanded(!expanded)}
  216. />
  217. </Toggle>
  218. </MainContent>
  219. </section>
  220. </Body>
  221. );
  222. }
  223. function IconDoubleChevron(props: React.ComponentProps<typeof IconChevron>) {
  224. return (
  225. <DoubleChevronWrapper>
  226. <IconChevron style={{marginRight: `-3px`}} {...props} />
  227. <IconChevron style={{marginLeft: `-3px`}} {...props} />
  228. </DoubleChevronWrapper>
  229. );
  230. }
  231. function ExploreTagsProvider({children}: any) {
  232. const dataset = useExploreDataset();
  233. return (
  234. <SpanTagsProvider dataset={dataset} enabled>
  235. {children}
  236. </SpanTagsProvider>
  237. );
  238. }
  239. export function SpansTabContent(props: SpanTabProps) {
  240. Sentry.setTag('explore.visited', 'yes');
  241. return (
  242. <PageParamsProvider>
  243. <ExploreTagsProvider>
  244. <SpansTabContentImpl {...props} />
  245. </ExploreTagsProvider>
  246. </PageParamsProvider>
  247. );
  248. }
  249. function checkIsAllowedSelection(
  250. selection: PageFilters,
  251. maxPickableDays: MaxPickableDays
  252. ) {
  253. const maxPickableMinutes = maxPickableDays * 24 * 60;
  254. const selectedMinutes = getDiffInMinutes(selection.datetime);
  255. return selectedMinutes <= maxPickableMinutes;
  256. }
  257. const Body = styled(Layout.Body)<{withToolbar: boolean}>`
  258. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  259. display: grid;
  260. ${p =>
  261. p.withToolbar
  262. ? `grid-template-columns: 300px minmax(100px, auto);`
  263. : `grid-template-columns: 0px minmax(100px, auto);`}
  264. gap: ${space(2)} ${p => (p.withToolbar ? `${space(2)}` : '0px')};
  265. transition: 700ms;
  266. }
  267. `;
  268. const TopSection = styled('div')`
  269. display: grid;
  270. gap: ${space(2)};
  271. grid-column: 1/3;
  272. margin-bottom: ${space(2)};
  273. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  274. grid-template-columns: minmax(300px, auto) 1fr;
  275. margin-bottom: 0;
  276. }
  277. `;
  278. const SideSection = styled('aside')`
  279. overflow: hidden;
  280. `;
  281. const MainContent = styled('div')`
  282. position: relative;
  283. max-width: 100%;
  284. `;
  285. const StyledPageFilterBar = styled(PageFilterBar)`
  286. width: auto;
  287. `;
  288. const Toggle = styled('div')<{withToolbar: boolean}>`
  289. display: none;
  290. position: absolute;
  291. top: 0px;
  292. z-index: 1; /* place above loading mask */
  293. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  294. display: block;
  295. }
  296. `;
  297. const StyledButton = styled(Button)`
  298. width: 28px;
  299. border-left: 0px;
  300. border-top-left-radius: 0px;
  301. border-bottom-left-radius: 0px;
  302. `;
  303. const DoubleChevronWrapper = styled('div')`
  304. display: flex;
  305. `;