Browse Source

feat(ddm): Settings (#62374)

![image](https://github.com/getsentry/sentry/assets/9060071/806f09f5-5b50-49aa-a7ff-f91338451128)

Relates to https://github.com/getsentry/sentry/issues/62359
Closes https://github.com/getsentry/sentry/issues/60764
Closes https://github.com/getsentry/sentry/issues/60765
Closes https://github.com/getsentry/sentry/issues/60766
Matej Minar 1 year ago
parent
commit
6c23499509

+ 6 - 0
static/app/routes.tsx

@@ -567,6 +567,12 @@ function buildRoutes() {
         name={t('Performance')}
         component={make(() => import('sentry/views/settings/projectPerformance'))}
       />
+      <Route path="metrics/" name={t('Metrics')}>
+        <IndexRoute
+          component={make(() => import('sentry/views/settings/projectMetrics'))}
+        />
+      </Route>
+
       <Route path="source-maps/" name={t('Source Maps')}>
         <IndexRoute
           component={make(() => import('sentry/views/settings/projectSourceMaps'))}

+ 3 - 0
static/app/utils/metrics/index.tsx

@@ -50,6 +50,9 @@ import {
 
 import {DateString, PageFilters} from '../../types/core';
 
+export const METRICS_DOCS_URL =
+  'https://develop.sentry.dev/delightful-developer-metrics/';
+
 export enum MetricDisplayType {
   LINE = 'line',
   AREA = 'area',

+ 3 - 10
static/app/views/ddm/ddmOnboarding/sidebar.tsx

@@ -14,6 +14,7 @@ import platforms from 'sentry/data/platforms';
 import {t, tct} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {Project, SelectValue} from 'sentry/types';
+import {METRICS_DOCS_URL} from 'sentry/utils/metrics';
 import useOrganization from 'sentry/utils/useOrganization';
 
 import {useCurrentProjectState} from './useCurrentProjectState';
@@ -160,11 +161,7 @@ function OnboardingContent({
           })}
         </div>
         <div>
-          <LinkButton
-            size="sm"
-            href="https://develop.sentry.dev/delightful-developer-metrics/"
-            external
-          >
+          <LinkButton size="sm" href={METRICS_DOCS_URL} external>
             {t('Go to Sentry Documentation')}
           </LinkButton>
         </div>
@@ -182,11 +179,7 @@ function OnboardingContent({
           )}
         </div>
         <div>
-          <LinkButton
-            size="sm"
-            href="https://develop.sentry.dev/delightful-developer-metrics/"
-            external
-          >
+          <LinkButton size="sm" href={METRICS_DOCS_URL} external>
             {t('Read Docs')}
           </LinkButton>
         </div>

+ 2 - 1
static/app/views/ddm/layout.tsx

@@ -18,6 +18,7 @@ import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilt
 import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
+import {METRICS_DOCS_URL} from 'sentry/utils/metrics';
 import {useDDMContext} from 'sentry/views/ddm/context';
 import {useMetricsOnboardingSidebar} from 'sentry/views/ddm/ddmOnboarding/useMetricsOnboardingSidebar';
 import {MetricScratchpad} from 'sentry/views/ddm/scratchpad';
@@ -36,7 +37,7 @@ export const DDMLayout = memo(() => {
           <Layout.Title>
             {t('Metrics')}
             <PageHeadingQuestionTooltip
-              docsUrl="https://develop.sentry.dev/delightful-developer-metrics/"
+              docsUrl={METRICS_DOCS_URL}
               title={t('Delightful Developer Metrics.')}
             />
             <FeatureBadge type="alpha" />

+ 9 - 0
static/app/views/settings/project/navigationConfiguration.tsx

@@ -112,6 +112,15 @@ export default function getConfiguration({
           title: t('Performance'),
           show: () => !!organization?.features?.includes('performance-view'),
         },
+        {
+          path: `${pathPrefix}/metrics/`,
+          title: t('Metrics'),
+          show: () =>
+            !!(
+              organization?.features?.includes('custom-metrics') &&
+              organization?.features?.includes('ddm-ui')
+            ),
+        },
       ],
     },
     {

+ 21 - 0
static/app/views/settings/projectMetrics/index.tsx

@@ -0,0 +1,21 @@
+import {RouteComponentProps} from 'react-router';
+
+import Feature from 'sentry/components/acl/feature';
+import {Organization, Project} from 'sentry/types';
+
+import ProjectMetrics from './projectMetrics';
+
+type Props = RouteComponentProps<{projectId: string}, {}> & {
+  organization: Organization;
+  project: Project;
+};
+
+function ProjectMetricsContainer(props: Props) {
+  return (
+    <Feature features={['ddm-ui', 'custom-metrics']}>
+      <ProjectMetrics {...props} />
+    </Feature>
+  );
+}
+
+export default ProjectMetricsContainer;

+ 122 - 0
static/app/views/settings/projectMetrics/projectMetrics.tsx

@@ -0,0 +1,122 @@
+import {Fragment, useMemo} from 'react';
+import {browserHistory, RouteComponentProps} from 'react-router';
+import styled from '@emotion/styled';
+import debounce from 'lodash/debounce';
+
+import ExternalLink from 'sentry/components/links/externalLink';
+import PanelTable from 'sentry/components/panels/panelTable';
+import SearchBar from 'sentry/components/searchBar';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
+import Tag from 'sentry/components/tag';
+import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
+import {t, tct} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {Organization, Project} from 'sentry/types';
+import {getReadableMetricType, METRICS_DOCS_URL} from 'sentry/utils/metrics';
+import {formatMRI} from 'sentry/utils/metrics/mri';
+import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
+import {middleEllipsis} from 'sentry/utils/middleEllipsis';
+import {decodeScalar} from 'sentry/utils/queryString';
+import routeTitleGen from 'sentry/utils/routeTitle';
+import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
+import TextBlock from 'sentry/views/settings/components/text/textBlock';
+import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
+
+type Props = {
+  organization: Organization;
+  project: Project;
+} & RouteComponentProps<{projectId: string}, {}>;
+
+function ProjectMetrics({project, location}: Props) {
+  const {data: meta, isLoading} = useMetricsMeta([parseInt(project.id, 10)], ['custom']);
+  const query = decodeScalar(location.query.query, '');
+
+  const debouncedSearch = useMemo(
+    () =>
+      debounce(
+        (searchQuery: string) =>
+          browserHistory.replace({
+            pathname: location.pathname,
+            query: {...location.query, query: searchQuery},
+          }),
+        DEFAULT_DEBOUNCE_DURATION
+      ),
+    [location.pathname, location.query]
+  );
+
+  const metrics = meta
+    .sort((a, b) => formatMRI(a.mri).localeCompare(formatMRI(b.mri)))
+    .filter(
+      ({mri, type, unit}) =>
+        mri.includes(query) ||
+        getReadableMetricType(type).includes(query) ||
+        unit.includes(query)
+    );
+
+  return (
+    <Fragment>
+      <SentryDocumentTitle title={routeTitleGen(t('Metrics'), project.slug, false)} />
+      <SettingsPageHeader title={t('Metrics')} />
+
+      <TextBlock>
+        {tct(
+          `Metrics are numerical values that can track anything about your environment over time, from latency to error rates to user signups. To learn more about metrics, [link:read the docs].`,
+          {
+            link: <ExternalLink href={METRICS_DOCS_URL} />,
+          }
+        )}
+      </TextBlock>
+
+      <PermissionAlert project={project} />
+
+      <SearchWrapper>
+        <SearchBar
+          placeholder={t('Search Metrics')}
+          onChange={debouncedSearch}
+          query={query}
+        />
+      </SearchWrapper>
+
+      <StyledPanelTable
+        headers={[
+          t('Metric'),
+          <RightAligned key="type"> {t('Type')}</RightAligned>,
+          <RightAligned key="unit">{t('Unit')}</RightAligned>,
+        ]}
+        emptyMessage={
+          query
+            ? t('No metrics match the query.')
+            : t('There are no custom metrics for this project.')
+        }
+        isEmpty={metrics.length === 0}
+        isLoading={isLoading}
+      >
+        {metrics.map(({mri, type, unit}) => (
+          <Fragment key={mri}>
+            <div>{middleEllipsis(formatMRI(mri), 65, /\.|-|_/)}</div>
+            <RightAligned>
+              <Tag>{getReadableMetricType(type)}</Tag>
+            </RightAligned>
+            <RightAligned>
+              <Tag>{unit}</Tag>
+            </RightAligned>
+          </Fragment>
+        ))}
+      </StyledPanelTable>
+    </Fragment>
+  );
+}
+
+const SearchWrapper = styled('div')`
+  margin-bottom: ${space(2)};
+`;
+
+const StyledPanelTable = styled(PanelTable)`
+  grid-template-columns: 1fr minmax(115px, min-content) minmax(115px, min-content);
+`;
+
+const RightAligned = styled('div')`
+  text-align: right;
+`;
+
+export default ProjectMetrics;