content.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import {useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import Feature from 'sentry/components/acl/feature';
  5. import {Alert} from 'sentry/components/alert';
  6. import FeatureBadge from 'sentry/components/badge/featureBadge';
  7. import {Button} from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  9. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  12. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  13. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  14. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  15. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  16. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  17. import {
  18. EAPSpanSearchQueryBuilder,
  19. SpanSearchQueryBuilder,
  20. } from 'sentry/components/performance/spanSearchQueryBuilder';
  21. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  22. import {t} from 'sentry/locale';
  23. import {space} from 'sentry/styles/space';
  24. import {defined} from 'sentry/utils';
  25. import {dedupeArray} from 'sentry/utils/dedupeArray';
  26. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  27. import {
  28. type AggregationKey,
  29. ALLOWED_EXPLORE_VISUALIZE_AGGREGATES,
  30. } from 'sentry/utils/fields';
  31. import {useLocation} from 'sentry/utils/useLocation';
  32. import {useNavigate} from 'sentry/utils/useNavigate';
  33. import useOrganization from 'sentry/utils/useOrganization';
  34. import usePageFilters from 'sentry/utils/usePageFilters';
  35. import {ExploreCharts} from 'sentry/views/explore/charts';
  36. import {
  37. PageParamsProvider,
  38. useExploreDataset,
  39. useExploreMode,
  40. useExploreQuery,
  41. useExploreVisualizes,
  42. useSetExploreQuery,
  43. } from 'sentry/views/explore/contexts/pageParamsContext';
  44. import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
  45. import {
  46. SpanTagsProvider,
  47. useSpanTags,
  48. } from 'sentry/views/explore/contexts/spanTagsContext';
  49. import {useExploreAggregatesTable} from 'sentry/views/explore/hooks/useExploreAggregatesTable';
  50. import {useExploreSpansTable} from 'sentry/views/explore/hooks/useExploreSpansTable';
  51. import {useExploreTimeseries} from 'sentry/views/explore/hooks/useExploreTimeseries';
  52. import {useExploreTracesTable} from 'sentry/views/explore/hooks/useExploreTracesTable';
  53. import {Tab, useTab} from 'sentry/views/explore/hooks/useTab';
  54. import {ExploreTables} from 'sentry/views/explore/tables';
  55. import {ExploreToolbar} from 'sentry/views/explore/toolbar';
  56. import {combineConfidenceForSeries} from 'sentry/views/explore/utils';
  57. function ExploreContentImpl() {
  58. const location = useLocation();
  59. const navigate = useNavigate();
  60. const organization = useOrganization();
  61. const {selection} = usePageFilters();
  62. const dataset = useExploreDataset();
  63. const mode = useExploreMode();
  64. const visualizes = useExploreVisualizes();
  65. const [samplesTab, setSamplesTab] = useTab();
  66. const numberTags = useSpanTags('number');
  67. const stringTags = useSpanTags('string');
  68. const query = useExploreQuery();
  69. const setQuery = useSetExploreQuery();
  70. const toolbarExtras = organization.features.includes('visibility-explore-dataset')
  71. ? ['dataset toggle' as const]
  72. : [];
  73. const switchToOldTraceExplorer = useCallback(() => {
  74. navigate({
  75. ...location,
  76. query: {
  77. ...location.query,
  78. view: 'trace',
  79. },
  80. });
  81. }, [location, navigate]);
  82. const maxPickableDays = 7;
  83. const queryType: 'aggregate' | 'samples' | 'traces' =
  84. mode === Mode.AGGREGATE
  85. ? 'aggregate'
  86. : samplesTab === Tab.TRACE
  87. ? 'traces'
  88. : 'samples';
  89. const aggregatesTableResult = useExploreAggregatesTable({
  90. query,
  91. enabled: queryType === 'aggregate',
  92. });
  93. const spansTableResult = useExploreSpansTable({
  94. query,
  95. enabled: queryType === 'samples',
  96. });
  97. const tracesTableResult = useExploreTracesTable({
  98. query,
  99. enabled: queryType === 'traces',
  100. });
  101. const {timeseriesResult, canUsePreviousResults} = useExploreTimeseries({query});
  102. const confidences = useMemo(
  103. () =>
  104. visualizes.map(visualize => {
  105. const dedupedYAxes = dedupeArray(visualize.yAxes);
  106. const series = dedupedYAxes
  107. .flatMap(yAxis => timeseriesResult.data[yAxis])
  108. .filter(defined);
  109. return combineConfidenceForSeries(series);
  110. }),
  111. [timeseriesResult.data, visualizes]
  112. );
  113. const tableError =
  114. queryType === 'aggregate'
  115. ? aggregatesTableResult.result.error?.message ?? ''
  116. : queryType === 'traces'
  117. ? tracesTableResult.result.error?.message ?? ''
  118. : spansTableResult.result.error?.message ?? '';
  119. const chartError = timeseriesResult.error?.message ?? '';
  120. return (
  121. <SentryDocumentTitle title={t('Traces')} orgSlug={organization.slug}>
  122. <PageFiltersContainer maxPickableDays={maxPickableDays}>
  123. <Layout.Page>
  124. <Layout.Header>
  125. <Layout.HeaderContent>
  126. <Layout.Title>
  127. {t('Traces')}
  128. <PageHeadingQuestionTooltip
  129. docsUrl="https://github.com/getsentry/sentry/discussions/81239"
  130. title={t(
  131. 'Find problematic spans/traces or compute real-time metrics via aggregation.'
  132. )}
  133. linkLabel={t('Read the Discussion')}
  134. />
  135. <FeatureBadge
  136. title={t(
  137. 'This feature is available for early adopters and the UX may change'
  138. )}
  139. type="beta"
  140. />
  141. </Layout.Title>
  142. </Layout.HeaderContent>
  143. <Layout.HeaderActions>
  144. <ButtonBar gap={1}>
  145. <Feature organization={organization} features="visibility-explore-admin">
  146. <Button onClick={switchToOldTraceExplorer} size="sm">
  147. {t('Switch to Old Trace Explore')}
  148. </Button>
  149. </Feature>
  150. <FeedbackWidgetButton />
  151. </ButtonBar>
  152. </Layout.HeaderActions>
  153. </Layout.Header>
  154. <Body>
  155. <TopSection>
  156. <StyledPageFilterBar condensed>
  157. <ProjectPageFilter />
  158. <EnvironmentPageFilter />
  159. <DatePageFilter
  160. defaultPeriod="7d"
  161. maxPickableDays={maxPickableDays}
  162. relativeOptions={({arbitraryOptions}) => ({
  163. ...arbitraryOptions,
  164. '1h': t('Last 1 hour'),
  165. '24h': t('Last 24 hours'),
  166. '7d': t('Last 7 days'),
  167. })}
  168. />
  169. </StyledPageFilterBar>
  170. {dataset === DiscoverDatasets.SPANS_INDEXED ? (
  171. <SpanSearchQueryBuilder
  172. projects={selection.projects}
  173. initialQuery={query}
  174. onSearch={setQuery}
  175. searchSource="explore"
  176. />
  177. ) : (
  178. <EAPSpanSearchQueryBuilder
  179. projects={selection.projects}
  180. initialQuery={query}
  181. onSearch={setQuery}
  182. searchSource="explore"
  183. getFilterTokenWarning={
  184. mode === Mode.SAMPLES
  185. ? key => {
  186. if (
  187. ALLOWED_EXPLORE_VISUALIZE_AGGREGATES.includes(
  188. key as AggregationKey
  189. )
  190. ) {
  191. return t(
  192. "This key won't affect the results because samples mode does not support aggregate functions"
  193. );
  194. }
  195. return undefined;
  196. }
  197. : undefined
  198. }
  199. supportedAggregates={ALLOWED_EXPLORE_VISUALIZE_AGGREGATES}
  200. numberTags={numberTags}
  201. stringTags={stringTags}
  202. />
  203. )}
  204. </TopSection>
  205. <ExploreToolbar extras={toolbarExtras} />
  206. <MainSection fullWidth>
  207. {(tableError || chartError) && (
  208. <Alert type="error" showIcon>
  209. {tableError || chartError}
  210. </Alert>
  211. )}
  212. <ExploreCharts
  213. canUsePreviousResults={canUsePreviousResults}
  214. confidences={confidences}
  215. query={query}
  216. timeseriesResult={timeseriesResult}
  217. />
  218. <ExploreTables
  219. aggregatesTableResult={aggregatesTableResult}
  220. spansTableResult={spansTableResult}
  221. tracesTableResult={tracesTableResult}
  222. confidences={confidences}
  223. samplesTab={samplesTab}
  224. setSamplesTab={setSamplesTab}
  225. />
  226. </MainSection>
  227. </Body>
  228. </Layout.Page>
  229. </PageFiltersContainer>
  230. </SentryDocumentTitle>
  231. );
  232. }
  233. function ExploreTagsProvider({children}) {
  234. const dataset = useExploreDataset();
  235. return (
  236. <SpanTagsProvider dataset={dataset} enabled>
  237. {children}
  238. </SpanTagsProvider>
  239. );
  240. }
  241. export function ExploreContent() {
  242. Sentry.setTag('explore.visited', 'yes');
  243. return (
  244. <PageParamsProvider>
  245. <ExploreTagsProvider>
  246. <ExploreContentImpl />
  247. </ExploreTagsProvider>
  248. </PageParamsProvider>
  249. );
  250. }
  251. const Body = styled(Layout.Body)`
  252. gap: ${space(2)};
  253. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  254. grid-template-columns: 350px minmax(100px, auto);
  255. gap: ${space(2)};
  256. }
  257. @media (min-width: ${p => p.theme.breakpoints.xxlarge}) {
  258. grid-template-columns: 400px minmax(100px, auto);
  259. }
  260. `;
  261. const TopSection = styled('div')`
  262. display: grid;
  263. gap: ${space(2)};
  264. grid-column: 1/3;
  265. margin-bottom: ${space(2)};
  266. @media (min-width: ${p => p.theme.breakpoints.large}) {
  267. grid-template-columns: minmax(350px, auto) 1fr;
  268. margin-bottom: 0;
  269. }
  270. @media (min-width: ${p => p.theme.breakpoints.xxlarge}) {
  271. grid-template-columns: minmax(400px, auto) 1fr;
  272. }
  273. `;
  274. const MainSection = styled(Layout.Main)`
  275. grid-column: 2/3;
  276. `;
  277. const StyledPageFilterBar = styled(PageFilterBar)`
  278. width: auto;
  279. `;