Browse Source

ref(replay): Extract the ReplaySection to its own file and update logic (#63484)

The logic inside the ternary's was getting a little difficult to follow.
So I broke it out into it's own file with return statements to make it
easier to read.

The basic text output looks like this when rendered "No replay captured"
in this case:

![SCR-20240118-nvpu](https://github.com/getsentry/sentry/assets/187460/bab85498-ec58-41fc-91d5-c992fda4fab9)

And the CTA version still looking good:

![SCR-20240118-nwdy](https://github.com/getsentry/sentry/assets/187460/2d9254f2-0a71-475d-b2a7-b28e666bfab7)


Also, i made a truth-table to track down everything that needs to
happen. The column on the right matches with each `return` statement in
the code (counting from top of function), and is 1:1 with the outcome we
want to see. Blank spaces means use the same value as the cell above,
and `-` means the value doesn't matter to the return value. Three return
values have `<-` in there... these states should be impossible, but
that's the value that the code will return (in the other cases, i
decided first and then wrote code to match).

| platformSupported | replayId | hasReplayId | isFetchingSentOneReplay |
hasSentOneReplay | RETURN VALUE | Step |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| false | - | - | - | - | Sorry / Upsell | 1 |
| true | falsey | undefined | true | - | Loading | 3 |
|  |  |  | false | false | CTA | 4 |
|  |  |  | false | true | *None captured | 6 |
|  |  | false | true | - | Loading | 3 |
|  |  |  | false | false | CTA | 4 |
|  |  |  | false | true | *None Captured | 6 |
|  |  | true | true | - | Loading <- | 3 |
|  |  |  | false | false | CTA <- | 4 |
|  |  |  | false | true | *None Captured | 6 |
|  | truthy | undefined | - | - | Loading | 3 |
|  |  |  | - | - | Loading | 3 |
|  |  |  | - | - | Loading | 3 |
|  |  | false | true | - | Loading | 3 |
|  |  |  | false | false | CTA | 4 |
|  |  |  | false | true | *Not Found | 5 |
|  |  | true | - | - | Show preview | 2 |
|  |  |  | - | - | Show preview | 2 |
|  |  |  | - | - | Show preview | 2 |

Fixes https://github.com/getsentry/sentry/issues/61426
Ryan Albrecht 1 year ago
parent
commit
da74f6c805

+ 3 - 49
static/app/components/events/eventReplay/replayPreview.tsx

@@ -3,17 +3,14 @@ import styled from '@emotion/styled';
 
 import {Alert} from 'sentry/components/alert';
 import {LinkButton} from 'sentry/components/button';
-import ExternalLink from 'sentry/components/links/externalLink';
-import Link from 'sentry/components/links/link';
-import List from 'sentry/components/list';
-import ListItem from 'sentry/components/list/listItem';
 import Placeholder from 'sentry/components/placeholder';
 import {Flex} from 'sentry/components/profiling/flex';
+import MissingReplayAlert from 'sentry/components/replays/alerts/missingReplayAlert';
 import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
 import ReplayPlayer from 'sentry/components/replays/replayPlayer';
 import ReplayProcessingError from 'sentry/components/replays/replayProcessingError';
 import {IconDelete, IconPlay} from 'sentry/icons';
-import {t, tct} from 'sentry/locale';
+import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
 import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
@@ -93,50 +90,7 @@ function ReplayPreview({
   }
 
   if (fetchError) {
-    const reasons = [
-      t('The replay is still processing'),
-      tct(
-        'The replay was rate-limited and could not be accepted. [link:View the stats page] for more information.',
-        {
-          link: <Link to={`/organizations/${orgSlug}/stats/?dataCategory=replays`} />,
-        }
-      ),
-      t('The replay has been deleted by a member in your organization.'),
-      t('There were network errors and the replay was not saved.'),
-      tct('[link:Read the docs] to understand why.', {
-        link: (
-          <ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/#error-linking" />
-        ),
-      }),
-    ];
-
-    return (
-      <Alert
-        type="info"
-        showIcon
-        data-test-id="replay-error"
-        trailingItems={
-          <LinkButton
-            external
-            href="https://docs.sentry.io/platforms/javascript/session-replay/#error-linking"
-            size="xs"
-          >
-            {t('Read Docs')}
-          </LinkButton>
-        }
-      >
-        <p>
-          {t(
-            'The replay for this event cannot be found. This could be due to these reasons:'
-          )}
-        </p>
-        <List symbol="bullet">
-          {reasons.map((reason, i) => (
-            <ListItem key={i}>{reason}</ListItem>
-          ))}
-        </List>
-      </Alert>
-    );
+    return <MissingReplayAlert orgSlug={orgSlug} />;
   }
 
   if (fetching || !replayRecord) {

+ 8 - 27
static/app/components/feedback/feedbackItem/feedbackItem.tsx

@@ -6,22 +6,18 @@ import CrashReportSection from 'sentry/components/feedback/feedbackItem/crashRep
 import FeedbackActivitySection from 'sentry/components/feedback/feedbackItem/feedbackActivitySection';
 import FeedbackItemHeader from 'sentry/components/feedback/feedbackItem/feedbackItemHeader';
 import Section from 'sentry/components/feedback/feedbackItem/feedbackItemSection';
+import FeedbackReplay from 'sentry/components/feedback/feedbackItem/feedbackReplay';
 import FeedbackViewers from 'sentry/components/feedback/feedbackItem/feedbackViewers';
-import ReplayInlineCTAPanel from 'sentry/components/feedback/feedbackItem/replayInlineCTAPanel';
-import ReplaySection from 'sentry/components/feedback/feedbackItem/replaySection';
 import TagsSection from 'sentry/components/feedback/feedbackItem/tagsSection';
 import PanelItem from 'sentry/components/panels/panelItem';
 import {Flex} from 'sentry/components/profiling/flex';
 import QuestionTooltip from 'sentry/components/questionTooltip';
 import TextCopyInput from 'sentry/components/textCopyInput';
-import {replayPlatforms} from 'sentry/data/platformCategories';
 import {IconChat, IconFire, IconLink, IconPlay, IconTag} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {Event} from 'sentry/types';
 import type {FeedbackIssue} from 'sentry/utils/feedback/types';
-import useReplayCountForFeedbacks from 'sentry/utils/replayCount/useReplayCountForFeedbacks';
-import {useHaveSelectedProjectsSentAnyReplayEvents} from 'sentry/utils/replays/hooks/useReplayOnboarding';
 import useOrganization from 'sentry/utils/useOrganization';
 
 interface Props {
@@ -32,16 +28,9 @@ interface Props {
 
 export default function FeedbackItem({feedbackItem, eventData, tags}: Props) {
   const organization = useOrganization();
-  const {feedbackHasReplay} = useReplayCountForFeedbacks();
-  const hasReplayId = feedbackHasReplay(feedbackItem.id);
-
   const url = eventData?.tags.find(tag => tag.key === 'url');
-  const replayId = eventData?.contexts?.feedback?.replay_id;
   const crashReportId = eventData?.contexts?.feedback?.associated_event_id;
 
-  const {hasSentOneReplay} = useHaveSelectedProjectsSentAnyReplayEvents();
-  const platformSupported = replayPlatforms.includes(feedbackItem.platform);
-
   return (
     <Fragment>
       <FeedbackItemHeader eventData={eventData} feedbackItem={feedbackItem} />
@@ -78,21 +67,13 @@ export default function FeedbackItem({feedbackItem, eventData, tags}: Props) {
           </Section>
         )}
 
-        {hasReplayId && replayId ? (
-          <Section icon={<IconPlay size="xs" />} title={t('Linked Replay')}>
-            <ErrorBoundary mini>
-              <ReplaySection
-                eventTimestampMs={new Date(feedbackItem.firstSeen).getTime()}
-                organization={organization}
-                replayId={replayId}
-              />
-            </ErrorBoundary>
-          </Section>
-        ) : hasSentOneReplay || !platformSupported ? null : (
-          <Section icon={<IconPlay size="xs" />} title={t('Linked Replay')}>
-            <ReplayInlineCTAPanel />
-          </Section>
-        )}
+        <Section icon={<IconPlay size="xs" />} title={t('Linked Replay')}>
+          <FeedbackReplay
+            eventData={eventData}
+            feedbackItem={feedbackItem}
+            organization={organization}
+          />
+        </Section>
 
         <Section icon={<IconTag size="xs" />} title={t('Tags')}>
           <TagsSection tags={tags} />

+ 65 - 0
static/app/components/feedback/feedbackItem/feedbackReplay.tsx

@@ -0,0 +1,65 @@
+import {Fragment} from 'react';
+
+import ErrorBoundary from 'sentry/components/errorBoundary';
+import ReplayInlineCTAPanel from 'sentry/components/feedback/feedbackItem/replayInlineCTAPanel';
+import ReplaySection from 'sentry/components/feedback/feedbackItem/replaySection';
+import Placeholder from 'sentry/components/placeholder';
+import MissingReplayAlert from 'sentry/components/replays/alerts/missingReplayAlert';
+import ReplayUnsupportedAlert from 'sentry/components/replays/alerts/replayUnsupportedAlert';
+import {replayPlatforms} from 'sentry/data/platformCategories';
+import {t} from 'sentry/locale';
+import type {Event, Organization} from 'sentry/types';
+import type {FeedbackIssue} from 'sentry/utils/feedback/types';
+import useReplayCountForFeedbacks from 'sentry/utils/replayCount/useReplayCountForFeedbacks';
+import {useHaveSelectedProjectsSentAnyReplayEvents} from 'sentry/utils/replays/hooks/useReplayOnboarding';
+
+interface Props {
+  eventData: Event | undefined;
+  feedbackItem: FeedbackIssue;
+  organization: Organization;
+}
+
+export default function FeedbackReplay({eventData, feedbackItem, organization}: Props) {
+  const {feedbackHasReplay} = useReplayCountForFeedbacks();
+  const hasReplayId = feedbackHasReplay(feedbackItem.id);
+
+  const replayId = eventData?.contexts?.feedback?.replay_id;
+  const {hasSentOneReplay, fetching: isFetchingSentOneReplay} =
+    useHaveSelectedProjectsSentAnyReplayEvents();
+  const platformSupported = replayPlatforms.includes(feedbackItem.platform);
+
+  if (!platformSupported) {
+    return (
+      <ReplayUnsupportedAlert
+        primaryAction="create"
+        projectSlug={feedbackItem.project.slug}
+      />
+    );
+  }
+
+  if (replayId && hasReplayId) {
+    return (
+      <ErrorBoundary mini>
+        <ReplaySection
+          eventTimestampMs={new Date(feedbackItem.firstSeen).getTime()}
+          organization={organization}
+          replayId={replayId}
+        />
+      </ErrorBoundary>
+    );
+  }
+
+  if ((replayId && hasReplayId === undefined) || isFetchingSentOneReplay) {
+    return <Placeholder />;
+  }
+
+  if (!hasSentOneReplay) {
+    return <ReplayInlineCTAPanel />;
+  }
+
+  if (replayId) {
+    return <MissingReplayAlert orgSlug={organization.slug} />;
+  }
+
+  return <Fragment>{t('No replay captured')}</Fragment>;
+}

+ 6 - 0
static/app/components/replays/alerts/missingReplayAlert.stories.tsx

@@ -0,0 +1,6 @@
+import MissingReplayAlert from 'sentry/components/replays/alerts/missingReplayAlert';
+import storyBook from 'sentry/stories/storyBook';
+
+export default storyBook(MissingReplayAlert, story => {
+  story('All', () => <MissingReplayAlert orgSlug="MY-ORG" />);
+});

+ 58 - 0
static/app/components/replays/alerts/missingReplayAlert.tsx

@@ -0,0 +1,58 @@
+import {Alert} from 'sentry/components/alert';
+import {LinkButton} from 'sentry/components/button';
+import ExternalLink from 'sentry/components/links/externalLink';
+import Link from 'sentry/components/links/link';
+import List from 'sentry/components/list';
+import ListItem from 'sentry/components/list/listItem';
+import {t, tct} from 'sentry/locale';
+
+interface Props {
+  orgSlug: string;
+}
+
+export default function MissingReplayAlert({orgSlug}: Props) {
+  const reasons = [
+    t('The replay is still processing'),
+    tct(
+      'The replay was rate-limited and could not be accepted. [link:View the stats page] for more information.',
+      {
+        link: <Link to={`/organizations/${orgSlug}/stats/?dataCategory=replays`} />,
+      }
+    ),
+    t('The replay has been deleted by a member in your organization.'),
+    t('There were network errors and the replay was not saved.'),
+    tct('[link:Read the docs] to understand why.', {
+      link: (
+        <ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/#error-linking" />
+      ),
+    }),
+  ];
+
+  return (
+    <Alert
+      type="info"
+      showIcon
+      data-test-id="replay-error"
+      trailingItems={
+        <LinkButton
+          external
+          href="https://docs.sentry.io/platforms/javascript/session-replay/#error-linking"
+          size="xs"
+        >
+          {t('Read Docs')}
+        </LinkButton>
+      }
+    >
+      <p>
+        {t(
+          'The replay for this event cannot be found. This could be due to these reasons:'
+        )}
+      </p>
+      <List symbol="bullet">
+        {reasons.map((reason, i) => (
+          <ListItem key={i}>{reason}</ListItem>
+        ))}
+      </List>
+    </Alert>
+  );
+}

+ 16 - 0
static/app/components/replays/alerts/replayUnsupportedAlert.stories.tsx

@@ -0,0 +1,16 @@
+import ReplayUnsupportedAlert from 'sentry/components/replays/alerts/replayUnsupportedAlert';
+import Matrix from 'sentry/components/stories/matrix';
+import storyBook from 'sentry/stories/storyBook';
+
+export default storyBook(ReplayUnsupportedAlert, story => {
+  story('All', () => (
+    <Matrix
+      propMatrix={{
+        primaryAction: ['create', 'setup'],
+        projectSlug: ['MY-PROJECT'],
+      }}
+      render={ReplayUnsupportedAlert}
+      selectedProps={['primaryAction', 'projectSlug']}
+    />
+  ));
+});

+ 29 - 0
static/app/components/replays/alerts/replayUnsupportedAlert.tsx

@@ -0,0 +1,29 @@
+import Alert from 'sentry/components/alert';
+import ExternalLink from 'sentry/components/links/externalLink';
+import {IconInfo} from 'sentry/icons';
+import {t, tct} from 'sentry/locale';
+
+interface Props {
+  primaryAction: 'create' | 'setup';
+  projectSlug: string;
+}
+
+export default function ReplayUnsupportedAlert({primaryAction, projectSlug}: Props) {
+  const link = (
+    <ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/" />
+  );
+  return (
+    <Alert icon={<IconInfo />}>
+      <strong>{t(`Session Replay isn't available for %s.`, projectSlug)}</strong>{' '}
+      {primaryAction === 'create'
+        ? tct(
+            `Create a project using our [link:Sentry browser SDK package], or equivalent framework SDK.`,
+            {link}
+          )
+        : tct(
+            `Select a project using our [link:Sentry browser SDK package], or equivalent framework SDK.`,
+            {link}
+          )}
+    </Alert>
+  );
+}

+ 5 - 23
static/app/views/replays/list/replayOnboardingPanel.tsx

@@ -4,16 +4,15 @@ import styled from '@emotion/styled';
 import emptyStateImg from 'sentry-images/spot/replays-empty-state.svg';
 
 import Accordion from 'sentry/components/accordion/accordion';
-import Alert from 'sentry/components/alert';
 import {Button} from 'sentry/components/button';
 import ButtonBar from 'sentry/components/buttonBar';
 import HookOrDefault from 'sentry/components/hookOrDefault';
 import ExternalLink from 'sentry/components/links/externalLink';
 import {useProjectCreationAccess} from 'sentry/components/projects/useProjectCreationAccess';
 import QuestionTooltip from 'sentry/components/questionTooltip';
+import ReplayUnsupportedAlert from 'sentry/components/replays/alerts/replayUnsupportedAlert';
 import {Tooltip} from 'sentry/components/tooltip';
 import {replayPlatforms} from 'sentry/data/platformCategories';
-import {IconInfo} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
 import PreferencesStore from 'sentry/stores/preferencesStore';
 import {useLegacyStore} from 'sentry/stores/useLegacyStore';
@@ -95,27 +94,10 @@ export default function ReplayOnboardingPanel() {
     <Fragment>
       <OnboardingAlertHook>
         {hasSelectedProjects && allSelectedProjectsUnsupported && (
-          <Alert icon={<IconInfo />}>
-            {tct(
-              `[projectMsg] [action] a project using our [link], or equivalent framework SDK.`,
-              {
-                action: primaryAction === 'create' ? t('Create') : t('Select'),
-                projectMsg: (
-                  <strong>
-                    {t(
-                      `Session Replay isn't available for project %s.`,
-                      selectedProjects[0].slug
-                    )}
-                  </strong>
-                ),
-                link: (
-                  <ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/">
-                    {t('Sentry browser SDK package')}
-                  </ExternalLink>
-                ),
-              }
-            )}
-          </Alert>
+          <ReplayUnsupportedAlert
+            primaryAction={primaryAction}
+            projectSlug={selectedProjects[0].slug}
+          />
         )}
       </OnboardingAlertHook>
       <ReplayPanel image={<HeroImage src={emptyStateImg} breakpoints={breakpoints} />}>