content.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import {useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import omit from 'lodash/omit';
  4. import {Alert} from 'sentry/components/alert';
  5. import FeatureBadge from 'sentry/components/badge/featureBadge';
  6. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  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 PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  12. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  13. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  14. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  15. import {IconClose} from 'sentry/icons/iconClose';
  16. import {t, tct} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import type {MetricAggregation, MRI} from 'sentry/types/metrics';
  19. import {browserHistory} from 'sentry/utils/browserHistory';
  20. import {getFormattedMQL} from 'sentry/utils/metrics';
  21. import {decodeInteger} from 'sentry/utils/queryString';
  22. import {useLocation} from 'sentry/utils/useLocation';
  23. import useOrganization from 'sentry/utils/useOrganization';
  24. import {ExploreContent} from 'sentry/views/explore/content';
  25. import * as ModuleLayout from 'sentry/views/insights/common/components/moduleLayout';
  26. import {usePageParams} from './hooks/usePageParams';
  27. import {useTraces} from './hooks/useTraces';
  28. import {TracesChart} from './tracesChart';
  29. import {TracesSearchBar} from './tracesSearchBar';
  30. import {TracesTable} from './tracesTable';
  31. import {normalizeTraces} from './utils';
  32. const TRACE_EXPLORER_DOCS_URL = 'https://docs.sentry.io/product/explore/traces/';
  33. const DEFAULT_STATS_PERIOD = '24h';
  34. const DEFAULT_PER_PAGE = 50;
  35. export default function Wrapper(props) {
  36. const location = useLocation();
  37. const organization = useOrganization();
  38. if (
  39. location.query.view !== 'trace' &&
  40. organization.features.includes('visibility-explore-view')
  41. ) {
  42. return <ExploreContent {...props} />;
  43. }
  44. return <Content {...props} />;
  45. }
  46. function Content() {
  47. const location = useLocation();
  48. const organization = useOrganization();
  49. const limit = useMemo(() => {
  50. return decodeInteger(location.query.perPage, DEFAULT_PER_PAGE);
  51. }, [location.query.perPage]);
  52. const {queries, metricsMax, metricsMin, metricsOp, metricsQuery, mri} =
  53. usePageParams(location);
  54. const hasMetric = metricsOp && mri;
  55. const removeMetric = useCallback(() => {
  56. browserHistory.push({
  57. ...location,
  58. query: omit(location.query, [
  59. 'mri',
  60. 'metricsOp',
  61. 'metricsQuery',
  62. 'metricsMax',
  63. 'metricsMin',
  64. ]),
  65. });
  66. }, [location]);
  67. const handleSearch = useCallback(
  68. (searchIndex: number, searchQuery: string) => {
  69. const newQueries = [...queries];
  70. if (newQueries.length === 0) {
  71. // In the odd case someone wants to add search bars before any query has been made, we add both the default one shown and a new one.
  72. newQueries[0] = '';
  73. }
  74. newQueries[searchIndex] = searchQuery;
  75. browserHistory.push({
  76. ...location,
  77. query: {
  78. ...location.query,
  79. cursor: undefined,
  80. query: typeof searchQuery === 'string' ? newQueries : queries,
  81. },
  82. });
  83. },
  84. [location, queries]
  85. );
  86. const handleClearSearch = useCallback(
  87. (searchIndex: number) => {
  88. const newQueries = [...queries];
  89. if (typeof newQueries[searchIndex] !== undefined) {
  90. delete newQueries[searchIndex];
  91. browserHistory.push({
  92. ...location,
  93. query: {
  94. ...location.query,
  95. cursor: undefined,
  96. query: newQueries,
  97. },
  98. });
  99. return true;
  100. }
  101. return false;
  102. },
  103. [location, queries]
  104. );
  105. const tracesQuery = useTraces({
  106. limit,
  107. query: queries,
  108. mri: hasMetric ? mri : undefined,
  109. metricsMax: hasMetric ? metricsMax : undefined,
  110. metricsMin: hasMetric ? metricsMin : undefined,
  111. metricsOp: hasMetric ? metricsOp : undefined,
  112. metricsQuery: hasMetric ? metricsQuery : undefined,
  113. });
  114. const isLoading = tracesQuery.isFetching;
  115. const isError = !isLoading && tracesQuery.isError;
  116. const isEmpty = !isLoading && !isError && (tracesQuery?.data?.data?.length ?? 0) === 0;
  117. const rawData = !isLoading && !isError ? tracesQuery?.data?.data : undefined;
  118. const data = normalizeTraces(rawData);
  119. return (
  120. <SentryDocumentTitle title={t('Traces')} orgSlug={organization.slug}>
  121. <PageFiltersContainer
  122. defaultSelection={{
  123. datetime: {start: null, end: null, utc: null, period: DEFAULT_STATS_PERIOD},
  124. }}
  125. >
  126. <Layout.Page>
  127. <Layout.Header>
  128. <Layout.HeaderContent>
  129. <HeaderContentBar>
  130. <Layout.Title>
  131. {t('Traces')}
  132. <PageHeadingQuestionTooltip
  133. docsUrl={TRACE_EXPLORER_DOCS_URL}
  134. title={t(
  135. 'Traces lets you search for individual spans that make up a trace, linked by a trace id.'
  136. )}
  137. />
  138. <FeatureBadge type="beta" />
  139. </Layout.Title>
  140. <FeedbackWidgetButton />
  141. </HeaderContentBar>
  142. </Layout.HeaderContent>
  143. </Layout.Header>
  144. <Layout.Body>
  145. <LayoutMain fullWidth>
  146. <PageFilterBar condensed>
  147. <ProjectPageFilter />
  148. <EnvironmentPageFilter />
  149. <DatePageFilter defaultPeriod="2h" />
  150. </PageFilterBar>
  151. {hasMetric && (
  152. <StyledAlert
  153. type="info"
  154. showIcon
  155. trailingItems={<StyledCloseButton onClick={removeMetric} />}
  156. >
  157. {tct('The metric query [metricQuery] is filtering the results below.', {
  158. metricQuery: (
  159. <strong>
  160. {getFormattedMQL({
  161. mri: mri as MRI,
  162. aggregation: metricsOp as MetricAggregation,
  163. query: metricsQuery,
  164. })}
  165. </strong>
  166. ),
  167. })}
  168. </StyledAlert>
  169. )}
  170. {isError && typeof tracesQuery.error?.responseJSON?.detail === 'string' ? (
  171. <StyledAlert type="error" showIcon>
  172. {tracesQuery.error?.responseJSON?.detail}
  173. </StyledAlert>
  174. ) : null}
  175. <TracesSearchBar
  176. queries={queries}
  177. handleSearch={handleSearch}
  178. handleClearSearch={handleClearSearch}
  179. />
  180. <ModuleLayout.Full>
  181. <TracesChart />
  182. </ModuleLayout.Full>
  183. <TracesTable
  184. isEmpty={isEmpty}
  185. isError={isError}
  186. isLoading={isLoading}
  187. queries={queries}
  188. data={data}
  189. />
  190. </LayoutMain>
  191. </Layout.Body>
  192. </Layout.Page>
  193. </PageFiltersContainer>
  194. </SentryDocumentTitle>
  195. );
  196. }
  197. const HeaderContentBar = styled('div')`
  198. display: flex;
  199. align-items: center;
  200. justify-content: space-between;
  201. flex-direction: row;
  202. `;
  203. const LayoutMain = styled(Layout.Main)`
  204. display: flex;
  205. flex-direction: column;
  206. gap: ${space(2)};
  207. `;
  208. const StyledAlert = styled(Alert)`
  209. margin-bottom: 0;
  210. `;
  211. const StyledCloseButton = styled(IconClose)`
  212. cursor: pointer;
  213. `;