Browse Source

feat(replays): Updated Replay Preview and CTA (#41615)

Update the Replay Preview inside Event Details to match latest designs

| Desc | Img |
| --- | --- |
| CTA button is "Open Replay" | <img width="838" alt="SCR-20221121-ogy"
src="https://user-images.githubusercontent.com/187460/203208705-36be1a59-8b42-4d24-88c3-c9555399b2ee.png">
|
| Blue info message when a replay isn't found (this replaces the red
error from before). It's legit if a replay can't be found, not always an
error. | <img width="837" alt="SCR-20221121-oh2"
src="https://user-images.githubusercontent.com/187460/203208753-66a934bb-6550-4596-82d9-63e4702319e4.png">
|
| New onboarding widget which sides open setup instructions. | <img
width="701" alt="SCR-20221122-dcs"
src="https://user-images.githubusercontent.com/187460/203383320-d6814a81-04c4-40cf-a57f-6094bedc631b.png">
|
| Clicking "Get started" opens this existing config sidebar | <img
width="1404" alt="SCR-20221121-ois"
src="https://user-images.githubusercontent.com/187460/203208944-455c7e32-3fca-486e-a1fc-67f55471a9ba.png">
|

Fixes #40719

Co-authored-by: Dane Grant <danecando@gmail.com>
Ryan Albrecht 2 years ago
parent
commit
95819b95fb

+ 18 - 3
static/app/components/events/eventReplay/index.tsx

@@ -3,21 +3,36 @@ import {useCallback} from 'react';
 import ErrorBoundary from 'sentry/components/errorBoundary';
 import LazyLoad from 'sentry/components/lazyLoad';
 import {Event} from 'sentry/types/event';
+import {useShouldShowOnboarding} from 'sentry/utils/replays/hooks/useReplayOnboarding';
 
 type Props = {
   event: Event;
   orgSlug: string;
   projectSlug: string;
-  replayId: string;
+  replayId: undefined | string;
 };
 
 export default function EventReplay({replayId, orgSlug, projectSlug, event}: Props) {
-  const component = useCallback(() => import('./replayContent'), []);
+  const showPanel = useShouldShowOnboarding();
+  const onboardingPanel = useCallback(() => import('./replayInlineOnboardingPanel'), []);
+  const replayPreview = useCallback(() => import('./replayPreview'), []);
+
+  if (showPanel) {
+    return (
+      <ErrorBoundary mini>
+        <LazyLoad component={onboardingPanel} />
+      </ErrorBoundary>
+    );
+  }
+
+  if (!replayId) {
+    return null;
+  }
 
   return (
     <ErrorBoundary mini>
       <LazyLoad
-        component={component}
+        component={replayPreview}
         replaySlug={`${projectSlug}:${replayId}`}
         orgSlug={orgSlug}
         event={event}

+ 29 - 0
static/app/components/events/eventReplay/replayInlineOnboardingPanel.spec.tsx

@@ -0,0 +1,29 @@
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import localStorage from 'sentry/utils/localStorage';
+
+import ReplayInlineOnboardingPanel from './replayInlineOnboardingPanel';
+
+jest.mock('sentry/utils/localStorage');
+
+const TEN_SECONDS = 10 * 1000;
+
+describe('replayInlineOnboardingPanel', () => {
+  it('should render by default', async () => {
+    const {container} = render(<ReplayInlineOnboardingPanel />);
+    expect(await screen.findByText('Configure Session Replay')).toBeInTheDocument();
+    expect(container).toSnapshot();
+  });
+
+  it('should not render if hideUntil is set', async () => {
+    localStorage.getItem = jest.fn().mockReturnValue(Date.now() + TEN_SECONDS);
+    render(<ReplayInlineOnboardingPanel />);
+    expect(await screen.queryByText('Configure Session Replay')).not.toBeInTheDocument();
+  });
+
+  it('should clear the hideUntil time if it has expired', async () => {
+    localStorage.getItem = jest.fn().mockReturnValue(Date.now() - TEN_SECONDS);
+    render(<ReplayInlineOnboardingPanel />);
+    expect(await screen.findByText('Configure Session Replay')).toBeInTheDocument();
+  });
+});

+ 123 - 0
static/app/components/events/eventReplay/replayInlineOnboardingPanel.tsx

@@ -0,0 +1,123 @@
+import {useState} from 'react';
+import styled from '@emotion/styled';
+
+import replaysInlineOnboarding from 'sentry-images/spot/replays-inline-onboarding.svg';
+
+import Button from 'sentry/components/button';
+import ButtonBar from 'sentry/components/buttonBar';
+import {t} from 'sentry/locale';
+import space from 'sentry/styles/space';
+import localStorage from 'sentry/utils/localStorage';
+import {useReplayOnboardingSidebarPanel} from 'sentry/utils/replays/hooks/useReplayOnboarding';
+
+const LOCAL_STORAGE_KEY = 'replay-preview-onboarding-hide-until';
+const SNOOZE_TIME = 1000 * 60 * 60 * 24 * 7; // 1 week
+const DISMISS_TIME = 1000 * 60 * 60 * 24 * 365; // 1 year
+
+function getHideUntilTime() {
+  return Number(localStorage.getItem(LOCAL_STORAGE_KEY)) || 0;
+}
+
+function setHideUntilTime(offset: number) {
+  localStorage.setItem(LOCAL_STORAGE_KEY, String(Date.now() + offset));
+}
+
+function clearHideUntilTime() {
+  localStorage.removeItem(LOCAL_STORAGE_KEY);
+}
+
+export default function ReplayOnboardingPanel() {
+  const [isHidden, setIsHidden] = useState(() => {
+    const hideUntilTime = getHideUntilTime();
+    if (hideUntilTime && Date.now() < hideUntilTime) {
+      return true;
+    }
+    clearHideUntilTime();
+    return false;
+  });
+  const {activateSidebar} = useReplayOnboardingSidebarPanel();
+
+  if (isHidden) {
+    return null;
+  }
+
+  return (
+    <StyledOnboardingPanel>
+      <div>
+        <Heading>{t('Configure Session Replay')}</Heading>
+        <Content>
+          {t(
+            'Playback your app to identify the root cause of errors and latency issues.'
+          )}
+        </Content>
+        <ButtonList>
+          <Button onClick={activateSidebar} priority="primary" size="sm">
+            {t('Get Started')}
+          </Button>
+          <ButtonBar merged>
+            <Button
+              size="sm"
+              onClick={() => {
+                setHideUntilTime(SNOOZE_TIME);
+                setIsHidden(true);
+              }}
+            >
+              {t('Snooze')}
+            </Button>
+            <Button
+              size="sm"
+              onClick={() => {
+                setHideUntilTime(DISMISS_TIME);
+                setIsHidden(true);
+              }}
+            >
+              {t('Dismiss')}
+            </Button>
+          </ButtonBar>
+        </ButtonList>
+      </div>
+      <Illustration src={replaysInlineOnboarding} width={220} height={112} alt="" />
+    </StyledOnboardingPanel>
+  );
+}
+
+const StyledOnboardingPanel = styled('div')`
+  display: flex;
+  flex-direction: column;
+  max-width: 600px;
+  border: 1px dashed ${p => p.theme.border};
+  border-radius: ${p => p.theme.borderRadius};
+  padding: ${space(3)};
+  margin-bottom: ${space(3)};
+
+  @media (min-width: ${p => p.theme.breakpoints.small}) {
+    flex-direction: row;
+  }
+`;
+
+const Heading = styled('h3')`
+  text-transform: uppercase;
+  font-size: ${p => p.theme.fontSizeSmall};
+  color: ${p => p.theme.gray300};
+  margin-bottom: ${space(1)};
+`;
+
+const Content = styled('p')`
+  margin-bottom: ${space(2)};
+  font-size: ${p => p.theme.fontSizeMedium};
+`;
+
+const Illustration = styled('img')`
+  display: none;
+
+  @media (min-width: ${p => p.theme.breakpoints.small}) {
+    display: block;
+  }
+`;
+
+const ButtonList = styled('div')`
+  display: inline-flex;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 0 ${space(1)};
+`;

+ 16 - 18
static/app/components/events/eventReplay/replayContent.spec.tsx → static/app/components/events/eventReplay/replayPreview.spec.tsx

@@ -6,7 +6,7 @@ import ReplayReader from 'sentry/utils/replays/replayReader';
 import {OrganizationContext} from 'sentry/views/organizationContext';
 import {RouteContext} from 'sentry/views/routeContext';
 
-import ReplayContent from './replayContent';
+import ReplayPreview from './replayPreview';
 
 const mockOrgSlug = 'sentry-emerging-tech';
 const mockReplaySlug = 'replays:761104e184c64d439ee1014b72b4d83b';
@@ -92,7 +92,7 @@ const render: typeof baseRender = children => {
   );
 };
 
-describe('ReplayContent', () => {
+describe('ReplayPreview', () => {
   it('Should render a placeholder when is fetching the replay data', () => {
     // Change the mocked hook to return a loading state
     (useReplayData as jest.Mock).mockImplementationOnce(() => {
@@ -103,7 +103,7 @@ describe('ReplayContent', () => {
     });
 
     render(
-      <ReplayContent
+      <ReplayPreview
         orgSlug={mockOrgSlug}
         replaySlug={mockReplaySlug}
         event={mockEvent}
@@ -123,35 +123,33 @@ describe('ReplayContent', () => {
       };
     });
 
-    expect(() =>
-      render(
-        <ReplayContent
-          orgSlug={mockOrgSlug}
-          replaySlug={mockReplaySlug}
-          event={mockEvent}
-        />
-      )
-    ).toThrow();
+    render(
+      <ReplayPreview
+        orgSlug={mockOrgSlug}
+        replaySlug={mockReplaySlug}
+        event={mockEvent}
+      />
+    );
+
+    expect(screen.getByTestId('replay-error')).toBeVisible();
   });
 
   it('Should render details button when there is a replay', () => {
     render(
-      <ReplayContent
+      <ReplayPreview
         orgSlug={mockOrgSlug}
         replaySlug={mockReplaySlug}
         event={mockEvent}
       />
     );
 
-    const detailButtons = screen.getAllByLabelText('View Full Replay');
-    // Expect the details buttons to have the correct href
-    expect(detailButtons[0]).toHaveAttribute('href', mockButtonHref);
-    expect(detailButtons[1]).toHaveAttribute('href', mockButtonHref);
+    const detailButton = screen.getByLabelText('Open Replay');
+    expect(detailButton).toHaveAttribute('href', mockButtonHref);
   });
 
   it('Should render all its elements correctly', () => {
     render(
-      <ReplayContent
+      <ReplayPreview
         orgSlug={mockOrgSlug}
         replaySlug={mockReplaySlug}
         event={mockEvent}

+ 32 - 65
static/app/components/events/eventReplay/replayContent.tsx → static/app/components/events/eventReplay/replayPreview.tsx

@@ -1,7 +1,7 @@
 import {useMemo} from 'react';
-import {Link} from 'react-router';
 import styled from '@emotion/styled';
 
+import Alert from 'sentry/components/alert';
 import Button from 'sentry/components/button';
 import Placeholder from 'sentry/components/placeholder';
 import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
@@ -33,10 +33,6 @@ function ReplayContent({orgSlug, replaySlug, event}: Props) {
     ? Math.floor(new Date(event.dateCreated).getTime() / 1000) * 1000
     : 0;
 
-  if (fetchError) {
-    throw new Error('Failed to load Replay');
-  }
-
   const replayRecord = replay?.getReplay();
 
   const startTimestampMs = replayRecord?.startedAt.getTime() ?? 0;
@@ -49,6 +45,14 @@ function ReplayContent({orgSlug, replaySlug, event}: Props) {
     return 0;
   }, [eventTimestamp, startTimestampMs]);
 
+  if (fetchError) {
+    return (
+      <Alert type="info" showIcon data-test-id="replay-error">
+        {t('The replay associated with this event could not be found.')}
+      </Alert>
+    );
+  }
+
   if (fetching || !replayRecord) {
     return (
       <StyledPlaceholder
@@ -71,31 +75,18 @@ function ReplayContent({orgSlug, replaySlug, event}: Props) {
   return (
     <ReplayContextProvider replay={replay} initialTimeOffset={initialTimeOffset}>
       <PlayerContainer data-test-id="player-container">
+        <StaticPanel>
+          <ReplayPlayer isPreview />
+        </StaticPanel>
+        <CTAOverlay>
+          <Button icon={<IconPlay />} priority="primary" to={fullReplayUrl}>
+            {t('Open Replay')}
+          </Button>
+        </CTAOverlay>
         <BadgeContainer>
           <FeatureText>{t('Replays')}</FeatureText>
           <ReplaysFeatureBadge />
         </BadgeContainer>
-        <FluidHeight>
-          <CTAOverlayLink aria-label={t('View Full Replay')} to={fullReplayUrl}>
-            <CTAIcon />
-          </CTAOverlayLink>
-
-          <StaticPanel>
-            <ReplayPlayer isPreview />
-          </StaticPanel>
-        </FluidHeight>
-
-        <CTAButtonContainer>
-          <Button
-            data-test-id="view-replay-button"
-            to={fullReplayUrl}
-            priority="primary"
-            size="sm"
-            icon={<IconPlay size="sm" />}
-          >
-            {t('View Full Replay')}
-          </Button>
-        </CTAButtonContainer>
       </PlayerContainer>
     </ReplayContextProvider>
   );
@@ -109,6 +100,21 @@ const PlayerContainer = styled(FluidHeight)`
   max-height: 448px;
 `;
 
+const StaticPanel = styled(FluidHeight)`
+  border: 1px solid ${p => p.theme.border};
+  border-radius: ${p => p.theme.borderRadius};
+`;
+
+const CTAOverlay = styled('div')`
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: rgba(255, 255, 255, 0.5);
+`;
+
 const BadgeContainer = styled('div')`
   display: flex;
   align-items: center;
@@ -118,7 +124,6 @@ const BadgeContainer = styled('div')`
   background: ${p => p.theme.background};
   border-radius: 2.25rem;
   padding: ${space(0.75)} ${space(0.75)} ${space(0.75)} ${space(1)};
-  z-index: 2;
   box-shadow: ${p => p.theme.dropShadowLightest};
   gap: 0 ${space(0.25)};
 `;
@@ -129,44 +134,6 @@ const FeatureText = styled('div')`
   color: ${p => p.theme.text};
 `;
 
-const CTAOverlayLink = styled(Link)`
-  position: absolute;
-  width: 100%;
-  height: 100%;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  z-index: 1;
-`;
-
-const CTAIcon = styled(({className}: {className?: string}) => (
-  <div className={className}>
-    <IconPlay size="xl" />
-  </div>
-))`
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  width: 72px;
-  height: 72px;
-  border-radius: 50%;
-  background: ${p => p.theme.purple400};
-  color: white;
-  padding-left: 5px; /* Align the icon in the center of the circle */
-`;
-
-const StaticPanel = styled(FluidHeight)`
-  background: ${p => p.theme.background};
-  border: 1px solid ${p => p.theme.border};
-  border-radius: ${p => p.theme.borderRadius};
-  box-shadow: ${p => p.theme.dropShadowLight};
-`;
-
-const CTAButtonContainer = styled('div')`
-  display: flex;
-  justify-content: flex-end;
-`;
-
 const StyledPlaceholder = styled(Placeholder)`
   margin-bottom: ${space(2)};
 `;

+ 53 - 12
static/app/components/events/interfaces/breadcrumbs/breadcrumbs.spec.tsx

@@ -1,21 +1,21 @@
 import selectEvent from 'react-select-event';
 
 import {initializeOrg} from 'sentry-test/initializeOrg';
-import {
-  render,
-  screen,
-  userEvent,
-  waitFor,
-  within,
-} from 'sentry-test/reactTestingLibrary';
+import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
 import {textWithMarkupMatcher} from 'sentry-test/utils';
 
 import Breadcrumbs from 'sentry/components/events/interfaces/breadcrumbs';
 import {BreadcrumbLevelType, BreadcrumbType} from 'sentry/types/breadcrumbs';
+import {
+  useReplayOnboardingSidebarPanel,
+  useShouldShowOnboarding,
+} from 'sentry/utils/replays/hooks/useReplayOnboarding';
 import ReplayReader from 'sentry/utils/replays/replayReader';
 
 const mockReplay = ReplayReader.factory(TestStubs.ReplayReaderParams());
 
+jest.mock('sentry/utils/replays/hooks/useReplayOnboarding');
+
 jest.mock('screenfull', () => ({
   enabled: true,
   isFullscreen: false,
@@ -41,7 +41,20 @@ describe('Breadcrumbs', () => {
   let props: React.ComponentProps<typeof Breadcrumbs>;
   const {router} = initializeOrg();
 
+  const MockUseReplayOnboardingSidebarPanel =
+    useReplayOnboardingSidebarPanel as jest.MockedFunction<
+      typeof useReplayOnboardingSidebarPanel
+    >;
+
+  const MockUseShouldShowOnboarding = useShouldShowOnboarding as jest.MockedFunction<
+    typeof useShouldShowOnboarding
+  >;
   beforeEach(() => {
+    MockUseShouldShowOnboarding.mockReturnValue(true);
+    MockUseReplayOnboardingSidebarPanel.mockReturnValue({
+      enabled: true,
+      activateSidebar: jest.fn(),
+    });
     props = {
       route: {},
       router,
@@ -186,9 +199,39 @@ describe('Breadcrumbs', () => {
 
       expect(screen.getByTestId('last-crumb')).toBeInTheDocument();
     });
+  });
+
+  describe('replay', () => {
+    it('should render the replay inline onboarding component when replays are enabled', async function () {
+      MockUseShouldShowOnboarding.mockReturnValue(true);
+      MockUseReplayOnboardingSidebarPanel.mockReturnValue({
+        enabled: true,
+        activateSidebar: jest.fn(),
+      });
+      const {container} = render(
+        <Breadcrumbs
+          {...props}
+          event={TestStubs.Event({
+            entries: [],
+            tags: [],
+          })}
+          organization={TestStubs.Organization({
+            features: ['session-replay-ui'],
+          })}
+        />
+      );
+
+      expect(await screen.findByText('Configure Session Replay')).toBeInTheDocument();
+      expect(container).toSnapshot();
+    });
 
     it('should render a replay when there is a replayId', async function () {
-      render(
+      MockUseShouldShowOnboarding.mockReturnValue(false);
+      MockUseReplayOnboardingSidebarPanel.mockReturnValue({
+        enabled: false,
+        activateSidebar: jest.fn(),
+      });
+      const {container} = render(
         <Breadcrumbs
           {...props}
           event={TestStubs.Event({
@@ -201,10 +244,8 @@ describe('Breadcrumbs', () => {
         />
       );
 
-      await waitFor(() => {
-        expect(screen.getByText('Replays')).toBeVisible();
-        expect(screen.getByTestId('player-container')).toBeInTheDocument();
-      });
+      expect(await screen.findByTestId('player-container')).toBeInTheDocument();
+      expect(container).toSnapshot();
     });
 
     it('can change the sort', async function () {

+ 2 - 3
static/app/components/events/interfaces/breadcrumbs/index.tsx

@@ -279,8 +279,7 @@ function BreadcrumbsContainer({
   }
 
   const replayId = event?.tags?.find(({key}) => key === 'replayId')?.value;
-  const showReplay =
-    !isShare && Boolean(replayId) && organization.features.includes('session-replay-ui');
+  const showReplay = !isShare && organization.features.includes('session-replay-ui');
 
   const actions = (
     <SearchAndSortWrapper isFullWidth={showReplay}>
@@ -317,7 +316,7 @@ function BreadcrumbsContainer({
       {showReplay ? (
         <Fragment>
           <EventReplay
-            replayId={replayId!}
+            replayId={replayId}
             projectSlug={projectSlug}
             orgSlug={organization.slug}
             event={event}

+ 3 - 0
static/app/utils/__mocks__/localStorage.tsx

@@ -5,6 +5,9 @@ const localStorageMock = function () {
     setItem: jest.fn((key, value) => {
       store[key] = value.toString();
     }),
+    removeItem: jest.fn(key => {
+      store[key] = null;
+    }),
     clear: jest.fn(() => {
       store = {};
     }),

+ 54 - 0
static/app/utils/replays/hooks/useReplayOnboarding.tsx

@@ -0,0 +1,54 @@
+import {useCallback, useEffect, useMemo} from 'react';
+
+import {SidebarPanelKey} from 'sentry/components/sidebar/types';
+import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
+import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
+import {Project} from 'sentry/types';
+import {PageFilters} from 'sentry/types/core';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import useProjects from 'sentry/utils/useProjects';
+import {useRouteContext} from 'sentry/utils/useRouteContext';
+
+function getProjectList(selectedProjects: PageFilters['projects'], projects: Project[]) {
+  if (selectedProjects[0] === ALL_ACCESS_PROJECTS || selectedProjects.length === 0) {
+    return projects;
+  }
+
+  const projectsByProjectId = projects.reduce<Record<string, Project>>((acc, project) => {
+    acc[project.id] = project;
+    return acc;
+  }, {});
+  return selectedProjects.map(id => projectsByProjectId[id]).filter(Boolean);
+}
+
+export function useShouldShowOnboarding() {
+  const {projects} = useProjects();
+  const {selection} = usePageFilters();
+
+  const shouldShowOnboardingPanel = useMemo(() => {
+    const projectList = getProjectList(selection.projects, projects);
+    const hasSentOneReplay = projectList.some(project => project.hasReplays);
+    return !hasSentOneReplay;
+  }, [selection.projects, projects]);
+
+  return shouldShowOnboardingPanel;
+}
+
+export function useReplayOnboardingSidebarPanel() {
+  const {location} = useRouteContext();
+  const enabled = useShouldShowOnboarding();
+
+  useEffect(() => {
+    if (enabled && location.hash === '#replay-sidequest') {
+      SidebarPanelStore.activatePanel(SidebarPanelKey.ReplaysOnboarding);
+    }
+  }, [enabled, location.hash]);
+
+  const activateSidebar = useCallback((event: {preventDefault: () => void}) => {
+    event.preventDefault();
+    window.location.hash = 'replay-sidequest';
+    SidebarPanelStore.activatePanel(SidebarPanelKey.ReplaysOnboarding);
+  }, []);
+
+  return {enabled, activateSidebar};
+}

+ 4 - 55
static/app/views/replays/replays.tsx

@@ -1,4 +1,4 @@
-import {Fragment, useCallback, useEffect, useMemo} from 'react';
+import {Fragment, useMemo} from 'react';
 import {browserHistory, RouteComponentProps} from 'react-router';
 import {useTheme} from '@emotion/react';
 import styled from '@emotion/styled';
@@ -9,23 +9,16 @@ import PageFiltersContainer from 'sentry/components/organizations/pageFilters/co
 import PageHeading from 'sentry/components/pageHeading';
 import Pagination from 'sentry/components/pagination';
 import ReplaysFeatureBadge from 'sentry/components/replays/replaysFeatureBadge';
-import {SidebarPanelKey} from 'sentry/components/sidebar/types';
-import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
 import {t} from 'sentry/locale';
-import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
 import {PageContent} from 'sentry/styles/organization';
-import {Project} from 'sentry/types';
-import {PageFilters} from 'sentry/types/core';
 import EventView from 'sentry/utils/discover/eventView';
 import {decodeScalar} from 'sentry/utils/queryString';
 import {DEFAULT_SORT, REPLAY_LIST_FIELDS} from 'sentry/utils/replays/fetchReplayList';
 import useReplayList from 'sentry/utils/replays/hooks/useReplayList';
+import {useReplayOnboardingSidebarPanel} from 'sentry/utils/replays/hooks/useReplayOnboarding';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
-import {useLocation} from 'sentry/utils/useLocation';
 import useMedia from 'sentry/utils/useMedia';
 import useOrganization from 'sentry/utils/useOrganization';
-import usePageFilters from 'sentry/utils/usePageFilters';
-import useProjects from 'sentry/utils/useProjects';
 import ReplaysFilters from 'sentry/views/replays/filters';
 import ReplayOnboardingPanel from 'sentry/views/replays/list/replayOnboardingPanel';
 import ReplayTable from 'sentry/views/replays/replayTable';
@@ -33,50 +26,6 @@ import type {ReplayListLocationQuery} from 'sentry/views/replays/types';
 
 type Props = RouteComponentProps<{orgId: string}, {}, any, ReplayListLocationQuery>;
 
-function getProjectList(selectedProjects: PageFilters['projects'], projects: Project[]) {
-  if (selectedProjects[0] === ALL_ACCESS_PROJECTS || selectedProjects.length === 0) {
-    return projects;
-  }
-
-  const projectsByProjectId = projects.reduce<Record<string, Project>>((acc, project) => {
-    acc[project.id] = project;
-    return acc;
-  }, {});
-  return selectedProjects.map(id => projectsByProjectId[id]).filter(Boolean);
-}
-
-function useShouldShowOnboardingPanel() {
-  const {projects} = useProjects();
-  const {selection} = usePageFilters();
-
-  const shouldShowOnboardingPanel = useMemo(() => {
-    const projectList = getProjectList(selection.projects, projects);
-    const hasSentOneReplay = projectList.some(project => project.hasReplays);
-    return !hasSentOneReplay;
-  }, [selection.projects, projects]);
-
-  return shouldShowOnboardingPanel;
-}
-
-function useReplayOnboardingSidebarPanel() {
-  const location = useLocation();
-  const enabled = useShouldShowOnboardingPanel();
-
-  useEffect(() => {
-    if (enabled && location.hash === '#replay-sidequest') {
-      SidebarPanelStore.activatePanel(SidebarPanelKey.ReplaysOnboarding);
-    }
-  }, [enabled, location.hash]);
-
-  const activate = useCallback(event => {
-    event.preventDefault();
-    window.location.hash = 'replay-sidequest';
-    SidebarPanelStore.activatePanel(SidebarPanelKey.ReplaysOnboarding);
-  }, []);
-
-  return {enabled, activate};
-}
-
 function Replays({location}: Props) {
   const organization = useOrganization();
   const theme = useTheme();
@@ -105,7 +54,7 @@ function Replays({location}: Props) {
     eventView,
   });
 
-  const {enabled: shouldShowOnboardingPanel, activate: activateOnboardingSidebarPanel} =
+  const {enabled: shouldShowOnboardingPanel, activateSidebar} =
     useReplayOnboardingSidebarPanel();
 
   return (
@@ -122,7 +71,7 @@ function Replays({location}: Props) {
           <ReplaysFilters />
           {shouldShowOnboardingPanel ? (
             <ReplayOnboardingPanel>
-              <Button onClick={activateOnboardingSidebarPanel} priority="primary">
+              <Button onClick={activateSidebar} priority="primary">
                 {t('Get Started')}
               </Button>
               <Button

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