Browse Source

feat(toolbar): add issues related to release & implement analytics provider (#75194)

- add a section to the releases panel with the latest issues for that
release
- also create an analytics provider, which helps re-use the issue list
item component, as well as simplify some of the analytics
tracking/reduce copy-paste
- will need to follow up with updating the corresponding amplitude
dashboards
- closes https://github.com/getsentry/sentry/issues/74564



https://github.com/user-attachments/assets/25e43dfe-e8e4-4aa9-bd29-9a439023e862




With no issues:
<img width="353" alt="SCR-20240729-lwce"
src="https://github.com/user-attachments/assets/c25c8295-4415-4687-8695-dc7d72f70907">
Michelle Zhang 7 months ago
parent
commit
c6effcc162

+ 43 - 52
static/app/components/devtoolbar/components/alerts/alertsPanel.tsx

@@ -2,6 +2,7 @@ import {css} from '@emotion/react';
 
 
 import ActorAvatar from 'sentry/components/avatar/actorAvatar';
 import ActorAvatar from 'sentry/components/avatar/actorAvatar';
 import AlertBadge from 'sentry/components/badge/alertBadge';
 import AlertBadge from 'sentry/components/badge/alertBadge';
+import AnalyticsProvider from 'sentry/components/devtoolbar/components/analyticsProvider';
 import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import Placeholder from 'sentry/components/placeholder';
 import Placeholder from 'sentry/components/placeholder';
 import TextOverflow from 'sentry/components/textOverflow';
 import TextOverflow from 'sentry/components/textOverflow';
@@ -33,7 +34,7 @@ import useTeams from '../teams/useTeams';
 import useInfiniteAlertsList from './useInfiniteAlertsList';
 import useInfiniteAlertsList from './useInfiniteAlertsList';
 
 
 export default function AlertsPanel() {
 export default function AlertsPanel() {
-  const {projectId, projectSlug, projectPlatform, trackAnalytics} = useConfiguration();
+  const {projectId, projectSlug, projectPlatform} = useConfiguration();
   const queryResult = useInfiniteAlertsList();
   const queryResult = useInfiniteAlertsList();
 
 
   const estimateSize = 84;
   const estimateSize = 84;
@@ -41,40 +42,34 @@ export default function AlertsPanel() {
 
 
   return (
   return (
     <PanelLayout title="Alerts">
     <PanelLayout title="Alerts">
-      <div css={[smallCss, panelSectionCss, panelInsetContentCss]}>
-        <span css={[resetFlexRowCss, {gap: 'var(--space50)'}]}>
-          Active alerts in{' '}
-          <SentryAppLink
-            to={{url: `/projects/${projectSlug}/`}}
-            onClick={() => {
-              trackAnalytics?.({
-                eventKey: `devtoolbar.alerts-list.header.click`,
-                eventName: `devtoolbar: Click alert-list header`,
-              });
-            }}
-          >
-            <div
-              css={[
-                resetFlexRowCss,
-                {display: 'inline-flex', gap: 'var(--space50)', alignItems: 'center'},
-              ]}
-            >
-              <ProjectBadge
-                css={css({'&& img': {boxShadow: 'none'}})}
-                project={{
-                  slug: projectSlug,
-                  id: projectId,
-                  platform: projectPlatform as PlatformKey,
-                }}
-                avatarSize={16}
-                hideName
-                avatarProps={{hasTooltip: false}}
-              />
-              {projectSlug}
-            </div>
-          </SentryAppLink>
-        </span>
-      </div>
+      <AnalyticsProvider nameVal="header" keyVal="header">
+        <div css={[smallCss, panelSectionCss, panelInsetContentCss]}>
+          <span css={[resetFlexRowCss, {gap: 'var(--space50)'}]}>
+            Active alerts in{' '}
+            <SentryAppLink to={{url: `/projects/${projectSlug}/`}}>
+              <div
+                css={[
+                  resetFlexRowCss,
+                  {display: 'inline-flex', gap: 'var(--space50)', alignItems: 'center'},
+                ]}
+              >
+                <ProjectBadge
+                  css={css({'&& img': {boxShadow: 'none'}})}
+                  project={{
+                    slug: projectSlug,
+                    id: projectId,
+                    platform: projectPlatform as PlatformKey,
+                  }}
+                  avatarSize={16}
+                  hideName
+                  avatarProps={{hasTooltip: false}}
+                />
+                {projectSlug}
+              </div>
+            </SentryAppLink>
+          </span>
+        </div>
+      </AnalyticsProvider>
 
 
       <div css={resetFlexColumnCss}>
       <div css={resetFlexColumnCss}>
         <InfiniteListState
         <InfiniteListState
@@ -109,7 +104,7 @@ export default function AlertsPanel() {
 }
 }
 
 
 function AlertListItem({item}: {item: Incident}) {
 function AlertListItem({item}: {item: Incident}) {
-  const {organizationSlug, trackAnalytics} = useConfiguration();
+  const {organizationSlug} = useConfiguration();
 
 
   const ownerId = item.alertRule.owner?.split(':').at(1);
   const ownerId = item.alertRule.owner?.split(':').at(1);
 
 
@@ -154,22 +149,18 @@ function AlertListItem({item}: {item: Incident}) {
         <TimeSince date={item.dateStarted} unitStyle="extraShort" />
         <TimeSince date={item.dateStarted} unitStyle="extraShort" />
       </div>
       </div>
 
 
-      <TextOverflow css={smallCss} style={{gridArea: 'name'}}>
-        <SentryAppLink
-          to={{
-            url: alertDetailsLink({slug: organizationSlug} as Organization, item),
-            query: {alert: item.identifier},
-          }}
-          onClick={() => {
-            trackAnalytics?.({
-              eventKey: `devtoolbar.alert-list.item.click`,
-              eventName: `devtoolbar: Click alert-list item`,
-            });
-          }}
-        >
-          <strong>{item.title}</strong>
-        </SentryAppLink>
-      </TextOverflow>
+      <AnalyticsProvider nameVal="item" keyVal="item">
+        <TextOverflow css={smallCss} style={{gridArea: 'name'}}>
+          <SentryAppLink
+            to={{
+              url: alertDetailsLink({slug: organizationSlug} as Organization, item),
+              query: {alert: item.identifier},
+            }}
+          >
+            <strong>{item.title}</strong>
+          </SentryAppLink>
+        </TextOverflow>
+      </AnalyticsProvider>
 
 
       <div css={smallCss} style={{gridArea: 'message'}}>
       <div css={smallCss} style={{gridArea: 'message'}}>
         <ActivatedMetricAlertRuleStatus rule={rule} />
         <ActivatedMetricAlertRuleStatus rule={rule} />

+ 25 - 0
static/app/components/devtoolbar/components/analyticsProvider.tsx

@@ -0,0 +1,25 @@
+import {createContext, type ReactNode, useContext} from 'react';
+
+export const AnalyticsContext = createContext<{eventKey: string; eventName: string}>({
+  eventName: 'devtoolbar',
+  eventKey: 'devtoolbar',
+});
+
+export default function AnalyticsProvider({
+  children,
+  keyVal,
+  nameVal,
+}: {
+  children: ReactNode;
+  keyVal: string;
+  nameVal: string;
+}) {
+  const {eventKey, eventName} = useContext(AnalyticsContext);
+  return (
+    <AnalyticsContext.Provider
+      value={{eventName: eventName + ' ' + nameVal, eventKey: eventKey + '.' + keyVal}}
+    >
+      {children}
+    </AnalyticsContext.Provider>
+  );
+}

+ 4 - 1
static/app/components/devtoolbar/components/app.tsx

@@ -1,6 +1,7 @@
 import {Fragment, Suspense} from 'react';
 import {Fragment, Suspense} from 'react';
 import {Global} from '@emotion/react';
 import {Global} from '@emotion/react';
 
 
+import AnalyticsProvider from 'sentry/components/devtoolbar/components/analyticsProvider';
 import LoadingTriangle from 'sentry/components/loadingTriangle';
 import LoadingTriangle from 'sentry/components/loadingTriangle';
 import {useSessionStorage} from 'sentry/utils/useSessionStorage';
 import {useSessionStorage} from 'sentry/utils/useSessionStorage';
 
 
@@ -33,7 +34,9 @@ export default function App() {
       <div css={[fixedContainerBaseCss, placement.fixedContainer.css, {visibility}]}>
       <div css={[fixedContainerBaseCss, placement.fixedContainer.css, {visibility}]}>
         {isDisabled ? null : (
         {isDisabled ? null : (
           <Fragment>
           <Fragment>
-            <Navigation setIsDisabled={setIsDisabled} />
+            <AnalyticsProvider nameVal="nav" keyVal="nav">
+              <Navigation setIsDisabled={setIsDisabled} />
+            </AnalyticsProvider>
             <Suspense fallback={<LoadingPanel />}>
             <Suspense fallback={<LoadingPanel />}>
               <PanelRouter />
               <PanelRouter />
             </Suspense>
             </Suspense>

+ 10 - 13
static/app/components/devtoolbar/components/featureFlags/featureFlagsPanel.tsx

@@ -1,5 +1,6 @@
 import {useRef, useState} from 'react';
 import {useRef, useState} from 'react';
 
 
+import AnalyticsProvider from 'sentry/components/devtoolbar/components/analyticsProvider';
 import useEnabledFeatureFlags from 'sentry/components/devtoolbar/components/featureFlags/useEnabledFeatureFlags';
 import useEnabledFeatureFlags from 'sentry/components/devtoolbar/components/featureFlags/useEnabledFeatureFlags';
 import {inlineLinkCss} from 'sentry/components/devtoolbar/styles/link';
 import {inlineLinkCss} from 'sentry/components/devtoolbar/styles/link';
 import EmptyStateWarning from 'sentry/components/emptyStateWarning';
 import EmptyStateWarning from 'sentry/components/emptyStateWarning';
@@ -15,7 +16,7 @@ import PanelLayout from '../panelLayout';
 
 
 export default function FeatureFlagsPanel() {
 export default function FeatureFlagsPanel() {
   const featureFlags = useEnabledFeatureFlags();
   const featureFlags = useEnabledFeatureFlags();
-  const {organizationSlug, featureFlagTemplateUrl, trackAnalytics} = useConfiguration();
+  const {organizationSlug, featureFlagTemplateUrl} = useConfiguration();
   const [searchTerm, setSearchTerm] = useState('');
   const [searchTerm, setSearchTerm] = useState('');
   const searchInput = useRef<HTMLInputElement>(null);
   const searchInput = useRef<HTMLInputElement>(null);
 
 
@@ -58,18 +59,14 @@ export default function FeatureFlagsPanel() {
             return (
             return (
               <Cell key={flag} css={[panelInsetContentCss, {alignItems: 'flex-start'}]}>
               <Cell key={flag} css={[panelInsetContentCss, {alignItems: 'flex-start'}]}>
                 {featureFlagTemplateUrl?.(flag) ? (
                 {featureFlagTemplateUrl?.(flag) ? (
-                  <ExternalLink
-                    css={[smallCss, inlineLinkCss]}
-                    href={featureFlagTemplateUrl(flag)}
-                    onClick={() => {
-                      trackAnalytics?.({
-                        eventKey: `devtoolbar.feature-flag-list.item.click`,
-                        eventName: `devtoolbar: Click feature-flag-list item`,
-                      });
-                    }}
-                  >
-                    {flag}
-                  </ExternalLink>
+                  <AnalyticsProvider nameVal="item" keyVal="item">
+                    <ExternalLink
+                      css={[smallCss, inlineLinkCss]}
+                      href={featureFlagTemplateUrl(flag)}
+                    >
+                      {flag}
+                    </ExternalLink>
+                  </AnalyticsProvider>
                 ) : (
                 ) : (
                   <span>{flag}</span>
                   <span>{flag}</span>
                 )}
                 )}

+ 58 - 61
static/app/components/devtoolbar/components/feedback/feedbackPanel.tsx

@@ -2,6 +2,7 @@ import {useMemo} from 'react';
 import {css} from '@emotion/react';
 import {css} from '@emotion/react';
 
 
 import ActorAvatar from 'sentry/components/avatar/actorAvatar';
 import ActorAvatar from 'sentry/components/avatar/actorAvatar';
+import AnalyticsProvider from 'sentry/components/devtoolbar/components/analyticsProvider';
 import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import Placeholder from 'sentry/components/placeholder';
 import Placeholder from 'sentry/components/placeholder';
 import TextOverflow from 'sentry/components/textOverflow';
 import TextOverflow from 'sentry/components/textOverflow';
@@ -109,7 +110,7 @@ export default function FeedbackPanel() {
 }
 }
 
 
 function FeedbackListItem({item}: {item: FeedbackIssueListItem}) {
 function FeedbackListItem({item}: {item: FeedbackIssueListItem}) {
-  const {projectSlug, projectId, trackAnalytics} = useConfiguration();
+  const {projectSlug, projectId} = useConfiguration();
   const {feedbackHasReplay} = useReplayCountForFeedbacks();
   const {feedbackHasReplay} = useReplayCountForFeedbacks();
 
 
   const hasReplayId = feedbackHasReplay(item.id);
   const hasReplayId = feedbackHasReplay(item.id);
@@ -120,71 +121,67 @@ function FeedbackListItem({item}: {item: FeedbackIssueListItem}) {
   const hasComments = item.numComments > 0;
   const hasComments = item.numComments > 0;
 
 
   return (
   return (
-    <div css={listItemGridCss}>
-      <TextOverflow css={smallCss} style={{gridArea: 'name'}}>
-        <SentryAppLink
-          to={{
-            url: '/feedback/',
-            query: {project: projectId, feedbackSlug: `${projectSlug}:${item.id}`},
-          }}
-          onClick={() => {
-            trackAnalytics?.({
-              eventKey: `devtoolbar.feedback-list.item.click`,
-              eventName: `devtoolbar: Click feedback-list item`,
-            });
-          }}
-        >
-          <strong>
-            {item.metadata.name ?? item.metadata.contact_email ?? 'Anonymous User'}
-          </strong>
-        </SentryAppLink>
-      </TextOverflow>
-
-      <div
-        css={[gridFlexEndCss, xSmallCss]}
-        style={{gridArea: 'time', color: 'var(--gray300)'}}
-      >
-        <TimeSince date={item.firstSeen} unitStyle="extraShort" />
-      </div>
-
-      <div style={{gridArea: 'message'}}>
-        <TextOverflow css={[smallCss, textOverflowTwoLinesCss]}>
-          {item.metadata.message}
+    <AnalyticsProvider nameVal="item" keyVal="item">
+      <div css={listItemGridCss}>
+        <TextOverflow css={smallCss} style={{gridArea: 'name'}}>
+          <SentryAppLink
+            to={{
+              url: '/feedback/',
+              query: {project: projectId, feedbackSlug: `${projectSlug}:${item.id}`},
+            }}
+          >
+            <strong>
+              {item.metadata.name ?? item.metadata.contact_email ?? 'Anonymous User'}
+            </strong>
+          </SentryAppLink>
         </TextOverflow>
         </TextOverflow>
-      </div>
 
 
-      <div css={[badgeWithLabelCss, xSmallCss]} style={{gridArea: 'owner'}}>
-        {item.project ? (
-          <ProjectBadge
-            css={css({'&& img': {boxShadow: 'none'}})}
-            project={item.project}
-            avatarSize={16}
-            hideName
-            avatarProps={{hasTooltip: false}}
-          />
-        ) : null}
-        <TextOverflow>{item.shortId}</TextOverflow>
-      </div>
-
-      <div css={gridFlexEndCss} style={{gridArea: 'icons'}}>
-        {/* IssueTrackingSignals could have some refactoring so it doesn't
+        <div
+          css={[gridFlexEndCss, xSmallCss]}
+          style={{gridArea: 'time', color: 'var(--gray300)'}}
+        >
+          <TimeSince date={item.firstSeen} unitStyle="extraShort" />
+        </div>
+
+        <div style={{gridArea: 'message'}}>
+          <TextOverflow css={[smallCss, textOverflowTwoLinesCss]}>
+            {item.metadata.message}
+          </TextOverflow>
+        </div>
+
+        <div css={[badgeWithLabelCss, xSmallCss]} style={{gridArea: 'owner'}}>
+          {item.project ? (
+            <ProjectBadge
+              css={css({'&& img': {boxShadow: 'none'}})}
+              project={item.project}
+              avatarSize={16}
+              hideName
+              avatarProps={{hasTooltip: false}}
+            />
+          ) : null}
+          <TextOverflow>{item.shortId}</TextOverflow>
+        </div>
+
+        <div css={gridFlexEndCss} style={{gridArea: 'icons'}}>
+          {/* IssueTrackingSignals could have some refactoring so it doesn't
             depend on useOrganization, and so the filenames match up better with
             depend on useOrganization, and so the filenames match up better with
             the exported functions */}
             the exported functions */}
-        {/* <IssueTrackingSignals group={item as unknown as Group} /> */}
-
-        {hasComments ? <IconChat size="sm" /> : null}
-        {isFatal ? <IconFatal size="xs" color="red400" /> : null}
-        {hasReplayId ? <IconPlay size="xs" /> : null}
-        {hasAttachments ? <IconImage size="xs" /> : null}
-        {item.assignedTo ? (
-          <ActorAvatar
-            actor={item.assignedTo}
-            size={16}
-            tooltipOptions={{containerDisplayMode: 'flex'}}
-          />
-        ) : null}
+          {/* <IssueTrackingSignals group={item as unknown as Group} /> */}
+
+          {hasComments ? <IconChat size="sm" /> : null}
+          {isFatal ? <IconFatal size="xs" color="red400" /> : null}
+          {hasReplayId ? <IconPlay size="xs" /> : null}
+          {hasAttachments ? <IconImage size="xs" /> : null}
+          {item.assignedTo ? (
+            <ActorAvatar
+              actor={item.assignedTo}
+              size={16}
+              tooltipOptions={{containerDisplayMode: 'flex'}}
+            />
+          ) : null}
+        </div>
       </div>
       </div>
-    </div>
+    </AnalyticsProvider>
   );
   );
 }
 }
 
 

+ 82 - 0
static/app/components/devtoolbar/components/issueListItem.tsx

@@ -0,0 +1,82 @@
+import {css} from '@emotion/react';
+
+import ActorAvatar from 'sentry/components/avatar/actorAvatar';
+import AnalyticsProvider from 'sentry/components/devtoolbar/components/analyticsProvider';
+import SentryAppLink from 'sentry/components/devtoolbar/components/sentryAppLink';
+import useConfiguration from 'sentry/components/devtoolbar/hooks/useConfiguration';
+import {
+  badgeWithLabelCss,
+  gridFlexEndCss,
+  listItemGridCss,
+} from 'sentry/components/devtoolbar/styles/listItem';
+import {smallCss, xSmallCss} from 'sentry/components/devtoolbar/styles/typography';
+import TimesTag from 'sentry/components/group/inboxBadges/timesTag';
+import ProjectBadge from 'sentry/components/idBadge/projectBadge';
+import TextOverflow from 'sentry/components/textOverflow';
+import TimeSince from 'sentry/components/timeSince';
+import type {Group} from 'sentry/types/group';
+
+export default function IssueListItem({item}: {item: Group}) {
+  const {projectId} = useConfiguration();
+
+  return (
+    <AnalyticsProvider keyVal="issue-list.item" nameVal="issue list item">
+      <div css={listItemGridCss}>
+        <TextOverflow
+          css={[badgeWithLabelCss, smallCss, 'display: block']}
+          style={{gridArea: 'name', fontWeight: item.hasSeen ? 'bold' : 400}}
+        >
+          <SentryAppLink
+            to={{
+              url: `/issues/${item.id}/`,
+              query: {project: projectId},
+            }}
+          >
+            <strong>{item.metadata.type ?? '<unknown>'}</strong>
+          </SentryAppLink>
+        </TextOverflow>
+
+        <div
+          css={[gridFlexEndCss, xSmallCss]}
+          style={{gridArea: 'time', color: 'var(--gray300)'}}
+        >
+          <TimeSince date={item.firstSeen} unitStyle="extraShort" />
+        </div>
+
+        <div style={{gridArea: 'message'}}>
+          <TextOverflow css={[smallCss]}>{item.metadata.value}</TextOverflow>
+        </div>
+
+        <div css={[badgeWithLabelCss, xSmallCss]} style={{gridArea: 'owner'}}>
+          <ProjectBadge
+            css={css({'&& img': {boxShadow: 'none'}})}
+            project={item.project}
+            avatarSize={16}
+            hideName
+            avatarProps={{hasTooltip: false}}
+          />
+          <TextOverflow>{item.shortId}</TextOverflow>
+        </div>
+
+        <div css={gridFlexEndCss} style={{gridArea: 'icons'}}>
+          {item.lifetime || item.firstSeen || item.lastSeen ? (
+            <div className="flex-row">
+              <TimesTag
+                lastSeen={item.lifetime?.lastSeen || item.lastSeen}
+                firstSeen={item.lifetime?.firstSeen || item.firstSeen}
+              />
+            </div>
+          ) : null}
+
+          {item.assignedTo ? (
+            <ActorAvatar
+              actor={item.assignedTo}
+              size={16}
+              tooltipOptions={{containerDisplayMode: 'flex'}}
+            />
+          ) : null}
+        </div>
+      </div>
+    </AnalyticsProvider>
+  );
+}

+ 4 - 87
static/app/components/devtoolbar/components/issues/issuesPanel.tsx

@@ -1,32 +1,18 @@
-import {css} from '@emotion/react';
-
-import ActorAvatar from 'sentry/components/avatar/actorAvatar';
-import TimesTag from 'sentry/components/group/inboxBadges/timesTag';
-import ProjectBadge from 'sentry/components/idBadge/projectBadge';
+import IssueListItem from 'sentry/components/devtoolbar/components/issueListItem';
 import Placeholder from 'sentry/components/placeholder';
 import Placeholder from 'sentry/components/placeholder';
-import TextOverflow from 'sentry/components/textOverflow';
-import TimeSince from 'sentry/components/timeSince';
-import type {Group} from 'sentry/types/group';
 
 
-import useConfiguration from '../../hooks/useConfiguration';
 import useCurrentTransactionName from '../../hooks/useCurrentTransactionName';
 import useCurrentTransactionName from '../../hooks/useCurrentTransactionName';
-import {
-  badgeWithLabelCss,
-  gridFlexEndCss,
-  listItemGridCss,
-  listItemPlaceholderWrapperCss,
-} from '../../styles/listItem';
+import {listItemPlaceholderWrapperCss} from '../../styles/listItem';
 import {panelInsetContentCss, panelSectionCss} from '../../styles/panel';
 import {panelInsetContentCss, panelSectionCss} from '../../styles/panel';
 import {resetFlexColumnCss} from '../../styles/reset';
 import {resetFlexColumnCss} from '../../styles/reset';
-import {smallCss, xSmallCss} from '../../styles/typography';
+import {smallCss} from '../../styles/typography';
 import InfiniteListItems from '../infiniteListItems';
 import InfiniteListItems from '../infiniteListItems';
 import InfiniteListState from '../infiniteListState';
 import InfiniteListState from '../infiniteListState';
 import PanelLayout from '../panelLayout';
 import PanelLayout from '../panelLayout';
-import SentryAppLink from '../sentryAppLink';
 
 
 import useInfiniteIssuesList from './useInfiniteIssuesList';
 import useInfiniteIssuesList from './useInfiniteIssuesList';
 
 
-export default function FeedbackPanel() {
+export default function IssuesPanel() {
   const transactionName = useCurrentTransactionName();
   const transactionName = useCurrentTransactionName();
   const queryResult = useInfiniteIssuesList({
   const queryResult = useInfiniteIssuesList({
     query: `url:*${transactionName}`,
     query: `url:*${transactionName}`,
@@ -74,72 +60,3 @@ export default function FeedbackPanel() {
     </PanelLayout>
     </PanelLayout>
   );
   );
 }
 }
-
-function IssueListItem({item}: {item: Group}) {
-  const {projectSlug, projectId, trackAnalytics} = useConfiguration();
-
-  return (
-    <div css={listItemGridCss}>
-      <TextOverflow
-        css={[badgeWithLabelCss, smallCss]}
-        style={{gridArea: 'name', fontWeight: item.hasSeen ? 'bold' : 400}}
-      >
-        <SentryAppLink
-          to={{
-            url: `/issues/${item.id}/`,
-            query: {project: projectId, feedbackSlug: `${projectSlug}:${item.id}`},
-          }}
-          onClick={() => {
-            trackAnalytics?.({
-              eventKey: `devtoolbar.issue-list.item.click`,
-              eventName: `devtoolbar: Click issue-list item`,
-            });
-          }}
-        >
-          <strong>{item.metadata.type ?? '<unknown>'}</strong>
-        </SentryAppLink>
-      </TextOverflow>
-
-      <div
-        css={[gridFlexEndCss, xSmallCss]}
-        style={{gridArea: 'time', color: 'var(--gray300)'}}
-      >
-        <TimeSince date={item.firstSeen} unitStyle="extraShort" />
-      </div>
-
-      <div style={{gridArea: 'message'}}>
-        <TextOverflow css={[smallCss]}>{item.metadata.value}</TextOverflow>
-      </div>
-
-      <div css={[badgeWithLabelCss, xSmallCss]} style={{gridArea: 'owner'}}>
-        <ProjectBadge
-          css={css({'&& img': {boxShadow: 'none'}})}
-          project={item.project}
-          avatarSize={16}
-          hideName
-          avatarProps={{hasTooltip: false}}
-        />
-        <TextOverflow>{item.shortId}</TextOverflow>
-      </div>
-
-      <div css={gridFlexEndCss} style={{gridArea: 'icons'}}>
-        {item.lifetime || item.firstSeen || item.lastSeen ? (
-          <div className="flex-row">
-            <TimesTag
-              lastSeen={item.lifetime?.lastSeen || item.lastSeen}
-              firstSeen={item.lifetime?.firstSeen || item.firstSeen}
-            />
-          </div>
-        ) : null}
-
-        {item.assignedTo ? (
-          <ActorAvatar
-            actor={item.assignedTo}
-            size={16}
-            tooltipOptions={{containerDisplayMode: 'flex'}}
-          />
-        ) : null}
-      </div>
-    </div>
-  );
-}

+ 7 - 5
static/app/components/devtoolbar/components/navigation.tsx

@@ -1,6 +1,8 @@
-import type {ReactNode} from 'react';
+import {type ReactNode, useContext} from 'react';
 
 
+import {AnalyticsContext} from 'sentry/components/devtoolbar/components/analyticsProvider';
 import SessionStatusBadge from 'sentry/components/devtoolbar/components/releases/sessionStatusBadge';
 import SessionStatusBadge from 'sentry/components/devtoolbar/components/releases/sessionStatusBadge';
+import useConfiguration from 'sentry/components/devtoolbar/hooks/useConfiguration';
 import {
 import {
   IconClose,
   IconClose,
   IconFlag,
   IconFlag,
@@ -10,7 +12,6 @@ import {
   IconSiren,
   IconSiren,
 } from 'sentry/icons';
 } from 'sentry/icons';
 
 
-import useConfiguration from '../hooks/useConfiguration';
 import usePlacementCss from '../hooks/usePlacementCss';
 import usePlacementCss from '../hooks/usePlacementCss';
 import useToolbarRoute from '../hooks/useToolbarRoute';
 import useToolbarRoute from '../hooks/useToolbarRoute';
 import {navigationCss} from '../styles/navigation';
 import {navigationCss} from '../styles/navigation';
@@ -28,6 +29,7 @@ export default function Navigation({
   const placement = usePlacementCss();
   const placement = usePlacementCss();
 
 
   const {state: route} = useToolbarRoute();
   const {state: route} = useToolbarRoute();
+  const {eventName, eventKey} = useContext(AnalyticsContext);
   const isRouteActive = !!route.activePanel;
   const isRouteActive = !!route.activePanel;
 
 
   return (
   return (
@@ -37,11 +39,11 @@ export default function Navigation({
     >
     >
       <IconButton
       <IconButton
         onClick={() => {
         onClick={() => {
-          setIsDisabled(true);
           trackAnalytics?.({
           trackAnalytics?.({
-            eventKey: `devtoolbar.nav.hide.click`,
-            eventName: `devtoolbar: Hide devtoolbar`,
+            eventKey: eventKey + '.hide.click',
+            eventName: eventName + ' hide devtoolbar clicked',
           });
           });
+          setIsDisabled(true);
         }}
         }}
         title="Hide for this session"
         title="Hide for this session"
         icon={<IconClose />}
         icon={<IconClose />}

+ 1 - 1
static/app/components/devtoolbar/components/panelLayout.tsx

@@ -4,8 +4,8 @@ import {panelCss, panelHeadingCss, panelSectionCss} from '../styles/panel';
 import {resetDialogCss, resetFlexColumnCss} from '../styles/reset';
 import {resetDialogCss, resetFlexColumnCss} from '../styles/reset';
 
 
 interface Props {
 interface Props {
-  title: string;
   children?: React.ReactNode;
   children?: React.ReactNode;
+  title?: string;
   titleLeft?: React.ReactNode;
   titleLeft?: React.ReactNode;
   titleRight?: React.ReactNode;
   titleRight?: React.ReactNode;
 }
 }

+ 27 - 5
static/app/components/devtoolbar/components/panelRouter.tsx

@@ -1,5 +1,7 @@
 import {lazy} from 'react';
 import {lazy} from 'react';
 
 
+import AnalyticsProvider from 'sentry/components/devtoolbar/components/analyticsProvider';
+
 import useToolbarRoute from '../hooks/useToolbarRoute';
 import useToolbarRoute from '../hooks/useToolbarRoute';
 
 
 const PanelAlerts = lazy(() => import('./alerts/alertsPanel'));
 const PanelAlerts = lazy(() => import('./alerts/alertsPanel'));
@@ -13,15 +15,35 @@ export default function PanelRouter() {
 
 
   switch (state.activePanel) {
   switch (state.activePanel) {
     case 'alerts':
     case 'alerts':
-      return <PanelAlerts />;
+      return (
+        <AnalyticsProvider keyVal="alerts-panel" nameVal="Alerts panel">
+          <PanelAlerts />
+        </AnalyticsProvider>
+      );
     case 'feedback':
     case 'feedback':
-      return <PanelFeedback />;
+      return (
+        <AnalyticsProvider keyVal="feedback-panel" nameVal="Feedback panel">
+          <PanelFeedback />
+        </AnalyticsProvider>
+      );
     case 'issues':
     case 'issues':
-      return <PanelIssues />;
+      return (
+        <AnalyticsProvider keyVal="issues-panel" nameVal="Issues panel">
+          <PanelIssues />
+        </AnalyticsProvider>
+      );
     case 'featureFlags':
     case 'featureFlags':
-      return <PanelFeatureFlags />;
+      return (
+        <AnalyticsProvider keyVal="feature-flags-panel" nameVal="Feature Flags panel">
+          <PanelFeatureFlags />
+        </AnalyticsProvider>
+      );
     case 'releases':
     case 'releases':
-      return <PanelReleases />;
+      return (
+        <AnalyticsProvider keyVal="releases-panel" nameVal="Releases panel">
+          <PanelReleases />
+        </AnalyticsProvider>
+      );
     default:
     default:
       return null;
       return null;
   }
   }

Some files were not shown because too many files changed in this diff