Browse Source

fix: Protect against feedbacks ingested without project association (#74218)

Fixes some feedback types to make them align with reality a little more.

This comment is still true:
```
// Typescript is a lie. `project` might be missing if feedback didn't go
// through the new ingest pipeline. This can happen in new self-hosted upgrades.
```
So what I did is bake that into the feedback item type, and fix any
downstream callsites, so that the feedback item page can render in this
case. I also wrapped the FeedbackItem component in an ErrorBoundary, so
if there are problems the rest of the page should appear.

Fixes https://github.com/getsentry/sentry/issues/67412
Fixes https://github.com/getsentry/sentry/issues/70136
Ryan Albrecht 7 months ago
parent
commit
ee95b68195

+ 1 - 1
static/app/components/deprecatedAssigneeSelectorDropdown.tsx

@@ -223,7 +223,7 @@ export class DeprecatedAssigneeSelectorDropdown extends Component<
       return [];
       return [];
     }
     }
 
 
-    const teams = ProjectsStore.getBySlug(group.project.slug)?.teams ?? [];
+    const teams = ProjectsStore.getBySlug(group.project?.slug)?.teams ?? [];
     return teams
     return teams
       .sort((a, b) => a.slug.localeCompare(b.slug))
       .sort((a, b) => a.slug.localeCompare(b.slug))
       .map(team => ({
       .map(team => ({

+ 9 - 7
static/app/components/devtoolbar/components/feedback/feedbackPanel.tsx

@@ -143,13 +143,15 @@ function FeedbackListItem({item}: {item: FeedbackIssueListItem}) {
       </div>
       </div>
 
 
       <div css={[badgeWithLabelCss, xSmallCss]} style={{gridArea: 'owner'}}>
       <div css={[badgeWithLabelCss, xSmallCss]} style={{gridArea: 'owner'}}>
-        <ProjectBadge
-          css={css({'&& img': {boxShadow: 'none'}})}
-          project={item.project}
-          avatarSize={16}
-          hideName
-          avatarProps={{hasTooltip: false}}
-        />
+        {item.project ? (
+          <ProjectBadge
+            css={css({'&& img': {boxShadow: 'none'}})}
+            project={item.project}
+            avatarSize={16}
+            hideName
+            avatarProps={{hasTooltip: false}}
+          />
+        ) : null}
         <TextOverflow>{item.shortId}</TextOverflow>
         <TextOverflow>{item.shortId}</TextOverflow>
       </div>
       </div>
 
 

+ 2 - 1
static/app/components/feedback/feedbackItem/feedbackActions.tsx

@@ -11,6 +11,7 @@ import {IconEllipsis} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {space} from 'sentry/styles/space';
 import type {Event} from 'sentry/types/event';
 import type {Event} from 'sentry/types/event';
+import type {Group} from 'sentry/types/group';
 import {defined} from 'sentry/utils';
 import {defined} from 'sentry/utils';
 import type {FeedbackIssue} from 'sentry/utils/feedback/types';
 import type {FeedbackIssue} from 'sentry/utils/feedback/types';
 
 
@@ -33,7 +34,7 @@ export default function FeedbackActions({
     <Flex gap={space(1)} align="center" className={className} style={style}>
     <Flex gap={space(1)} align="center" className={className} style={style}>
       <ErrorBoundary mini>
       <ErrorBoundary mini>
         <FeedbackAssignedTo
         <FeedbackAssignedTo
-          feedbackIssue={feedbackItem}
+          feedbackIssue={feedbackItem as any as Group}
           feedbackEvent={eventData}
           feedbackEvent={eventData}
           showActorName={['medium', 'large'].includes(size)}
           showActorName={['medium', 'large'].includes(size)}
         />
         />

+ 9 - 5
static/app/components/feedback/feedbackItem/feedbackActivitySection.tsx

@@ -4,16 +4,20 @@ import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicato
 import useFeedbackCache from 'sentry/components/feedback/useFeedbackCache';
 import useFeedbackCache from 'sentry/components/feedback/useFeedbackCache';
 import useMutateActivity from 'sentry/components/feedback/useMutateActivity';
 import useMutateActivity from 'sentry/components/feedback/useMutateActivity';
 import {t} from 'sentry/locale';
 import {t} from 'sentry/locale';
-import type {Group, GroupActivity, GroupActivityNote, User} from 'sentry/types';
 import type {NoteType} from 'sentry/types/alerts';
 import type {NoteType} from 'sentry/types/alerts';
-import {GroupActivityType} from 'sentry/types/group';
-import type {FeedbackIssue} from 'sentry/utils/feedback/types';
+import {
+  type Group,
+  type GroupActivity,
+  type GroupActivityNote,
+  GroupActivityType,
+} from 'sentry/types/group';
+import type {User} from 'sentry/types/user';
 import {uniqueId} from 'sentry/utils/guid';
 import {uniqueId} from 'sentry/utils/guid';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
 import ActivitySection from 'sentry/views/issueDetails/activitySection';
 import ActivitySection from 'sentry/views/issueDetails/activitySection';
 
 
 type Props = {
 type Props = {
-  feedbackItem: FeedbackIssue;
+  feedbackItem: Group;
 };
 };
 
 
 function FeedbackActivitySection(props: Props) {
 function FeedbackActivitySection(props: Props) {
@@ -30,7 +34,7 @@ function FeedbackActivitySection(props: Props) {
       invalidateCached([feedbackItem.id]);
       invalidateCached([feedbackItem.id]);
     },
     },
     organization,
     organization,
-    group: feedbackItem as unknown as Group,
+    group: feedbackItem,
   });
   });
 
 
   const deleteOptions = useMemo(() => {
   const deleteOptions = useMemo(() => {

+ 3 - 2
static/app/components/feedback/feedbackItem/feedbackAssignedTo.tsx

@@ -11,14 +11,15 @@ import {getAssignedToDisplayName, getOwnerList} from 'sentry/components/group/as
 import {IconChevron, IconUser} from 'sentry/icons';
 import {IconChevron, IconUser} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {space} from 'sentry/styles/space';
-import type {FeedbackEvent, FeedbackIssue} from 'sentry/utils/feedback/types';
+import type {Group} from 'sentry/types/group';
+import type {FeedbackEvent} from 'sentry/utils/feedback/types';
 import {useApiQuery} from 'sentry/utils/queryClient';
 import {useApiQuery} from 'sentry/utils/queryClient';
 import useApi from 'sentry/utils/useApi';
 import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
 
 
 interface Props {
 interface Props {
   feedbackEvent: FeedbackEvent | undefined;
   feedbackEvent: FeedbackEvent | undefined;
-  feedbackIssue: FeedbackIssue;
+  feedbackIssue: Group;
   showActorName: boolean;
   showActorName: boolean;
 }
 }
 
 

+ 21 - 18
static/app/components/feedback/feedbackItem/feedbackItem.tsx

@@ -18,6 +18,7 @@ import {IconChat, IconFire, IconLink, IconTag} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {space} from 'sentry/styles/space';
 import type {Event} from 'sentry/types/event';
 import type {Event} from 'sentry/types/event';
+import type {Group} from 'sentry/types/group';
 import type {FeedbackIssue} from 'sentry/utils/feedback/types';
 import type {FeedbackIssue} from 'sentry/utils/feedback/types';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
 
 
@@ -76,7 +77,7 @@ export default function FeedbackItem({feedbackItem, eventData, tags}: Props) {
           </Section>
           </Section>
         ) : null}
         ) : null}
 
 
-        {crashReportId && (
+        {crashReportId && feedbackItem.project ? (
           <Section icon={<IconFire size="xs" />} title={t('Linked Error')}>
           <Section icon={<IconFire size="xs" />} title={t('Linked Error')}>
             <ErrorBoundary mini>
             <ErrorBoundary mini>
               <CrashReportSection
               <CrashReportSection
@@ -86,7 +87,7 @@ export default function FeedbackItem({feedbackItem, eventData, tags}: Props) {
               />
               />
             </ErrorBoundary>
             </ErrorBoundary>
           </Section>
           </Section>
-        )}
+        ) : null}
 
 
         <FeedbackReplay
         <FeedbackReplay
           eventData={eventData}
           eventData={eventData}
@@ -98,22 +99,24 @@ export default function FeedbackItem({feedbackItem, eventData, tags}: Props) {
           <TagsSection tags={tags} />
           <TagsSection tags={tags} />
         </Section>
         </Section>
 
 
-        <Section
-          icon={<IconChat size="xs" />}
-          title={
-            <Fragment>
-              {t('Internal Activity')}
-              <QuestionTooltip
-                size="xs"
-                title={t(
-                  'Use this section to post comments that are visible only to your organization. It will also automatically update when someone resolves or assigns the feedback.'
-                )}
-              />
-            </Fragment>
-          }
-        >
-          <FeedbackActivitySection feedbackItem={feedbackItem} />
-        </Section>
+        {feedbackItem.project ? (
+          <Section
+            icon={<IconChat size="xs" />}
+            title={
+              <Fragment>
+                {t('Internal Activity')}
+                <QuestionTooltip
+                  size="xs"
+                  title={t(
+                    'Use this section to post comments that are visible only to your organization. It will also automatically update when someone resolves or assigns the feedback.'
+                  )}
+                />
+              </Fragment>
+            }
+          >
+            <FeedbackActivitySection feedbackItem={feedbackItem as unknown as Group} />
+          </Section>
+        ) : null}
       </OverflowPanelItem>
       </OverflowPanelItem>
     </Fragment>
     </Fragment>
   );
   );

+ 2 - 2
static/app/components/feedback/feedbackItem/feedbackItemHeader.tsx

@@ -51,7 +51,7 @@ export default function FeedbackItemHeader({eventData, feedbackItem}: Props) {
         />
         />
       </Flex>
       </Flex>
 
 
-      {eventData && (
+      {eventData && feedbackItem.project ? (
         <Flex wrap="wrap" justify="flex-start" css={fixIssueLinkSpacing}>
         <Flex wrap="wrap" justify="flex-start" css={fixIssueLinkSpacing}>
           <ErrorBoundary mini>
           <ErrorBoundary mini>
             <IssueTrackingSection
             <IssueTrackingSection
@@ -61,7 +61,7 @@ export default function FeedbackItemHeader({eventData, feedbackItem}: Props) {
             />
             />
           </ErrorBoundary>
           </ErrorBoundary>
         </Flex>
         </Flex>
-      )}
+      ) : null}
     </VerticalSpacing>
     </VerticalSpacing>
   );
   );
 }
 }

+ 8 - 3
static/app/components/feedback/feedbackItem/feedbackItemLoader.tsx

@@ -1,5 +1,6 @@
 import {useEffect} from 'react';
 import {useEffect} from 'react';
 
 
+import ErrorBoundary from 'sentry/components/errorBoundary';
 import FeedbackEmptyDetails from 'sentry/components/feedback/details/feedbackEmptyDetails';
 import FeedbackEmptyDetails from 'sentry/components/feedback/details/feedbackEmptyDetails';
 import FeedbackErrorDetails from 'sentry/components/feedback/details/feedbackErrorDetails';
 import FeedbackErrorDetails from 'sentry/components/feedback/details/feedbackErrorDetails';
 import FeedbackItem from 'sentry/components/feedback/feedbackItem/feedbackItem';
 import FeedbackItem from 'sentry/components/feedback/feedbackItem/feedbackItem';
@@ -37,9 +38,13 @@ export default function FeedbackItemLoader() {
     <Placeholder height="100%" />
     <Placeholder height="100%" />
   ) : issueResult.isError ? (
   ) : issueResult.isError ? (
     <FeedbackErrorDetails error={t('Unable to load feedback')} />
     <FeedbackErrorDetails error={t('Unable to load feedback')} />
-  ) : !issueData ? (
-    <FeedbackEmptyDetails />
+  ) : issueData ? (
+    <ErrorBoundary
+      customComponent={<FeedbackErrorDetails error={t('Unable to load feedback')} />}
+    >
+      <FeedbackItem eventData={eventData} feedbackItem={issueData} tags={tags} />
+    </ErrorBoundary>
   ) : (
   ) : (
-    <FeedbackItem eventData={eventData} feedbackItem={issueData} tags={tags} />
+    <FeedbackEmptyDetails />
   );
   );
 }
 }

+ 1 - 1
static/app/components/feedback/feedbackItem/feedbackReplay.tsx

@@ -29,7 +29,7 @@ export default function FeedbackReplay({eventData, feedbackItem, organization}:
   const {hasSentOneReplay, fetching: isFetchingSentOneReplay} =
   const {hasSentOneReplay, fetching: isFetchingSentOneReplay} =
     useHaveSelectedProjectsSentAnyReplayEvents();
     useHaveSelectedProjectsSentAnyReplayEvents();
   const platformSupported = replayPlatforms.includes(
   const platformSupported = replayPlatforms.includes(
-    feedbackItem.project.platform as PlatformKey
+    feedbackItem.project?.platform as PlatformKey
   );
   );
 
 
   if (replayId && hasReplayId) {
   if (replayId && hasReplayId) {

+ 17 - 9
static/app/components/feedback/feedbackItem/feedbackShortId.tsx

@@ -4,6 +4,7 @@ import styled from '@emotion/styled';
 
 
 import {Flex} from 'sentry/components/container/flex';
 import {Flex} from 'sentry/components/container/flex';
 import {DropdownMenu} from 'sentry/components/dropdownMenu';
 import {DropdownMenu} from 'sentry/components/dropdownMenu';
+import useCurrentFeedbackProject from 'sentry/components/feedback/useCurrentFeedbackProject';
 import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import TextOverflow from 'sentry/components/textOverflow';
 import TextOverflow from 'sentry/components/textOverflow';
 import {IconChevron} from 'sentry/icons';
 import {IconChevron} from 'sentry/icons';
@@ -36,12 +37,17 @@ const hideDropdown = css`
 
 
 export default function FeedbackShortId({className, feedbackItem, style}: Props) {
 export default function FeedbackShortId({className, feedbackItem, style}: Props) {
   const organization = useOrganization();
   const organization = useOrganization();
+  const projectSlug = useCurrentFeedbackProject();
 
 
   const feedbackUrl =
   const feedbackUrl =
     window.location.origin +
     window.location.origin +
-    normalizeUrl(
-      `/organizations/${organization.slug}/feedback/?feedbackSlug=${feedbackItem.project.slug}:${feedbackItem.id}&project=${feedbackItem.project.id}`
-    );
+    normalizeUrl({
+      pathname: `/organizations/${organization.slug}/feedback/`,
+      query: {
+        feedbackSlug: `${projectSlug}:${feedbackItem.id}`,
+        project: feedbackItem.project?.id,
+      },
+    });
 
 
   const {onClick: handleCopyUrl} = useCopyToClipboard({
   const {onClick: handleCopyUrl} = useCopyToClipboard({
     successMessage: t('Copied Feedback URL to clipboard'),
     successMessage: t('Copied Feedback URL to clipboard'),
@@ -62,12 +68,14 @@ export default function FeedbackShortId({className, feedbackItem, style}: Props)
       css={hideDropdown}
       css={hideDropdown}
     >
     >
       <Flex gap={space(0.75)} align="center">
       <Flex gap={space(0.75)} align="center">
-        <ProjectBadge
-          project={feedbackItem.project}
-          avatarSize={16}
-          hideName
-          avatarProps={{hasTooltip: true, tooltip: feedbackItem.project.slug}}
-        />
+        {feedbackItem.project ? (
+          <ProjectBadge
+            project={feedbackItem.project}
+            avatarSize={16}
+            hideName
+            avatarProps={{hasTooltip: true, tooltip: feedbackItem.project.slug}}
+          />
+        ) : null}
         <ShortId>{feedbackItem.shortId}</ShortId>
         <ShortId>{feedbackItem.shortId}</ShortId>
       </Flex>
       </Flex>
       <DropdownMenu
       <DropdownMenu

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