Browse Source

feat(stat-detectors): Use event-facets in tags tab list page (#57356)

Redirect the tags query from the group tags endpoint to the
events facets endpoint. This is necessary because the tags we require
are not stored on the group explicitly, but rather through transactions
past a specific time period (the detection breakpoint).

Since we require the breakpoint timestamp, we need the latest event to
be loaded. If you're coming from the Overview page, an event is fetched
and cached from that visit. If you're refreshing the tags tab, there is
no preloaded event so we must make a query to fetch it, then that can be
used to fetch the tag facets.
Nar Saynorath 1 year ago
parent
commit
505b4c6ba5

+ 7 - 1
static/app/actionCreators/group.tsx

@@ -420,6 +420,8 @@ type FetchIssueTagsParameters = {
   isStatisticalDetector?: boolean;
   statisticalDetectorParameters?: {
     durationBaseline: number;
+    end: string;
+    start: string;
     transaction: string;
   };
 };
@@ -440,9 +442,11 @@ const makeFetchStatisticalDetectorTagsQueryKey = ({
   environment,
   statisticalDetectorParameters,
 }: FetchIssueTagsParameters): ApiQueryKey => {
-  const {transaction, durationBaseline} = statisticalDetectorParameters ?? {
+  const {transaction, durationBaseline, start, end} = statisticalDetectorParameters ?? {
     transaction: '',
     durationBaseline: 0,
+    start: undefined,
+    end: undefined,
   };
   return [
     `/organizations/${orgSlug}/events-facets/`,
@@ -452,6 +456,8 @@ const makeFetchStatisticalDetectorTagsQueryKey = ({
         transaction,
         includeAll: true,
         query: getSampleEventQuery({transaction, durationBaseline, addUpperBound: false}),
+        start,
+        end,
       },
     },
   ];

+ 36 - 27
static/app/components/group/tagFacets/index.tsx

@@ -73,36 +73,39 @@ export function TAGS_FORMATTER(tagsData: Record<string, GroupTagResponseItem>) {
   return transformedTagsData;
 }
 
+export function sumTagFacetsForTopValues(tag: Tag) {
+  return {
+    ...tag,
+    name: tag.key,
+    totalValues: tag.topValues.reduce((acc, {count}) => acc + count, 0),
+    topValues: tag.topValues.map(({name, count}) => ({
+      key: tag.key,
+      name,
+      value: name,
+      count,
+
+      // These values aren't displayed in the sidebar
+      firstSeen: '',
+      lastSeen: '',
+    })),
+  };
+}
+
 // Statistical detector issues need to use a Discover query
 // which means we need to massage the values to fit the component API
 function transformTagFacetDataToGroupTagResponseItems(
   tagFacetData: Record<string, Tag>
 ): Record<string, GroupTagResponseItem> {
   const keyedResponse = {};
-  Object.keys(tagFacetData).forEach(tagKey => {
-    if (tagKey === 'transaction') {
-      // Statistical detectors are scoped to a single transaction so
-      // this tag doesn't add anything to the user experience
-      return;
-    }
 
-    const tagData = tagFacetData[tagKey];
-    keyedResponse[tagKey] = {
-      ...tagData,
-      name: tagData.key,
-      totalValues: tagData.topValues.reduce((acc, {count}) => acc + count, 0),
-      topValues: tagData.topValues.map(({name, count}) => ({
-        key: tagData.key,
-        name,
-        value: name,
-        count,
-
-        // These values aren't displayed in the sidebar
-        firstSeen: '',
-        lastSeen: '',
-      })),
-    };
-  });
+  // Statistical detectors are scoped to a single transaction so
+  // the filter out transaction since the tag is not helpful in the UI
+  Object.keys(tagFacetData)
+    .filter(tagKey => tagKey !== 'transaction')
+    .forEach(tagKey => {
+      const tagData = tagFacetData[tagKey];
+      keyedResponse[tagKey] = sumTagFacetsForTopValues(tagData);
+    });
 
   return keyedResponse;
 }
@@ -130,15 +133,21 @@ export default function TagFacets({
 }: Props) {
   const organization = useOrganization();
 
+  const {transaction, aggregateRange2, breakpoint, requestEnd} =
+    event?.occurrence?.evidenceData ?? {};
   const {isLoading, isError, data, refetch} = useFetchIssueTagsForDetailsPage({
     groupId,
     orgSlug: organization.slug,
     environment: environments,
     isStatisticalDetector,
-    statisticalDetectorParameters: {
-      transaction: event?.occurrence?.evidenceData?.transaction,
-      durationBaseline: event?.occurrence?.evidenceData?.aggregateRange2,
-    },
+    statisticalDetectorParameters: breakpoint
+      ? {
+          transaction,
+          durationBaseline: aggregateRange2,
+          start: new Date(breakpoint * 1000).toISOString(),
+          end: new Date(requestEnd * 1000).toISOString(),
+        }
+      : undefined,
   });
 
   const tagsData = useMemo(() => {

+ 48 - 3
static/app/views/issueDetails/groupDetails.tsx

@@ -23,7 +23,14 @@ import {TabPanels, Tabs} from 'sentry/components/tabs';
 import {t} from 'sentry/locale';
 import GroupStore from 'sentry/stores/groupStore';
 import {space} from 'sentry/styles/space';
-import {Group, GroupStatus, IssueCategory, Organization, Project} from 'sentry/types';
+import {
+  Group,
+  GroupStatus,
+  IssueCategory,
+  IssueType,
+  Organization,
+  Project,
+} from 'sentry/types';
 import {Event} from 'sentry/types/event';
 import {defined} from 'sentry/utils';
 import {trackAnalytics} from 'sentry/utils/analytics';
@@ -747,6 +754,9 @@ function GroupDetailsContent({
 
 function GroupDetailsPageContent(props: GroupDetailsProps & FetchGroupDetailsState) {
   const projectSlug = props.group?.project?.slug;
+  const api = useApi();
+  const organization = useOrganization();
+  const [injectedEvent, setInjectedEvent] = useState(null);
   const {
     projects,
     initiallyLoaded: projectsLoaded,
@@ -770,6 +780,28 @@ function GroupDetailsPageContent(props: GroupDetailsProps & FetchGroupDetailsSta
     }
   }, [props.group, project, projects, projectsLoaded]);
 
+  useEffect(() => {
+    const fetchLatestEvent = async () => {
+      const event = await api.requestPromise(
+        `/organizations/${organization.slug}/issues/${props.group?.id}/events/latest/`
+      );
+      setInjectedEvent(event);
+    };
+    if (
+      props.group?.issueType === IssueType.PERFORMANCE_DURATION_REGRESSION &&
+      !defined(props.event)
+    ) {
+      fetchLatestEvent();
+    }
+  }, [
+    api,
+    organization.slug,
+    props.event,
+    props.group,
+    props.group?.id,
+    props.group?.issueType,
+  ]);
+
   if (props.error) {
     return (
       <GroupDetailsContentError errorType={props.errorType} onRetry={props.refetchData} />
@@ -786,12 +818,25 @@ function GroupDetailsPageContent(props: GroupDetailsProps & FetchGroupDetailsSta
     );
   }
 
-  if (!projectsLoaded || !projectWithFallback || !props.group) {
+  const isRegressionIssue =
+    props.group?.issueType === IssueType.PERFORMANCE_DURATION_REGRESSION;
+  const regressionIssueLoaded = defined(injectedEvent ?? props.event);
+  if (
+    !projectsLoaded ||
+    !projectWithFallback ||
+    !props.group ||
+    (isRegressionIssue && !regressionIssueLoaded)
+  ) {
     return <LoadingIndicator />;
   }
 
   return (
-    <GroupDetailsContent {...props} project={projectWithFallback} group={props.group} />
+    <GroupDetailsContent
+      {...props}
+      project={projectWithFallback}
+      group={props.group}
+      event={props.event ?? injectedEvent}
+    />
   );
 }
 

+ 55 - 3
static/app/views/issueDetails/groupTags.tsx

@@ -6,7 +6,9 @@ import {Alert} from 'sentry/components/alert';
 import Count from 'sentry/components/count';
 import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
 import {DeviceName} from 'sentry/components/deviceName';
+import {getSampleEventQuery} from 'sentry/components/events/eventStatisticalDetector/eventComparison/eventDisplay';
 import GlobalSelectionLink from 'sentry/components/globalSelectionLink';
+import {sumTagFacetsForTopValues} from 'sentry/components/group/tagFacets';
 import * as Layout from 'sentry/components/layouts/thirds';
 import ExternalLink from 'sentry/components/links/externalLink';
 import Link from 'sentry/components/links/link';
@@ -16,7 +18,7 @@ import PanelBody from 'sentry/components/panels/panelBody';
 import Version from 'sentry/components/version';
 import {tct} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
-import {Group, Organization, TagWithTopValues} from 'sentry/types';
+import {Event, Group, IssueType, Organization, TagWithTopValues} from 'sentry/types';
 import {percent} from 'sentry/utils';
 import withOrganization from 'sentry/utils/withOrganization';
 
@@ -25,6 +27,7 @@ type Props = DeprecatedAsyncComponent['props'] & {
   environments: string[];
   group: Group;
   organization: Organization;
+  event?: Event;
 } & RouteComponentProps<{}, {}>;
 
 type State = DeprecatedAsyncComponent['state'] & {
@@ -40,7 +43,42 @@ class GroupTags extends DeprecatedAsyncComponent<Props, State> {
   }
 
   getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
-    const {group, environments, organization} = this.props;
+    const {group, environments, organization, event} = this.props;
+
+    if (
+      organization.features.includes('performance-duration-regression-visible') &&
+      group.issueType === IssueType.PERFORMANCE_DURATION_REGRESSION
+    ) {
+      if (!event) {
+        // We need the event for its occurrence data to get the timestamps
+        // for querying
+        return [];
+      }
+
+      const {transaction, aggregateRange2, breakpoint, requestEnd} =
+        event.occurrence?.evidenceData ?? {};
+      return [
+        [
+          'tagFacets',
+          `/organizations/${organization.slug}/events-facets/`,
+          {
+            query: {
+              environment: environments,
+              transaction,
+              includeAll: true,
+              query: getSampleEventQuery({
+                transaction,
+                durationBaseline: aggregateRange2,
+                addUpperBound: false,
+              }),
+              start: new Date(breakpoint * 1000).toISOString(),
+              end: new Date(requestEnd * 1000).toISOString(),
+            },
+          },
+        ],
+      ];
+    }
+
     return [
       [
         'tagList',
@@ -53,11 +91,25 @@ class GroupTags extends DeprecatedAsyncComponent<Props, State> {
   }
 
   componentDidUpdate(prevProps: Props) {
-    if (!isEqual(prevProps.environments, this.props.environments)) {
+    if (
+      !isEqual(prevProps.environments, this.props.environments) ||
+      !isEqual(prevProps.event, this.props.event)
+    ) {
       this.remountComponent();
     }
   }
 
+  onRequestSuccess({stateKey, data}: {data: any; stateKey: string}): void {
+    if (stateKey === 'tagFacets') {
+      this.setState({
+        tagList: data
+          .filter(({key}) => key !== 'transaction')
+          .map(sumTagFacetsForTopValues),
+        tagFacets: undefined,
+      });
+    }
+  }
+
   renderTags() {
     const {baseUrl, location} = this.props;
     const {tagList} = this.state;

+ 2 - 0
static/app/views/issueDetails/utils.tsx

@@ -163,6 +163,8 @@ export const useFetchIssueTagsForDetailsPage = (
     isStatisticalDetector?: boolean;
     statisticalDetectorParameters?: {
       durationBaseline: number;
+      end: string;
+      start: string;
       transaction: string;
     };
   },