content.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  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 organization = useOrganization();
  37. if (organization.features.includes('visibility-explore-view')) {
  38. return <ExploreContent {...props} />;
  39. }
  40. return <Content {...props} />;
  41. }
  42. function Content() {
  43. const location = useLocation();
  44. const organization = useOrganization();
  45. const limit = useMemo(() => {
  46. return decodeInteger(location.query.perPage, DEFAULT_PER_PAGE);
  47. }, [location.query.perPage]);
  48. const {queries, metricsMax, metricsMin, metricsOp, metricsQuery, mri} =
  49. usePageParams(location);
  50. const hasMetric = metricsOp && mri;
  51. const removeMetric = useCallback(() => {
  52. browserHistory.push({
  53. ...location,
  54. query: omit(location.query, [
  55. 'mri',
  56. 'metricsOp',
  57. 'metricsQuery',
  58. 'metricsMax',
  59. 'metricsMin',
  60. ]),
  61. });
  62. }, [location]);
  63. const handleSearch = useCallback(
  64. (searchIndex: number, searchQuery: string) => {
  65. const newQueries = [...queries];
  66. if (newQueries.length === 0) {
  67. // 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.
  68. newQueries[0] = '';
  69. }
  70. newQueries[searchIndex] = searchQuery;
  71. browserHistory.push({
  72. ...location,
  73. query: {
  74. ...location.query,
  75. cursor: undefined,
  76. query: typeof searchQuery === 'string' ? newQueries : queries,
  77. },
  78. });
  79. },
  80. [location, queries]
  81. );
  82. const handleClearSearch = useCallback(
  83. (searchIndex: number) => {
  84. const newQueries = [...queries];
  85. if (typeof newQueries[searchIndex] !== undefined) {
  86. delete newQueries[searchIndex];
  87. browserHistory.push({
  88. ...location,
  89. query: {
  90. ...location.query,
  91. cursor: undefined,
  92. query: newQueries,
  93. },
  94. });
  95. return true;
  96. }
  97. return false;
  98. },
  99. [location, queries]
  100. );
  101. const tracesQuery = useTraces({
  102. limit,
  103. query: queries,
  104. mri: hasMetric ? mri : undefined,
  105. metricsMax: hasMetric ? metricsMax : undefined,
  106. metricsMin: hasMetric ? metricsMin : undefined,
  107. metricsOp: hasMetric ? metricsOp : undefined,
  108. metricsQuery: hasMetric ? metricsQuery : undefined,
  109. });
  110. const isLoading = tracesQuery.isFetching;
  111. const isError = !isLoading && tracesQuery.isError;
  112. const isEmpty = !isLoading && !isError && (tracesQuery?.data?.data?.length ?? 0) === 0;
  113. const rawData = !isLoading && !isError ? tracesQuery?.data?.data : undefined;
  114. const data = normalizeTraces(rawData);
  115. return (
  116. <SentryDocumentTitle title={t('Traces')} orgSlug={organization.slug}>
  117. <PageFiltersContainer
  118. defaultSelection={{
  119. datetime: {start: null, end: null, utc: null, period: DEFAULT_STATS_PERIOD},
  120. }}
  121. >
  122. <Layout.Page>
  123. <Layout.Header>
  124. <Layout.HeaderContent>
  125. <HeaderContentBar>
  126. <Layout.Title>
  127. {t('Traces')}
  128. <PageHeadingQuestionTooltip
  129. docsUrl={TRACE_EXPLORER_DOCS_URL}
  130. title={t(
  131. 'Traces lets you search for individual spans that make up a trace, linked by a trace id.'
  132. )}
  133. />
  134. <FeatureBadge type="beta" />
  135. </Layout.Title>
  136. <FeedbackWidgetButton />
  137. </HeaderContentBar>
  138. </Layout.HeaderContent>
  139. </Layout.Header>
  140. <Layout.Body>
  141. <LayoutMain fullWidth>
  142. <PageFilterBar condensed>
  143. <ProjectPageFilter />
  144. <EnvironmentPageFilter />
  145. <DatePageFilter defaultPeriod="2h" />
  146. </PageFilterBar>
  147. {hasMetric && (
  148. <StyledAlert
  149. type="info"
  150. showIcon
  151. trailingItems={<StyledCloseButton onClick={removeMetric} />}
  152. >
  153. {tct('The metric query [metricQuery] is filtering the results below.', {
  154. metricQuery: (
  155. <strong>
  156. {getFormattedMQL({
  157. mri: mri as MRI,
  158. aggregation: metricsOp as MetricAggregation,
  159. query: metricsQuery,
  160. })}
  161. </strong>
  162. ),
  163. })}
  164. </StyledAlert>
  165. )}
  166. {isError && typeof tracesQuery.error?.responseJSON?.detail === 'string' ? (
  167. <StyledAlert type="error" showIcon>
  168. {tracesQuery.error?.responseJSON?.detail}
  169. </StyledAlert>
  170. ) : null}
  171. <TracesSearchBar
  172. queries={queries}
  173. handleSearch={handleSearch}
  174. handleClearSearch={handleClearSearch}
  175. />
  176. <ModuleLayout.Full>
  177. <TracesChart />
  178. </ModuleLayout.Full>
  179. <TracesTable
  180. isEmpty={isEmpty}
  181. isError={isError}
  182. isLoading={isLoading}
  183. queries={queries}
  184. data={data}
  185. />
  186. </LayoutMain>
  187. </Layout.Body>
  188. </Layout.Page>
  189. </PageFiltersContainer>
  190. </SentryDocumentTitle>
  191. );
  192. }
  193. const HeaderContentBar = styled('div')`
  194. display: flex;
  195. align-items: center;
  196. justify-content: space-between;
  197. flex-direction: row;
  198. `;
  199. const LayoutMain = styled(Layout.Main)`
  200. display: flex;
  201. flex-direction: column;
  202. gap: ${space(2)};
  203. `;
  204. const StyledAlert = styled(Alert)`
  205. margin-bottom: 0;
  206. `;
  207. const StyledCloseButton = styled(IconClose)`
  208. cursor: pointer;
  209. `;