Browse Source

Merge branch 'master' into aknaus/feat/metrics-extraction/selectable-aggregates

ArthurKnaus 1 week ago
parent
commit
f205f151ff

+ 13 - 11
src/sentry/utils/concurrent.py

@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import collections
 import functools
 import logging
 import threading
@@ -10,7 +9,7 @@ from concurrent.futures._base import FINISHED, RUNNING
 from contextlib import contextmanager
 from queue import Full, PriorityQueue
 from time import time
-from typing import TYPE_CHECKING, TypeVar
+from typing import TYPE_CHECKING, Generic, NamedTuple, TypeVar
 
 from sentry_sdk import Hub
 
@@ -46,7 +45,10 @@ def execute(function: Callable[..., T], daemon=True):
 
 
 @functools.total_ordering
-class PriorityTask(collections.namedtuple("PriorityTask", "priority item")):
+class PriorityTask(NamedTuple, Generic[T]):
+    priority: int
+    item: tuple[Callable[[], T], Hub, FutureBase[T]]
+
     def __eq__(self, b):
         return self.priority == b.priority
 
@@ -54,7 +56,7 @@ class PriorityTask(collections.namedtuple("PriorityTask", "priority item")):
         return self.priority < b.priority
 
 
-class TimedFuture(FutureBase):
+class TimedFuture(FutureBase[T]):
     _condition: threading.Condition
     _state: str
 
@@ -126,7 +128,7 @@ class TimedFuture(FutureBase):
             return super().set_exception(*args, **kwargs)
 
 
-class Executor:
+class Executor(Generic[T]):
     """
     This class provides an API for executing tasks in different contexts
     (immediately, or asynchronously.)
@@ -140,7 +142,7 @@ class Executor:
 
     Future = TimedFuture
 
-    def submit(self, callable, priority=0, block=True, timeout=None) -> TimedFuture:
+    def submit(self, callable, priority=0, block=True, timeout=None) -> TimedFuture[T]:
         """
         Enqueue a task to be executed, returning a ``TimedFuture``.
 
@@ -151,7 +153,7 @@ class Executor:
         raise NotImplementedError
 
 
-class SynchronousExecutor(Executor):
+class SynchronousExecutor(Executor[T]):
     """
     This executor synchronously executes callables in the current thread.
 
@@ -166,7 +168,7 @@ class SynchronousExecutor(Executor):
         """
         Immediately execute a callable, returning a ``TimedFuture``.
         """
-        future = self.Future()
+        future: FutureBase[T] = self.Future()
         assert future.set_running_or_notify_cancel()
         try:
             result = callable()
@@ -177,7 +179,7 @@ class SynchronousExecutor(Executor):
         return future
 
 
-class ThreadedExecutor(Executor):
+class ThreadedExecutor(Executor[T]):
     """\
     This executor provides a method of executing callables in a threaded worker
     pool. The number of outstanding requests can be limited by the ``maxsize``
@@ -192,7 +194,7 @@ class ThreadedExecutor(Executor):
         self.__worker_count = worker_count
         self.__workers = set()
         self.__started = False
-        self.__queue: PriorityQueue[PriorityTask] = PriorityQueue(maxsize)
+        self.__queue: PriorityQueue[PriorityTask[T]] = PriorityQueue(maxsize)
         self.__lock = threading.Lock()
 
     def __worker(self):
@@ -237,7 +239,7 @@ class ThreadedExecutor(Executor):
         if not self.__started:
             self.start()
 
-        future = self.Future()
+        future: FutureBase[T] = self.Future()
         task = PriorityTask(priority, (callable, Hub.current, future))
         try:
             self.__queue.put(task, block=block, timeout=timeout)

+ 9 - 0
static/app/views/settings/projectMetrics/customMetricsTable.tsx

@@ -14,12 +14,14 @@ import {space} from 'sentry/styles/space';
 import type {MetricMeta} from 'sentry/types/metrics';
 import type {Project} from 'sentry/types/project';
 import {DEFAULT_METRICS_CARDINALITY_LIMIT} from 'sentry/utils/metrics/constants';
+import {hasCustomMetricsExtractionRules} from 'sentry/utils/metrics/features';
 import {getReadableMetricType} from 'sentry/utils/metrics/formatters';
 import {formatMRI} from 'sentry/utils/metrics/mri';
 import {useBlockMetric} from 'sentry/utils/metrics/useBlockMetric';
 import {useMetricsCardinality} from 'sentry/utils/metrics/useMetricsCardinality';
 import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
 import {middleEllipsis} from 'sentry/utils/string/middleEllipsis';
+import useOrganization from 'sentry/utils/useOrganization';
 import {useAccess} from 'sentry/views/settings/projectMetrics/access';
 import {BlockButton} from 'sentry/views/settings/projectMetrics/blockButton';
 import {useSearchQueryParam} from 'sentry/views/settings/projectMetrics/utils/useSearchQueryParam';
@@ -36,6 +38,7 @@ enum BlockingStatusTab {
 type MetricWithCardinality = MetricMeta & {cardinality: number};
 
 export function CustomMetricsTable({project}: Props) {
+  const organiztion = useOrganization();
   const [selectedTab, setSelectedTab] = useState(BlockingStatusTab.ACTIVE);
   const [query, setQuery] = useSearchQueryParam('metricsQuery');
 
@@ -80,6 +83,12 @@ export function CustomMetricsTable({project}: Props) {
       unit.includes(query)
   );
 
+  // If we have custom metrics extraction rules,
+  // we only show the custom metrics table if the project has custom metrics
+  if (hasCustomMetricsExtractionRules(organiztion) && metricsMeta.data.length === 0) {
+    return null;
+  }
+
   return (
     <Fragment>
       <SearchWrapper>

+ 6 - 32
static/app/views/settings/projectMetrics/metricsExtractionRulesTable.tsx

@@ -3,7 +3,6 @@ import styled from '@emotion/styled';
 
 import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
 import {openModal} from 'sentry/actionCreators/modal';
-import Tag from 'sentry/components/badge/tag';
 import {Button, LinkButton} from 'sentry/components/button';
 import {openConfirmModal} from 'sentry/components/confirm';
 import {modalCss} from 'sentry/components/featureFeedback/feedbackModal';
@@ -15,7 +14,6 @@ import {IconEdit} from 'sentry/icons/iconEdit';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {Project} from 'sentry/types/project';
-import {getReadableMetricType} from 'sentry/utils/metrics/formatters';
 import useOrganization from 'sentry/utils/useOrganization';
 import {MetricsExtractionRuleEditModal} from 'sentry/views/settings/projectMetrics/metricsExtractionRuleEditModal';
 import {
@@ -92,7 +90,7 @@ export function MetricsExtractionRulesTable({project}: Props) {
         <h6>{t('Span-based Metrics')}</h6>
         <FlexSpacer />
         <SearchBar
-          placeholder={t('Search Extraction Rules')}
+          placeholder={t('Search Metrics')}
           onChange={setQuery}
           query={query}
           size="sm"
@@ -138,26 +136,17 @@ function RulesTable({
           <IconArrow size="xs" direction="down" />
           {t('Span attribute')}
         </Cell>,
-        <Cell right key="type">
-          {t('Type')}
-        </Cell>,
-        <Cell right key="unit">
-          {t('Unit')}
-        </Cell>,
         <Cell right key="filters">
           {t('Filters')}
         </Cell>,
-        <Cell right key="tags">
-          {t('Tags')}
-        </Cell>,
         <Cell right key="actions">
           {t('Actions')}
         </Cell>,
       ]}
       emptyMessage={
         hasSearch
-          ? t('No extraction rules match the query.')
-          : t('You have not created any extraction rules yet.')
+          ? t('No metrics match the query.')
+          : t('You have not created any span-based metrics yet.')
       }
       isEmpty={extractionRules.length === 0}
       isLoading={isLoading}
@@ -167,12 +156,6 @@ function RulesTable({
         .map(rule => (
           <Fragment key={rule.spanAttribute + rule.type + rule.unit}>
             <Cell>{rule.spanAttribute}</Cell>
-            <Cell right>
-              <Tag>{getReadableMetricType(rule.type)}</Tag>
-            </Cell>
-            <Cell right>
-              <Tag>{rule.unit}</Tag>
-            </Cell>
             <Cell right>
               {rule.conditions.length ? (
                 <Button priority="link" onClick={() => onEdit(rule)}>
@@ -182,25 +165,16 @@ function RulesTable({
                 <NoValue>{t('(none)')}</NoValue>
               )}
             </Cell>
-            <Cell right>
-              {rule.tags.length ? (
-                <Button priority="link" onClick={() => onEdit(rule)}>
-                  {rule.tags.length}
-                </Button>
-              ) : (
-                <NoValue>{t('(none)')}</NoValue>
-              )}
-            </Cell>
             <Cell right>
               <Button
-                aria-label={t('Delete rule')}
+                aria-label={t('Delete metric')}
                 size="xs"
                 icon={<IconDelete />}
                 borderless
                 onClick={() => onDelete(rule)}
               />
               <Button
-                aria-label={t('Edit rule')}
+                aria-label={t('Edit metric')}
                 size="xs"
                 icon={<IconEdit />}
                 borderless
@@ -230,7 +204,7 @@ const FlexSpacer = styled('div')`
 `;
 
 const ExtractionRulesPanelTable = styled(PanelTable)`
-  grid-template-columns: 1fr repeat(5, min-content);
+  grid-template-columns: 1fr repeat(2, min-content);
 `;
 
 const Cell = styled('div')<{right?: boolean}>`

+ 4 - 2
static/app/views/settings/projectMetrics/projectMetrics.tsx

@@ -65,9 +65,11 @@ function ProjectMetrics({project}: Props) {
 
       <PermissionAlert project={project} />
 
-      {hasExtractionRules && <MetricsExtractionRulesTable project={project} />}
+      {hasExtractionRules ? <MetricsExtractionRulesTable project={project} /> : null}
 
-      <CustomMetricsTable project={project} />
+      {!hasExtractionRules || project.hasCustomMetrics ? (
+        <CustomMetricsTable project={project} />
+      ) : null}
     </Fragment>
   );
 }

+ 7 - 7
tests/sentry/utils/test_concurrent.py

@@ -95,7 +95,7 @@ def timestamp(t):
 
 
 def test_timed_future_success():
-    future = TimedFuture()
+    future: TimedFuture[object] = TimedFuture()
     assert future.get_timing() == (None, None)
 
     expected_result = mock.sentinel.RESULT_VALUE
@@ -121,7 +121,7 @@ def test_timed_future_success():
 
 
 def test_time_is_not_overwritten_if_fail_to_set_result():
-    future = TimedFuture()
+    future: TimedFuture[int] = TimedFuture()
 
     with timestamp(1.0):
         future.set_running_or_notify_cancel()
@@ -140,7 +140,7 @@ def test_time_is_not_overwritten_if_fail_to_set_result():
 
 
 def test_timed_future_error():
-    future = TimedFuture()
+    future: TimedFuture[None] = TimedFuture()
     assert future.get_timing() == (None, None)
 
     start_time, finish_time = expected_timing = (1.0, 2.0)
@@ -165,7 +165,7 @@ def test_timed_future_error():
 
 
 def test_timed_future_cancel():
-    future = TimedFuture()
+    future: TimedFuture[None] = TimedFuture()
     assert future.get_timing() == (None, None)
 
     with timestamp(1.0):
@@ -187,7 +187,7 @@ def test_timed_future_cancel():
 
 
 def test_synchronous_executor():
-    executor = SynchronousExecutor()
+    executor: SynchronousExecutor[object] = SynchronousExecutor()
 
     assert executor.submit(lambda: mock.sentinel.RESULT).result() is mock.sentinel.RESULT
 
@@ -204,7 +204,7 @@ def test_synchronous_executor():
 
 
 def test_threaded_same_priority_Tasks():
-    executor = ThreadedExecutor(worker_count=1)
+    executor: ThreadedExecutor[None] = ThreadedExecutor(worker_count=1)
 
     def callable():
         pass
@@ -215,7 +215,7 @@ def test_threaded_same_priority_Tasks():
 
 
 def test_threaded_executor():
-    executor = ThreadedExecutor(worker_count=1, maxsize=3)
+    executor: ThreadedExecutor[int] = ThreadedExecutor(worker_count=1, maxsize=3)
 
     def waiter(ready, waiting, result):
         ready.set()

+ 1 - 1
tests/sentry/utils/test_services.py

@@ -34,7 +34,7 @@ class Error(Operation):
 
 @pytest.fixture
 def delegator_fixture() -> tuple[Delegator, Mock, Mock]:
-    executor = SynchronousExecutor()
+    executor: SynchronousExecutor[object] = SynchronousExecutor()
     selector = Mock()
     callback = Mock()
     delegator = Delegator(