Browse Source

feat(new-trace): Adding performance setup checklist for only error tr… (#74887)

Org to test:
[link](https://perfquotaorg.dev.getsentry.net:7999/issues/?project=4506836555595776&referrer=sidebar&statsPeriod=14d)
navigate to the trace for issue details.

- We now push projects that are part of the trace to the top of the
checklist.
- Note: Will improve file-structure of the the new trace quality banners
as we add more.

<img width="1501" alt="Screenshot 2024-07-24 at 4 40 35 PM"
src="https://github.com/user-attachments/assets/ddbb536a-c2cf-45c0-8fcf-8c1e5abcb286">

---------

Co-authored-by: Abdullah Khan <abdullahkhan@PG9Y57YDXQ.local>
Abdullah Khan 7 months ago
parent
commit
5f48dd9cf7

+ 33 - 12
static/app/components/performanceOnboarding/sidebar.tsx

@@ -1,5 +1,6 @@
 import {Fragment, useEffect, useMemo, useState} from 'react';
 import styled from '@emotion/styled';
+import qs from 'qs';
 
 import HighlightTopRightPattern from 'sentry-images/pattern/highlight-top-right.svg';
 
@@ -30,6 +31,18 @@ import useProjects from 'sentry/utils/useProjects';
 
 import {filterProjects, generateDocKeys, isPlatformSupported} from './utils';
 
+function decodeProjectIds(projectIds: unknown): string[] | null {
+  if (Array.isArray(projectIds)) {
+    return projectIds;
+  }
+
+  if (typeof projectIds === 'string') {
+    return [projectIds];
+  }
+
+  return null;
+}
+
 function PerformanceOnboardingSidebar(props: CommonSidebarProps) {
   const {currentPanel, collapsed, hidePanel, orientation} = props;
   const isActive = currentPanel === SidebarPanelKey.PERFORMANCE_ONBOARDING;
@@ -45,6 +58,12 @@ function PerformanceOnboardingSidebar(props: CommonSidebarProps) {
   const {projectsWithoutFirstTransactionEvent, projectsForOnboarding} =
     filterProjects(projects);
 
+  const priorityProjectIds: Set<string> | null = useMemo(() => {
+    const queryParams = qs.parse(location.search);
+    const decodedProjectIds = decodeProjectIds(queryParams.project);
+    return decodedProjectIds === null ? null : new Set(decodedProjectIds);
+  }, []);
+
   useEffect(() => {
     if (
       currentProject ||
@@ -54,21 +73,22 @@ function PerformanceOnboardingSidebar(props: CommonSidebarProps) {
     ) {
       return;
     }
-    // Establish current project
 
-    const projectMap: Record<string, Project> = projects.reduce((acc, project) => {
-      acc[project.id] = project;
-      return acc;
-    }, {});
+    // Establish current project
+    if (priorityProjectIds) {
+      const projectMap: Record<string, Project> = projects.reduce((acc, project) => {
+        acc[project.id] = project;
+        return acc;
+      }, {});
 
-    if (selection.projects.length) {
-      const projectSelection = selection.projects.map(
-        projectId => projectMap[String(projectId)]
-      );
+      const priorityProjects: Project[] = [];
+      priorityProjectIds.forEach(projectId => {
+        priorityProjects.push(projectMap[String(projectId)]);
+      });
 
       // Among the project selection, find a project that has performance onboarding docs support, and has not sent
       // a first transaction event.
-      const maybeProject = projectSelection.find(project =>
+      const maybeProject = priorityProjects.find(project =>
         projectsForOnboarding.includes(project)
       );
       if (maybeProject) {
@@ -77,7 +97,7 @@ function PerformanceOnboardingSidebar(props: CommonSidebarProps) {
       }
 
       // Among the project selection, find a project that has not sent a first transaction event
-      const maybeProjectFallback = projectSelection.find(project =>
+      const maybeProjectFallback = priorityProjects.find(project =>
         projectsWithoutFirstTransactionEvent.includes(project)
       );
       if (maybeProjectFallback) {
@@ -102,6 +122,7 @@ function PerformanceOnboardingSidebar(props: CommonSidebarProps) {
     projectsForOnboarding,
     projectsWithoutFirstTransactionEvent,
     currentProject,
+    priorityProjectIds,
   ]);
 
   if (
@@ -129,7 +150,7 @@ function PerformanceOnboardingSidebar(props: CommonSidebarProps) {
         },
       };
 
-      if (currentProject.id === project.id) {
+      if (priorityProjectIds?.has(String(project.id))) {
         acc.unshift(itemProps);
       } else {
         acc.push(itemProps);

+ 4 - 0
static/app/components/sidebar/onboardingStep.tsx

@@ -89,6 +89,10 @@ export const DocumentationWrapper = styled('div')`
     margin-bottom: 0;
   }
 
+  p > code {
+    color: ${p => p.theme.pink300};
+  }
+
   /* Ensures documentation content is placed behind the checkbox */
   z-index: 1;
   position: relative;

+ 6 - 0
static/app/data/platformCategories.tsx

@@ -191,6 +191,12 @@ export const performance: PlatformKey[] = [
 export const withPerformanceOnboarding: Set<PlatformKey> = new Set([
   'javascript',
   'javascript-react',
+  'javascript-nextjs',
+  'python',
+  'python-django',
+  'python-flask',
+  'php',
+  'node',
 ]);
 
 // List of platforms that do not have performance support. We make use of this list in the product to not provide any Performance

+ 6 - 0
static/app/utils/analytics/tracingEventMap.tsx

@@ -6,6 +6,8 @@ export type TracingEventParameters = {
     shape: string;
     trace_duration_seconds: number;
   };
+  'trace.quality.performance_setup.checklist_triggered': {};
+  'trace.quality.performance_setup.learn_more_clicked': {};
   'trace.trace_layout.change': {
     layout: string;
   };
@@ -82,6 +84,10 @@ export const tracingEventMap: Record<TracingEventKey, string | null> = {
   'trace.trace_layout.tab_view': 'Viewed Trace Tab',
   'trace.trace_layout.search_focus': 'Focused Trace Search',
   'trace.trace_layout.reset_zoom': 'Reset Trace Zoom',
+  'trace.quality.performance_setup.checklist_triggered':
+    'Triggered Performance Setup Checklist',
+  'trace.quality.performance_setup.learn_more_clicked':
+    'Clicked Learn More in Performance Setup Banner',
   'trace.trace_layout.view_shortcuts': 'Viewed Trace Shortcuts',
   'trace.trace_warning_type': 'Viewed Trace Warning Type',
   'trace.trace_layout.zoom_to_fill': 'Trace Zoom to Fill',

+ 6 - 0
static/app/views/performance/newTraceDetails/index.tsx

@@ -85,6 +85,7 @@ import {
   DEFAULT_TRACE_VIEW_PREFERENCES,
   loadTraceViewPreferences,
 } from './traceState/tracePreferences';
+import {PerformanceSetupWarning} from './traceWarnings/performanceSetupWarning';
 import {isTraceNode} from './guards';
 import {Trace} from './trace';
 import {TraceMetadataHeader} from './traceMetadataHeader';
@@ -956,6 +957,11 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
 
   return (
     <Fragment>
+      <PerformanceSetupWarning
+        tree={tree}
+        traceSlug={props.traceSlug}
+        organization={organization}
+      />
       <TraceToolbar>
         <TraceSearchInput onTraceSearch={onTraceSearch} organization={organization} />
         <TraceResetZoomButton

+ 13 - 0
static/app/views/performance/newTraceDetails/traceAnalytics.tsx

@@ -83,6 +83,16 @@ const trackResetZoom = (organization: Organization) =>
     organization,
   });
 
+const trackPerformanceSetupChecklistTriggered = (organization: Organization) =>
+  trackAnalytics('trace.quality.performance_setup.checklist_triggered', {
+    organization,
+  });
+
+const trackPerformanceSetupLearnMoreClicked = (organization: Organization) =>
+  trackAnalytics('trace.quality.performance_setup.learn_more_clicked', {
+    organization,
+  });
+
 const trackViewShortcuts = (organization: Organization) =>
   trackAnalytics('trace.trace_layout.view_shortcuts', {
     organization,
@@ -112,6 +122,9 @@ const traceAnalytics = {
   trackResetZoom,
   trackViewShortcuts,
   trackTraceWarningType,
+  // Trace Quality Improvement
+  trackPerformanceSetupChecklistTriggered,
+  trackPerformanceSetupLearnMoreClicked,
 };
 
 export {traceAnalytics};

+ 2 - 0
static/app/views/performance/newTraceDetails/traceModels/traceTree.tsx

@@ -486,6 +486,7 @@ export class TraceTree {
   vital_types: Set<'web' | 'mobile'> = new Set();
   eventsCount: number = 0;
   profiled_events: Set<TraceTreeNode<TraceTree.NodeValue>> = new Set();
+  project_ids: Set<number> = new Set();
 
   private _spanPromises: Map<string, Promise<Event>> = new Map();
   private _list: TraceTreeNode<TraceTree.NodeValue>[] = [];
@@ -549,6 +550,7 @@ export class TraceTree {
 
       node.canFetch = true;
       tree.eventsCount += 1;
+      tree.project_ids.add(node.value.project_id);
 
       if (node.profiles.length > 0) {
         tree.profiled_events.add(node);

+ 1 - 13
static/app/views/performance/newTraceDetails/traceWarnings.tsx

@@ -3,7 +3,7 @@ import * as Sentry from '@sentry/react';
 
 import Alert from 'sentry/components/alert';
 import ExternalLink from 'sentry/components/links/externalLink';
-import {t, tct} from 'sentry/locale';
+import {t} from 'sentry/locale';
 import useOrganization from 'sentry/utils/useOrganization';
 import {traceAnalytics} from 'sentry/views/performance/newTraceDetails/traceAnalytics';
 
@@ -54,18 +54,6 @@ export function TraceWarnings({type}: TraceWarningsProps) {
         </Alert>
       );
     case TraceType.ONLY_ERRORS:
-      return (
-        <Alert type="info" showIcon>
-          {tct(
-            "The good news is we know these errors are related to each other. The bad news is that we can't tell you more than that. If you haven't already, [tracingLink: configure performance monitoring for your SDKs] to learn more about service interactions.",
-            {
-              tracingLink: (
-                <ExternalLink href="https://docs.sentry.io/product/performance/getting-started/" />
-              ),
-            }
-          )}
-        </Alert>
-      );
     case TraceType.ONE_ROOT:
     case TraceType.EMPTY_TRACE:
       return null;

+ 251 - 0
static/app/views/performance/newTraceDetails/traceWarnings/performanceSetupWarning.tsx

@@ -0,0 +1,251 @@
+import {useEffect, useMemo} from 'react';
+import {browserHistory} from 'react-router';
+import styled from '@emotion/styled';
+import qs from 'qs';
+
+import connectDotsImg from 'sentry-images/spot/performance-connect-dots.svg';
+
+import {Alert} from 'sentry/components/alert';
+import {Button} from 'sentry/components/button';
+import {DropdownMenu} from 'sentry/components/dropdownMenu';
+import ExternalLink from 'sentry/components/links/externalLink';
+import {SidebarPanelKey} from 'sentry/components/sidebar/types';
+import {withPerformanceOnboarding} from 'sentry/data/platformCategories';
+import {IconClose} from 'sentry/icons';
+import {t, tct} from 'sentry/locale';
+import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
+import {space} from 'sentry/styles/space';
+import type {Organization} from 'sentry/types/organization';
+import type {Project} from 'sentry/types/project';
+import useDismissAlert from 'sentry/utils/useDismissAlert';
+import useProjects from 'sentry/utils/useProjects';
+
+import {traceAnalytics} from '../traceAnalytics';
+import type {TraceTree} from '../traceModels/traceTree';
+import {TraceType} from '../traceType';
+
+type OnlyOrphanErrorWarningsProps = {
+  organization: Organization;
+  traceSlug: string | undefined;
+  tree: TraceTree;
+};
+
+function filterProjects(projects: Project[], tree: TraceTree) {
+  const projectsWithNoPerformance: Project[] = [];
+  const projectsWithOnboardingChecklist: Project[] = [];
+
+  for (const project of projects) {
+    if (tree.project_ids.has(Number(project.id))) {
+      if (!project.firstTransactionEvent) {
+        projectsWithNoPerformance.push(project);
+        if (project.platform && withPerformanceOnboarding.has(project.platform)) {
+          projectsWithOnboardingChecklist.push(project);
+        }
+      }
+    }
+  }
+
+  return {projectsWithNoPerformance, projectsWithOnboardingChecklist};
+}
+
+export function PerformanceSetupWarning({
+  traceSlug,
+  tree,
+  organization,
+}: OnlyOrphanErrorWarningsProps) {
+  const {projects} = useProjects();
+
+  const {projectsWithNoPerformance, projectsWithOnboardingChecklist} = useMemo(() => {
+    return filterProjects(projects, tree);
+  }, [projects, tree]);
+
+  const LOCAL_STORAGE_KEY = `${traceSlug}:performance-orphan-error-onboarding-banner-hide`;
+
+  useEffect(() => {
+    if (
+      projectsWithOnboardingChecklist.length > 0 &&
+      location.hash === '#performance-sidequest'
+    ) {
+      SidebarPanelStore.activatePanel(SidebarPanelKey.PERFORMANCE_ONBOARDING);
+    }
+  }, [projectsWithOnboardingChecklist]);
+
+  const {dismiss: snooze, isDismissed: isSnoozed} = useDismissAlert({
+    key: LOCAL_STORAGE_KEY,
+    expirationDays: 7,
+  });
+
+  const {dismiss, isDismissed} = useDismissAlert({
+    key: LOCAL_STORAGE_KEY,
+    expirationDays: 365,
+  });
+
+  if (
+    tree.type !== 'trace' ||
+    tree.shape !== TraceType.ONLY_ERRORS ||
+    projectsWithNoPerformance.length === 0
+  ) {
+    return null;
+  }
+
+  if (projectsWithOnboardingChecklist.length === 0) {
+    return (
+      <Alert type="info" showIcon>
+        {tct(
+          "Some of the projects associated with this trace don't support performance monitoring. To learn more about how to setup performance monitoring, visit our [documentation].",
+          {
+            documentationLink: (
+              <ExternalLink href="https://docs.sentry.io/product/performance/getting-started/">
+                {t('documentation')}
+              </ExternalLink>
+            ),
+          }
+        )}
+      </Alert>
+    );
+  }
+
+  if (isDismissed || isSnoozed) {
+    return null;
+  }
+
+  return (
+    <BannerWrapper>
+      <ActionsWrapper>
+        <BannerTitle>{t('Your setup is incomplete')}</BannerTitle>
+        <BannerDescription>
+          {t(
+            "Want to know why this string of errors happened? Configure performance monitoring to get a full picture of what's going on."
+          )}
+        </BannerDescription>
+        <ButtonsWrapper>
+          <ActionButton>
+            <Button
+              priority="primary"
+              onClick={event => {
+                event.preventDefault();
+                traceAnalytics.trackPerformanceSetupChecklistTriggered(organization);
+                browserHistory.replace({
+                  pathname: location.pathname,
+                  query: {
+                    ...qs.parse(location.search),
+                    project: projectsWithOnboardingChecklist.map(project => project.id),
+                  },
+                  hash: '#performance-sidequest',
+                });
+                SidebarPanelStore.activatePanel(SidebarPanelKey.PERFORMANCE_ONBOARDING);
+              }}
+            >
+              {t('Start Checklist')}
+            </Button>
+          </ActionButton>
+          <ActionButton>
+            <Button
+              onClick={() =>
+                traceAnalytics.trackPerformanceSetupLearnMoreClicked(organization)
+              }
+              href="https://docs.sentry.io/product/performance/"
+              external
+            >
+              {t('Learn More')}
+            </Button>
+          </ActionButton>
+        </ButtonsWrapper>
+      </ActionsWrapper>
+      {<Background image={connectDotsImg} />}
+      <CloseDropdownMenu
+        position="bottom-end"
+        triggerProps={{
+          showChevron: false,
+          borderless: true,
+          icon: <IconClose color="subText" />,
+        }}
+        size="xs"
+        items={[
+          {
+            key: 'dismiss',
+            label: t('Dismiss'),
+            onAction: () => {
+              dismiss();
+            },
+          },
+          {
+            key: 'snooze',
+            label: t('Snooze'),
+            onAction: () => {
+              snooze();
+            },
+          },
+        ]}
+      />
+    </BannerWrapper>
+  );
+}
+
+const BannerWrapper = styled('div')`
+  position: relative;
+  border: 1px solid ${p => p.theme.border};
+  border-radius: ${p => p.theme.borderRadius};
+  padding: ${space(2)} ${space(3)};
+  margin-bottom: ${space(2)};
+  background: linear-gradient(
+    90deg,
+    ${p => p.theme.backgroundSecondary}00 0%,
+    ${p => p.theme.backgroundSecondary}FF 70%,
+    ${p => p.theme.backgroundSecondary}FF 100%
+  );
+  container-type: inline-size;
+`;
+
+const ActionsWrapper = styled('div')`
+  max-width: 50%;
+`;
+
+const ButtonsWrapper = styled('div')`
+  display: flex;
+  align-items: center;
+  gap: ${space(0.5)};
+`;
+
+const BannerTitle = styled('div')`
+  font-size: ${p => p.theme.fontSizeExtraLarge};
+  margin-bottom: ${space(1)};
+  font-weight: ${p => p.theme.fontWeightBold};
+`;
+
+const BannerDescription = styled('div')`
+  margin-bottom: ${space(1.5)};
+`;
+
+const CloseDropdownMenu = styled(DropdownMenu)`
+  position: absolute;
+  display: block;
+  top: ${space(1)};
+  right: ${space(1)};
+  color: ${p => p.theme.white};
+  cursor: pointer;
+  z-index: 1;
+`;
+
+const Background = styled('div')<{image: any}>`
+  display: flex;
+  justify-self: flex-end;
+  position: absolute;
+  top: 14px;
+  right: 15px;
+  height: 81%;
+  width: 100%;
+  max-width: 413px;
+  background-image: url(${p => p.image});
+  background-repeat: no-repeat;
+  background-size: contain;
+
+  @container (max-width: 840px) {
+    display: none;
+  }
+`;
+
+const ActionButton = styled('div')`
+  display: flex;
+  gap: ${space(1)};
+`;