layout.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import {Fragment, memo, useCallback} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import * as Sentry from '@sentry/react';
  5. import emptyStateImg from 'sentry-images/spot/custom-metrics-empty-state.svg';
  6. import Alert from 'sentry/components/alert';
  7. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  8. import FeatureBadge from 'sentry/components/badge/featureBadge';
  9. import Banner from 'sentry/components/banner';
  10. import {Button} from 'sentry/components/button';
  11. import ButtonBar from 'sentry/components/buttonBar';
  12. import FloatingFeedbackWidget from 'sentry/components/feedback/widget/floatingFeedbackWidget';
  13. import * as Layout from 'sentry/components/layouts/thirds';
  14. import ExternalLink from 'sentry/components/links/externalLink';
  15. import LoadingIndicator from 'sentry/components/loadingIndicator';
  16. import OnboardingPanel from 'sentry/components/onboardingPanel';
  17. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  18. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  19. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  20. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  21. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  22. import {t, tct} from 'sentry/locale';
  23. import {space} from 'sentry/styles/space';
  24. import type {Organization} from 'sentry/types/organization';
  25. import {trackAnalytics} from 'sentry/utils/analytics';
  26. import {METRICS_DOCS_URL} from 'sentry/utils/metrics/constants';
  27. import {
  28. hasCustomMetrics,
  29. hasCustomMetricsExtractionRules,
  30. } from 'sentry/utils/metrics/features';
  31. import {useVirtualMetricsContext} from 'sentry/utils/metrics/virtualMetricsContext';
  32. import useDismissAlert from 'sentry/utils/useDismissAlert';
  33. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  34. import useMedia from 'sentry/utils/useMedia';
  35. import useOrganization from 'sentry/utils/useOrganization';
  36. import usePageFilters from 'sentry/utils/usePageFilters';
  37. import BackgroundSpace from 'sentry/views/discover/backgroundSpace';
  38. import {useMetricsContext} from 'sentry/views/metrics/context';
  39. import {useMetricsOnboardingSidebar} from 'sentry/views/metrics/ddmOnboarding/useMetricsOnboardingSidebar';
  40. import {IntervalSelect} from 'sentry/views/metrics/intervalSelect';
  41. import {MetricsApiChangeAlert} from 'sentry/views/metrics/metricsApiChangeAlert';
  42. import {MetricsStopIngestionAlert} from 'sentry/views/metrics/metricsIngestionStopAlert';
  43. import {PageHeaderActions} from 'sentry/views/metrics/pageHeaderActions';
  44. import {Queries} from 'sentry/views/metrics/queries';
  45. import {MetricScratchpad} from 'sentry/views/metrics/scratchpad';
  46. import {WidgetDetails} from 'sentry/views/metrics/widgetDetails';
  47. function showEmptyState({
  48. organization,
  49. isEmptyStateDismissed,
  50. hasPerformanceMetrics,
  51. hasSentCustomMetrics,
  52. }: {
  53. hasPerformanceMetrics: boolean;
  54. hasSentCustomMetrics: boolean;
  55. isEmptyStateDismissed: boolean;
  56. organization: Organization;
  57. }) {
  58. if (hasCustomMetricsExtractionRules(organization)) {
  59. return !hasSentCustomMetrics && !hasPerformanceMetrics;
  60. }
  61. return !isEmptyStateDismissed && !hasSentCustomMetrics;
  62. }
  63. export const MetricsLayout = memo(() => {
  64. const organization = useOrganization();
  65. const pageFilters = usePageFilters();
  66. const selectedProjects = pageFilters.selection.projects.join();
  67. const {
  68. hasCustomMetrics: hasSentCustomMetrics,
  69. hasPerformanceMetrics,
  70. isHasMetricsLoading,
  71. } = useMetricsContext();
  72. const virtualMetrics = useVirtualMetricsContext();
  73. const isLoading = isHasMetricsLoading || virtualMetrics.isLoading;
  74. const {activateSidebar} = useMetricsOnboardingSidebar();
  75. const {dismiss: emptyStateDismiss, isDismissed: isEmptyStateDismissed} =
  76. useDismissAlert({
  77. key: `${organization.id}:${selectedProjects}:metrics-empty-state-dismissed`,
  78. });
  79. const theme = useTheme();
  80. const isSmallBanner = useMedia(`(max-width: ${theme.breakpoints.medium})`);
  81. const [isBannerDismissed] = useLocalStorageState('metrics-banner-dismissed', false);
  82. const addCustomMetric = useCallback(
  83. (referrer: 'header' | 'onboarding_panel' | 'banner') => {
  84. Sentry.metrics.increment('ddm.add_custom_metric', 1, {
  85. tags: {
  86. referrer,
  87. },
  88. });
  89. trackAnalytics('ddm.open-onboarding', {
  90. organization,
  91. source: referrer,
  92. });
  93. activateSidebar();
  94. },
  95. [activateSidebar, organization]
  96. );
  97. const viewPerformanceMetrics = useCallback(() => {
  98. Sentry.metrics.increment('ddm.view_performance_metrics', 1);
  99. trackAnalytics('ddm.view_performance_metrics', {
  100. organization,
  101. });
  102. emptyStateDismiss();
  103. }, [emptyStateDismiss, organization]);
  104. const showOnboardingPanel = showEmptyState({
  105. organization,
  106. isEmptyStateDismissed,
  107. hasPerformanceMetrics,
  108. hasSentCustomMetrics,
  109. });
  110. if (!hasCustomMetrics(organization)) {
  111. return (
  112. <Layout.Page withPadding>
  113. <Alert type="warning">{t("You don't have access to this feature")}</Alert>
  114. </Layout.Page>
  115. );
  116. }
  117. return (
  118. <Fragment>
  119. <Layout.Header>
  120. <Layout.HeaderContent>
  121. <Layout.Title>
  122. {t('Metrics')}
  123. <PageHeadingQuestionTooltip
  124. docsUrl={METRICS_DOCS_URL}
  125. title={t(
  126. 'Metrics help you track and visualize the data points you care about, making it easier to monitor your application health and identify issues.'
  127. )}
  128. />
  129. <FeatureBadge type="beta" />
  130. </Layout.Title>
  131. </Layout.HeaderContent>
  132. <Layout.HeaderActions>
  133. {!showOnboardingPanel ? (
  134. <PageHeaderActions
  135. showAddMetricButton={
  136. hasCustomMetricsExtractionRules(organization) ||
  137. hasSentCustomMetrics ||
  138. (isEmptyStateDismissed && isBannerDismissed)
  139. }
  140. addCustomMetric={() => addCustomMetric('header')}
  141. />
  142. ) : null}
  143. </Layout.HeaderActions>
  144. </Layout.Header>
  145. <Layout.Body>
  146. <FloatingFeedbackWidget />
  147. <Layout.Main fullWidth>
  148. {isEmptyStateDismissed &&
  149. !hasSentCustomMetrics &&
  150. !hasCustomMetricsExtractionRules(organization) && (
  151. <Banner
  152. title={t('Custom Metrics')}
  153. subtitle={t(
  154. "Track your system's behaviour and profit from the same powerful features as you do with errors, like alerting and dashboards."
  155. )}
  156. backgroundComponent={<BackgroundSpace />}
  157. dismissKey="metrics"
  158. >
  159. <Button
  160. size={isSmallBanner ? 'xs' : undefined}
  161. translucentBorder
  162. onClick={() => addCustomMetric('banner')}
  163. >
  164. {t('Set Up')}
  165. </Button>
  166. </Banner>
  167. )}
  168. {hasCustomMetricsExtractionRules(organization) ? (
  169. !isLoading && hasSentCustomMetrics ? (
  170. <MetricsStopIngestionAlert />
  171. ) : null
  172. ) : (
  173. <MetricsApiChangeAlert />
  174. )}
  175. <FilterContainer>
  176. <PageFilterBar condensed>
  177. <ProjectPageFilter />
  178. <EnvironmentPageFilter />
  179. <DatePageFilter />
  180. </PageFilterBar>
  181. <IntervalSelect />
  182. </FilterContainer>
  183. {isLoading ? (
  184. <LoadingIndicator />
  185. ) : !showOnboardingPanel ? (
  186. <Fragment>
  187. <GuideAnchor target="metrics_onboarding" />
  188. <Queries />
  189. <MetricScratchpad />
  190. <WidgetDetails />
  191. </Fragment>
  192. ) : (
  193. <OnboardingPanel image={<EmptyStateImage src={emptyStateImg} />}>
  194. <h3>{t('Track and solve what matters')}</h3>
  195. {hasCustomMetricsExtractionRules(organization) ? (
  196. <Fragment>
  197. <p>
  198. {tct(
  199. 'Query and plot metrics extracted from your span data to visualise trends and identify anomalies. To get started, you need to enable [link:tracing].',
  200. {
  201. link: (
  202. <ExternalLink href="https://docs.sentry.io/concepts/key-terms/tracing/" />
  203. ),
  204. }
  205. )}
  206. </p>
  207. <Button
  208. priority="primary"
  209. href="https://docs.sentry.io/product/performance/getting-started/"
  210. external
  211. >
  212. {t('Set Up Tracing')}
  213. </Button>
  214. </Fragment>
  215. ) : (
  216. <Fragment>
  217. <p>
  218. {t(
  219. 'Create custom metrics to track and visualize the data points you care about over time, like processing time, checkout conversion rate, or user signups. See correlated trace exemplars and metrics if used together with Performance Monitoring.'
  220. )}
  221. </p>
  222. <ButtonList gap={1}>
  223. <Button
  224. priority="primary"
  225. onClick={() => addCustomMetric('onboarding_panel')}
  226. >
  227. {t('Set Up Custom Metric')}
  228. </Button>
  229. <Button href={METRICS_DOCS_URL} external>
  230. {t('Read Docs')}
  231. </Button>
  232. {hasPerformanceMetrics && (
  233. <Button onClick={viewPerformanceMetrics}>
  234. {t('View Performance Metrics')}
  235. </Button>
  236. )}
  237. </ButtonList>
  238. </Fragment>
  239. )}
  240. </OnboardingPanel>
  241. )}
  242. </Layout.Main>
  243. </Layout.Body>
  244. </Fragment>
  245. );
  246. });
  247. const FilterContainer = styled('div')`
  248. margin-bottom: ${space(2)};
  249. display: flex;
  250. justify-content: flex-start;
  251. flex-wrap: wrap;
  252. gap: ${space(1)};
  253. `;
  254. const EmptyStateImage = styled('img')`
  255. @media (min-width: ${p => p.theme.breakpoints.small}) {
  256. user-select: none;
  257. position: absolute;
  258. top: 0;
  259. bottom: 0;
  260. width: 220px;
  261. margin-top: auto;
  262. margin-bottom: auto;
  263. transform: translateX(-50%);
  264. left: 50%;
  265. }
  266. @media (min-width: ${p => p.theme.breakpoints.large}) {
  267. transform: translateX(-60%);
  268. width: 280px;
  269. }
  270. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  271. transform: translateX(-75%);
  272. width: 320px;
  273. }
  274. `;
  275. const ButtonList = styled(ButtonBar)`
  276. grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
  277. grid-auto-flow: row;
  278. gap: ${space(1)};
  279. `;