layout.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  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 Banner from 'sentry/components/banner';
  9. import {Button} from 'sentry/components/button';
  10. import ButtonBar from 'sentry/components/buttonBar';
  11. import FloatingFeedbackWidget from 'sentry/components/feedback/widget/floatingFeedbackWidget';
  12. import * as Layout from 'sentry/components/layouts/thirds';
  13. import LoadingIndicator from 'sentry/components/loadingIndicator';
  14. import OnboardingPanel from 'sentry/components/onboardingPanel';
  15. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  16. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  17. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  18. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  19. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  20. import {t} from 'sentry/locale';
  21. import {space} from 'sentry/styles/space';
  22. import {trackAnalytics} from 'sentry/utils/analytics';
  23. import {METRICS_DOCS_URL} from 'sentry/utils/metrics/constants';
  24. import {canSeeMetricsPage} from 'sentry/utils/metrics/features';
  25. import useDismissAlert from 'sentry/utils/useDismissAlert';
  26. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  27. import useMedia from 'sentry/utils/useMedia';
  28. import useOrganization from 'sentry/utils/useOrganization';
  29. import usePageFilters from 'sentry/utils/usePageFilters';
  30. import BackgroundSpace from 'sentry/views/discover/backgroundSpace';
  31. import {
  32. MetricsOnboardingPanelPrimaryAction,
  33. MetricsSubscriptionAlert,
  34. } from 'sentry/views/metrics/billing';
  35. import {useMetricsContext} from 'sentry/views/metrics/context';
  36. import {useMetricsOnboardingSidebar} from 'sentry/views/metrics/ddmOnboarding/useMetricsOnboardingSidebar';
  37. import {IntervalSelect} from 'sentry/views/metrics/intervalSelect';
  38. import {MetricsFeatureBadge} from 'sentry/views/metrics/metricFeatureBadge';
  39. import {PageHeaderActions} from 'sentry/views/metrics/pageHeaderActions';
  40. import {Queries} from 'sentry/views/metrics/queries';
  41. import {MetricScratchpad} from 'sentry/views/metrics/scratchpad';
  42. import {WidgetDetails} from 'sentry/views/metrics/widgetDetails';
  43. export const MetricsLayout = memo(() => {
  44. const organization = useOrganization();
  45. const pageFilters = usePageFilters();
  46. const selectedProjects = pageFilters.selection.projects.join();
  47. const {hasCustomMetrics, hasPerformanceMetrics, isHasMetricsLoading} =
  48. useMetricsContext();
  49. const {activateSidebar} = useMetricsOnboardingSidebar();
  50. const {dismiss: emptyStateDismiss, isDismissed: isEmptyStateDismissed} =
  51. useDismissAlert({
  52. key: `${organization.id}:${selectedProjects}:metrics-empty-state-dismissed`,
  53. });
  54. const theme = useTheme();
  55. const isSmallBanner = useMedia(`(max-width: ${theme.breakpoints.medium})`);
  56. const [isBannerDismissed] = useLocalStorageState('metrics-banner-dismissed', false);
  57. const showOnboardingPanel = !isEmptyStateDismissed && !hasCustomMetrics;
  58. const addCustomMetric = useCallback(
  59. (referrer: 'header' | 'onboarding_panel' | 'banner') => {
  60. Sentry.metrics.increment('ddm.add_custom_metric', 1, {
  61. tags: {
  62. referrer,
  63. },
  64. });
  65. trackAnalytics('ddm.open-onboarding', {
  66. organization,
  67. source: referrer,
  68. });
  69. activateSidebar();
  70. },
  71. [activateSidebar, organization]
  72. );
  73. const viewPerformanceMetrics = useCallback(() => {
  74. Sentry.metrics.increment('ddm.view_performance_metrics', 1);
  75. trackAnalytics('ddm.view_performance_metrics', {
  76. organization,
  77. });
  78. emptyStateDismiss();
  79. }, [emptyStateDismiss, organization]);
  80. if (!canSeeMetricsPage(organization)) {
  81. return (
  82. <Layout.Page withPadding>
  83. <Alert type="warning">{t("You don't have access to this feature")}</Alert>
  84. </Layout.Page>
  85. );
  86. }
  87. return (
  88. <Fragment>
  89. <MetricsSubscriptionAlert organization={organization} />
  90. <Layout.Header>
  91. <Layout.HeaderContent>
  92. <Layout.Title>
  93. {t('Metrics')}
  94. <PageHeadingQuestionTooltip
  95. docsUrl={METRICS_DOCS_URL}
  96. title={t(
  97. 'Metrics help you track and visualize the data points you care about, making it easier to monitor your application health and identify issues.'
  98. )}
  99. />
  100. <MetricsFeatureBadge />
  101. </Layout.Title>
  102. </Layout.HeaderContent>
  103. <Layout.HeaderActions>
  104. {!showOnboardingPanel ? (
  105. <PageHeaderActions
  106. showCustomMetricButton={
  107. hasCustomMetrics || (isEmptyStateDismissed && isBannerDismissed)
  108. }
  109. addCustomMetric={() => addCustomMetric('header')}
  110. />
  111. ) : null}
  112. </Layout.HeaderActions>
  113. </Layout.Header>
  114. <Layout.Body>
  115. <FloatingFeedbackWidget />
  116. <Layout.Main fullWidth>
  117. {isEmptyStateDismissed && !hasCustomMetrics && (
  118. <Banner
  119. title={t('Custom Metrics')}
  120. subtitle={t(
  121. "Track your system's behaviour and profit from the same powerful features as you do with errors, like alerting and dashboards."
  122. )}
  123. backgroundComponent={<BackgroundSpace />}
  124. dismissKey="metrics"
  125. >
  126. <Button
  127. size={isSmallBanner ? 'xs' : undefined}
  128. translucentBorder
  129. onClick={() => addCustomMetric('banner')}
  130. >
  131. {t('Set Up')}
  132. </Button>
  133. </Banner>
  134. )}
  135. <FilterContainer>
  136. <PageFilterBar condensed>
  137. <ProjectPageFilter />
  138. <EnvironmentPageFilter />
  139. <DatePageFilter />
  140. </PageFilterBar>
  141. <IntervalSelect />
  142. </FilterContainer>
  143. {isHasMetricsLoading ? (
  144. <LoadingIndicator />
  145. ) : !showOnboardingPanel ? (
  146. <Fragment>
  147. <GuideAnchor target="metrics_onboarding" />
  148. <Queries />
  149. <MetricScratchpad />
  150. <WidgetDetails />
  151. </Fragment>
  152. ) : (
  153. <OnboardingPanel image={<EmptyStateImage src={emptyStateImg} />}>
  154. <h3>{t('Track and solve what matters')}</h3>
  155. <p>
  156. {t(
  157. '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.'
  158. )}
  159. </p>
  160. <MetricsOnboardingPanelPrimaryAction organization={organization}>
  161. <ButtonList gap={1}>
  162. <Button
  163. priority="primary"
  164. onClick={() => addCustomMetric('onboarding_panel')}
  165. >
  166. {t('Set Up Custom Metric')}
  167. </Button>
  168. <Button href={METRICS_DOCS_URL} external>
  169. {t('Read Docs')}
  170. </Button>
  171. {hasPerformanceMetrics && (
  172. <Button onClick={viewPerformanceMetrics}>
  173. {t('View Performance Metrics')}
  174. </Button>
  175. )}
  176. </ButtonList>
  177. </MetricsOnboardingPanelPrimaryAction>
  178. </OnboardingPanel>
  179. )}
  180. </Layout.Main>
  181. </Layout.Body>
  182. </Fragment>
  183. );
  184. });
  185. const FilterContainer = styled('div')`
  186. margin-bottom: ${space(2)};
  187. display: flex;
  188. justify-content: flex-start;
  189. flex-wrap: wrap;
  190. gap: ${space(1)};
  191. `;
  192. const EmptyStateImage = styled('img')`
  193. @media (min-width: ${p => p.theme.breakpoints.small}) {
  194. user-select: none;
  195. position: absolute;
  196. top: 0;
  197. bottom: 0;
  198. width: 220px;
  199. margin-top: auto;
  200. margin-bottom: auto;
  201. transform: translateX(-50%);
  202. left: 50%;
  203. }
  204. @media (min-width: ${p => p.theme.breakpoints.large}) {
  205. transform: translateX(-60%);
  206. width: 280px;
  207. }
  208. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  209. transform: translateX(-75%);
  210. width: 320px;
  211. }
  212. `;
  213. const ButtonList = styled(ButtonBar)`
  214. grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
  215. grid-auto-flow: row;
  216. gap: ${space(1)};
  217. `;