Browse Source

feat(replays): Create a nicer "Update your SDK for dead/rage clicks" banner (#56133)

Build a new banner to let people know that it's time to update their SDK
and get Dead Clicks and Rage Clicks

Based on figma:
https://www.figma.com/file/yS1d4Q0m8bziJ15esyAUXB/Exploration%3A-Index-v6?type=design&node-id=618-1046&mode=design&t=HtDkmJFqDBy2bFSA-0

**Before:**

![SCR-20230912-nzcy](https://github.com/getsentry/sentry/assets/187460/24eeb9dc-ff3f-49db-9a69-4f6b19a22237)


**After:**

![SCR-20230912-nxtk](https://github.com/getsentry/sentry/assets/187460/fd3380fd-f3e7-44a9-97ec-5022cde9b877)


I tested the other states of the page too. The layout in these cases is
the main thing to test:

| Have dead/rage clicks | empty dead/rage cards | onboarding panel |
| --- | --- | --- |
|
![SCR-20230912-nxnm](https://github.com/getsentry/sentry/assets/187460/b39f6cf4-66b5-4d80-bbf6-8a0538437c34)
|
![SCR-20230912-nxsd](https://github.com/getsentry/sentry/assets/187460/40fa07e8-bb9e-4a6e-a457-ef7af14a2913)
|
![SCR-20230912-nxur](https://github.com/getsentry/sentry/assets/187460/318a3b2c-12c8-46d2-9a48-0e68f040f2eb)
|


Relates to https://github.com/getsentry/team-replay/issues/138
Ryan Albrecht 1 year ago
parent
commit
d3abd24abe

+ 82 - 0
static/app/components/replays/replayRageClickSdkVersionBanner.tsx

@@ -0,0 +1,82 @@
+import {useEffect} from 'react';
+import styled from '@emotion/styled';
+
+import replaysDeadRageBackground from 'sentry-images/spot/replay-dead-rage-changelog.svg';
+
+import {LinkButton} from 'sentry/components/button';
+import PageBanner from 'sentry/components/replays/pageBanner';
+import {IconBroadcast} from 'sentry/icons';
+import {t, tct} from 'sentry/locale';
+import {trackAnalytics} from 'sentry/utils/analytics';
+import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
+import {MIN_DEAD_RAGE_CLICK_SDK} from 'sentry/utils/replays/sdkVersions';
+import useDismissAlert from 'sentry/utils/useDismissAlert';
+import useOrganization from 'sentry/utils/useOrganization';
+import {useRoutes} from 'sentry/utils/useRoutes';
+
+export default function ReplayRageClickSdkVersionBanner() {
+  const organization = useOrganization();
+
+  const {dismiss, isDismissed} = useDismissAlert({
+    key: `${organization.id}:replay-rage-dead-click-sdk-version-banner-v1`,
+  });
+
+  const routes = useRoutes();
+  const surface = getRouteStringFromRoutes(routes);
+
+  useEffect(() => {
+    trackAnalytics('replay.rage-click-sdk-banner.rendered', {
+      is_dismissed: isDismissed,
+      organization,
+      surface,
+    });
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []); // Don't log immediatly after the banner is dismissed. On each pageload/mount is fine.
+
+  if (isDismissed) {
+    return null;
+  }
+
+  return (
+    <PageBanner
+      button={
+        <LinkButton
+          analyticsEventKey="replay.rage-click-sdk-banner.viewed_changelog"
+          analyticsEventName="Replay Rage Click SDK Banner Viewed Changelog"
+          analyticsParams={{surface}}
+          external
+          href={MIN_DEAD_RAGE_CLICK_SDK.changelog}
+          priority="primary"
+        >
+          {t('Read Changelog')}
+        </LinkButton>
+      }
+      description={t(
+        "Understand what your users do when your user experience doesn't meet their expectations"
+      )}
+      heading={t('Introducing Rage and Dead Clicks')}
+      icon={<IconBroadcast size="sm" />}
+      image={replaysDeadRageBackground}
+      onDismiss={() => {
+        trackAnalytics('replay.rage-click-sdk-banner.dismissed', {
+          organization,
+          surface,
+        });
+        dismiss();
+      }}
+      title={tct("What's new in [version]", {
+        version: (
+          <PurpleText>
+            {tct(`version [version_number]`, {
+              version_number: MIN_DEAD_RAGE_CLICK_SDK.minVersion,
+            })}
+          </PurpleText>
+        ),
+      })}
+    />
+  );
+}
+
+const PurpleText = styled('strong')`
+  color: ${p => p.theme.purple400};
+`;

+ 9 - 0
static/app/utils/analytics/replayAnalyticsEvents.tsx

@@ -68,6 +68,13 @@ export type ReplayEventParameters = {
     play: boolean;
     play: boolean;
     user_email: string;
     user_email: string;
   };
   };
+  'replay.rage-click-sdk-banner.dismissed': {
+    surface: string;
+  };
+  'replay.rage-click-sdk-banner.rendered': {
+    is_dismissed: boolean;
+    surface: string;
+  };
   'replay.render-issues-group-list': {
   'replay.render-issues-group-list': {
     platform: string | undefined;
     platform: string | undefined;
     project_id: string | undefined;
     project_id: string | undefined;
@@ -107,6 +114,8 @@ export const replayEventMap: Record<ReplayEventKey, string | null> = {
   'replay.list-time-spent': 'Time Spent Viewing Replay List',
   'replay.list-time-spent': 'Time Spent Viewing Replay List',
   'replay.list-view-setup-sidebar': 'Views Set Up Replays Sidebar',
   'replay.list-view-setup-sidebar': 'Views Set Up Replays Sidebar',
   'replay.play-pause': 'Played/Paused Replay',
   'replay.play-pause': 'Played/Paused Replay',
+  'replay.rage-click-sdk-banner.dismissed': 'Replay Rage Click SDK Banner Dismissed',
+  'replay.rage-click-sdk-banner.rendered': 'Replay Rage Click SDK Banner Rendered',
   'replay.render-issues-group-list': 'Render Issues Detail Replay List',
   'replay.render-issues-group-list': 'Render Issues Detail Replay List',
   'replay.render-player': 'Rendered ReplayPlayer',
   'replay.render-player': 'Rendered ReplayPlayer',
   'replay.search': 'Searched Replay',
   'replay.search': 'Searched Replay',

+ 5 - 0
static/app/utils/replays/sdkVersions.tsx

@@ -0,0 +1,5 @@
+export const MIN_DEAD_RAGE_CLICK_SDK = {
+  minVersion: '7.60.1',
+  changelog:
+    'https://changelog.getsentry.com/announcements/user-frustration-signals-rage-and-dead-clicks-in-session-replay',
+};

+ 6 - 19
static/app/views/replays/list/filters.tsx

@@ -1,27 +1,14 @@
-import type {ReactNode} from 'react';
-import styled from '@emotion/styled';
-
 import DatePageFilter from 'sentry/components/datePageFilter';
 import DatePageFilter from 'sentry/components/datePageFilter';
 import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
 import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
 import ProjectPageFilter from 'sentry/components/projectPageFilter';
 import ProjectPageFilter from 'sentry/components/projectPageFilter';
-import {space} from 'sentry/styles/space';
 
 
-export default function ReplaysFilters({children}: {children?: ReactNode}) {
+export default function ReplaysFilters() {
   return (
   return (
-    <FiltersContainer>
-      <PageFilterBar condensed>
-        <ProjectPageFilter resetParamsOnChange={['cursor']} />
-        <EnvironmentPageFilter resetParamsOnChange={['cursor']} />
-        <DatePageFilter alignDropdown="left" resetParamsOnChange={['cursor']} />
-      </PageFilterBar>
-      {children}
-    </FiltersContainer>
+    <PageFilterBar condensed>
+      <ProjectPageFilter resetParamsOnChange={['cursor']} />
+      <EnvironmentPageFilter resetParamsOnChange={['cursor']} />
+      <DatePageFilter alignDropdown="left" resetParamsOnChange={['cursor']} />
+    </PageFilterBar>
   );
   );
 }
 }
-
-const FiltersContainer = styled('div')`
-  display: flex;
-  flex-direction: row;
-  gap: ${space(2)};
-`;

+ 57 - 6
static/app/views/replays/list/listContent.spec.tsx

@@ -7,11 +7,13 @@ import {
   useReplayOnboardingSidebarPanel,
   useReplayOnboardingSidebarPanel,
 } from 'sentry/utils/replays/hooks/useReplayOnboarding';
 } from 'sentry/utils/replays/hooks/useReplayOnboarding';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
+import useProjectSdkNeedsUpdate from 'sentry/utils/useProjectSdkNeedsUpdate';
 import ListPage from 'sentry/views/replays/list/listContent';
 import ListPage from 'sentry/views/replays/list/listContent';
 
 
+jest.mock('sentry/utils/replays/hooks/useReplayOnboarding');
 jest.mock('sentry/utils/replays/hooks/useReplayPageview');
 jest.mock('sentry/utils/replays/hooks/useReplayPageview');
 jest.mock('sentry/utils/useOrganization');
 jest.mock('sentry/utils/useOrganization');
-jest.mock('sentry/utils/replays/hooks/useReplayOnboarding');
+jest.mock('sentry/utils/useProjectSdkNeedsUpdate');
 jest.mock('sentry/utils/replays/hooks/useReplayList', () => {
 jest.mock('sentry/utils/replays/hooks/useReplayList', () => {
   return {
   return {
     __esModule: true,
     __esModule: true,
@@ -31,6 +33,7 @@ const mockUseReplayList = jest.mocked(useReplayList);
 const mockUseHaveSelectedProjectsSentAnyReplayEvents = jest.mocked(
 const mockUseHaveSelectedProjectsSentAnyReplayEvents = jest.mocked(
   useHaveSelectedProjectsSentAnyReplayEvents
   useHaveSelectedProjectsSentAnyReplayEvents
 );
 );
+const mockUseProjectSdkNeedsUpdate = jest.mocked(useProjectSdkNeedsUpdate);
 const mockUseReplayOnboardingSidebarPanel = jest.mocked(useReplayOnboardingSidebarPanel);
 const mockUseReplayOnboardingSidebarPanel = jest.mocked(useReplayOnboardingSidebarPanel);
 
 
 mockUseReplayOnboardingSidebarPanel.mockReturnValue({activateSidebar: jest.fn()});
 mockUseReplayOnboardingSidebarPanel.mockReturnValue({activateSidebar: jest.fn()});
@@ -56,11 +59,9 @@ function getMockContext(mockOrg: Organization) {
 describe('ReplayList', () => {
 describe('ReplayList', () => {
   beforeEach(() => {
   beforeEach(() => {
     mockUseReplayList.mockClear();
     mockUseReplayList.mockClear();
+    mockUseHaveSelectedProjectsSentAnyReplayEvents.mockClear();
+    mockUseProjectSdkNeedsUpdate.mockClear();
     MockApiClient.clearMockResponses();
     MockApiClient.clearMockResponses();
-    MockApiClient.addMockResponse({
-      url: '/organizations/org-slug/sdk-updates/',
-      body: [],
-    });
     MockApiClient.addMockResponse({
     MockApiClient.addMockResponse({
       url: '/organizations/org-slug/tags/',
       url: '/organizations/org-slug/tags/',
       body: [],
       body: [],
@@ -73,6 +74,10 @@ describe('ReplayList', () => {
       fetching: false,
       fetching: false,
       hasSentOneReplay: false,
       hasSentOneReplay: false,
     });
     });
+    mockUseProjectSdkNeedsUpdate.mockReturnValue({
+      isFetching: false,
+      needsUpdate: false,
+    });
 
 
     render(<ListPage />, {
     render(<ListPage />, {
       context: getMockContext(mockOrg),
       context: getMockContext(mockOrg),
@@ -90,6 +95,10 @@ describe('ReplayList', () => {
       fetching: false,
       fetching: false,
       hasSentOneReplay: true,
       hasSentOneReplay: true,
     });
     });
+    mockUseProjectSdkNeedsUpdate.mockReturnValue({
+      isFetching: false,
+      needsUpdate: false,
+    });
 
 
     render(<ListPage />, {
     render(<ListPage />, {
       context: getMockContext(mockOrg),
       context: getMockContext(mockOrg),
@@ -107,6 +116,10 @@ describe('ReplayList', () => {
       fetching: false,
       fetching: false,
       hasSentOneReplay: false,
       hasSentOneReplay: false,
     });
     });
+    mockUseProjectSdkNeedsUpdate.mockReturnValue({
+      isFetching: false,
+      needsUpdate: false,
+    });
 
 
     render(<ListPage />, {
     render(<ListPage />, {
       context: getMockContext(mockOrg),
       context: getMockContext(mockOrg),
@@ -118,12 +131,50 @@ describe('ReplayList', () => {
     expect(mockUseReplayList).not.toHaveBeenCalled();
     expect(mockUseReplayList).not.toHaveBeenCalled();
   });
   });
 
 
-  it('should fetch the replay table when the org is on AM2 and sent some replays', async () => {
+  it('should render the rage-click sdk update banner when the org is AM2, has sent replays, but the sdk version is low', async () => {
+    const mockOrg = getMockOrganization({features: AM2_FEATURES});
+    mockUseHaveSelectedProjectsSentAnyReplayEvents.mockReturnValue({
+      fetching: false,
+      hasSentOneReplay: true,
+    });
+    mockUseProjectSdkNeedsUpdate.mockReturnValue({
+      isFetching: false,
+      needsUpdate: true,
+    });
+    mockUseReplayList.mockReturnValue({
+      replays: [],
+      isFetching: false,
+      fetchError: undefined,
+      pageLinks: null,
+    });
+
+    render(<ListPage />, {
+      context: getMockContext(mockOrg),
+    });
+
+    await waitFor(() => {
+      expect(screen.queryByText('Introducing Rage and Dead Clicks')).toBeInTheDocument();
+      expect(screen.queryByTestId('replay-table')).toBeInTheDocument();
+    });
+    expect(mockUseReplayList).toHaveBeenCalled();
+  });
+
+  it('should fetch the replay table and show dead/rage tables when the org is on AM2, has sent some replays, and has a newer SDK version', async () => {
     const mockOrg = getMockOrganization({features: AM2_FEATURES});
     const mockOrg = getMockOrganization({features: AM2_FEATURES});
     mockUseHaveSelectedProjectsSentAnyReplayEvents.mockReturnValue({
     mockUseHaveSelectedProjectsSentAnyReplayEvents.mockReturnValue({
       fetching: false,
       fetching: false,
       hasSentOneReplay: true,
       hasSentOneReplay: true,
     });
     });
+    mockUseProjectSdkNeedsUpdate.mockReturnValue({
+      isFetching: false,
+      needsUpdate: false,
+    });
+    mockUseReplayList.mockReturnValue({
+      replays: [],
+      isFetching: false,
+      fetchError: undefined,
+      pageLinks: null,
+    });
 
 
     render(<ListPage />, {
     render(<ListPage />, {
       context: getMockContext(mockOrg),
       context: getMockContext(mockOrg),

+ 66 - 12
static/app/views/replays/list/listContent.tsx

@@ -1,7 +1,14 @@
 import {Fragment} from 'react';
 import {Fragment} from 'react';
+import styled from '@emotion/styled';
 
 
+import ReplayRageClickSdkVersionBanner from 'sentry/components/replays/replayRageClickSdkVersionBanner';
+import {space} from 'sentry/styles/space';
 import {useHaveSelectedProjectsSentAnyReplayEvents} from 'sentry/utils/replays/hooks/useReplayOnboarding';
 import {useHaveSelectedProjectsSentAnyReplayEvents} from 'sentry/utils/replays/hooks/useReplayOnboarding';
+import {MIN_DEAD_RAGE_CLICK_SDK} from 'sentry/utils/replays/sdkVersions';
+import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import useProjectSdkNeedsUpdate from 'sentry/utils/useProjectSdkNeedsUpdate';
 import ReplaysFilters from 'sentry/views/replays/list/filters';
 import ReplaysFilters from 'sentry/views/replays/list/filters';
 import ReplayOnboardingPanel from 'sentry/views/replays/list/replayOnboardingPanel';
 import ReplayOnboardingPanel from 'sentry/views/replays/list/replayOnboardingPanel';
 import ReplaysErroneousDeadRageCards from 'sentry/views/replays/list/replaysErroneousDeadRageCards';
 import ReplaysErroneousDeadRageCards from 'sentry/views/replays/list/replaysErroneousDeadRageCards';
@@ -12,22 +19,69 @@ export default function ListContent() {
   const organization = useOrganization();
   const organization = useOrganization();
 
 
   const hasSessionReplay = organization.features.includes('session-replay');
   const hasSessionReplay = organization.features.includes('session-replay');
-  const {hasSentOneReplay, fetching} = useHaveSelectedProjectsSentAnyReplayEvents();
-  const showOnboarding = !hasSessionReplay || !hasSentOneReplay;
 
 
-  return fetching ? null : showOnboarding ? (
-    <Fragment>
-      <ReplaysFilters>
-        <ReplaysSearch />
-      </ReplaysFilters>
-      <ReplayOnboardingPanel />
-    </Fragment>
-  ) : (
+  const hasSentReplays = useHaveSelectedProjectsSentAnyReplayEvents();
+
+  const {
+    selection: {projects},
+  } = usePageFilters();
+  const rageClicksSdkVersion = useProjectSdkNeedsUpdate({
+    minVersion: MIN_DEAD_RAGE_CLICK_SDK.minVersion,
+    organization,
+    projectId: projects.map(String),
+  });
+
+  useRouteAnalyticsParams({
+    hasSessionReplay,
+    hasSentReplays: hasSentReplays.hasSentOneReplay,
+    hasRageClickMinSDK: !rageClicksSdkVersion.needsUpdate,
+  });
+
+  if (hasSentReplays.fetching || rageClicksSdkVersion.isFetching) {
+    return null;
+  }
+
+  if (!hasSessionReplay || !hasSentReplays.hasSentOneReplay) {
+    return (
+      <Fragment>
+        <FiltersContainer>
+          <ReplaysFilters />
+          <ReplaysSearch />
+        </FiltersContainer>
+        <ReplayOnboardingPanel />
+      </Fragment>
+    );
+  }
+
+  if (rageClicksSdkVersion.needsUpdate) {
+    return (
+      <Fragment>
+        <FiltersContainer>
+          <ReplaysFilters />
+          <ReplaysSearch />
+        </FiltersContainer>
+        <ReplayRageClickSdkVersionBanner />
+        <ReplaysList />
+      </Fragment>
+    );
+  }
+
+  return (
     <Fragment>
     <Fragment>
-      <ReplaysFilters />
+      <FiltersContainer>
+        <ReplaysFilters />
+      </FiltersContainer>
       <ReplaysErroneousDeadRageCards />
       <ReplaysErroneousDeadRageCards />
-      <ReplaysSearch />
+      <FiltersContainer>
+        <ReplaysSearch />
+      </FiltersContainer>
       <ReplaysList />
       <ReplaysList />
     </Fragment>
     </Fragment>
   );
   );
 }
 }
+
+const FiltersContainer = styled('div')`
+  display: flex;
+  flex-direction: row;
+  gap: ${space(2)};
+`;

+ 2 - 6
static/app/views/replays/list/replaysErroneousDeadRageCards.tsx

@@ -10,7 +10,6 @@ import {space} from 'sentry/styles/space';
 import type {Organization} from 'sentry/types';
 import type {Organization} from 'sentry/types';
 import EventView from 'sentry/utils/discover/eventView';
 import EventView from 'sentry/utils/discover/eventView';
 import useReplayList from 'sentry/utils/replays/hooks/useReplayList';
 import useReplayList from 'sentry/utils/replays/hooks/useReplayList';
-import {useHaveSelectedProjectsSentAnyReplayEvents} from 'sentry/utils/replays/hooks/useReplayOnboarding';
 import {useLocation} from 'sentry/utils/useLocation';
 import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
 import ReplayTable from 'sentry/views/replays/replayTable';
 import ReplayTable from 'sentry/views/replays/replayTable';
@@ -97,9 +96,6 @@ function ReplaysErroneousDeadRageCards() {
     );
     );
   }, [newLocation]);
   }, [newLocation]);
 
 
-  const hasSessionReplay = organization.features.includes('session-replay');
-  const {hasSentOneReplay, fetching} = useHaveSelectedProjectsSentAnyReplayEvents();
-
   const deadCols = [
   const deadCols = [
     ReplayColumn.MOST_DEAD_CLICKS,
     ReplayColumn.MOST_DEAD_CLICKS,
     ReplayColumn.COUNT_DEAD_CLICKS_NO_HEADER,
     ReplayColumn.COUNT_DEAD_CLICKS_NO_HEADER,
@@ -110,7 +106,7 @@ function ReplaysErroneousDeadRageCards() {
     ReplayColumn.COUNT_RAGE_CLICKS_NO_HEADER,
     ReplayColumn.COUNT_RAGE_CLICKS_NO_HEADER,
   ];
   ];
 
 
-  return hasSessionReplay && hasSentOneReplay && !fetching ? (
+  return (
     <SplitCardContainer>
     <SplitCardContainer>
       <CardTable
       <CardTable
         eventView={eventViewDead}
         eventView={eventViewDead}
@@ -139,7 +135,7 @@ function ReplaysErroneousDeadRageCards() {
         buttonLabel={t('Show all replays with rage clicks')}
         buttonLabel={t('Show all replays with rage clicks')}
       />
       />
     </SplitCardContainer>
     </SplitCardContainer>
-  ) : null;
+  );
 }
 }
 
 
 function CardTable({
 function CardTable({

+ 5 - 5
static/app/views/replays/replayTable/headerCell.tsx

@@ -1,7 +1,7 @@
 import ExternalLink from 'sentry/components/links/externalLink';
 import ExternalLink from 'sentry/components/links/externalLink';
 import {t, tct} from 'sentry/locale';
 import {t, tct} from 'sentry/locale';
 import type {Sort} from 'sentry/utils/discover/fields';
 import type {Sort} from 'sentry/utils/discover/fields';
-import {MIN_DEAD_RAGE_CLICK_SDK} from 'sentry/views/replays/replayTable';
+import {MIN_DEAD_RAGE_CLICK_SDK} from 'sentry/utils/replays/sdkVersions';
 import SortableHeader from 'sentry/views/replays/replayTable/sortableHeader';
 import SortableHeader from 'sentry/views/replays/replayTable/sortableHeader';
 import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
 import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
 
 
@@ -36,7 +36,7 @@ function HeaderCell({column, sort}: Props) {
           tooltip={tct(
           tooltip={tct(
             'A dead click is a user click that does not result in any page activity after 7 seconds. Requires SDK version >= [minSDK]. [link:Learn more.]',
             'A dead click is a user click that does not result in any page activity after 7 seconds. Requires SDK version >= [minSDK]. [link:Learn more.]',
             {
             {
-              minSDK: MIN_DEAD_RAGE_CLICK_SDK,
+              minSDK: MIN_DEAD_RAGE_CLICK_SDK.minVersion,
               link: <ExternalLink href="https://docs.sentry.io/platforms/javascript/" />,
               link: <ExternalLink href="https://docs.sentry.io/platforms/javascript/" />,
             }
             }
           )}
           )}
@@ -58,7 +58,7 @@ function HeaderCell({column, sort}: Props) {
           tooltip={tct(
           tooltip={tct(
             'A rage click is 5 or more clicks on a dead element, which exhibits no page activity after 7 seconds. Requires SDK version >= [minSDK]. [link:Learn more.]',
             'A rage click is 5 or more clicks on a dead element, which exhibits no page activity after 7 seconds. Requires SDK version >= [minSDK]. [link:Learn more.]',
             {
             {
-              minSDK: MIN_DEAD_RAGE_CLICK_SDK,
+              minSDK: MIN_DEAD_RAGE_CLICK_SDK.minVersion,
               link: <ExternalLink href="https://docs.sentry.io/platforms/javascript/" />,
               link: <ExternalLink href="https://docs.sentry.io/platforms/javascript/" />,
             }
             }
           )}
           )}
@@ -87,7 +87,7 @@ function HeaderCell({column, sort}: Props) {
           tooltip={tct(
           tooltip={tct(
             'A rage click is 5 or more clicks on a dead element, which exhibits no page activity after 7 seconds. Requires SDK version >= [minSDK]. [link:Learn more.]',
             'A rage click is 5 or more clicks on a dead element, which exhibits no page activity after 7 seconds. Requires SDK version >= [minSDK]. [link:Learn more.]',
             {
             {
-              minSDK: MIN_DEAD_RAGE_CLICK_SDK,
+              minSDK: MIN_DEAD_RAGE_CLICK_SDK.minVersion,
               link: <ExternalLink href="https://docs.sentry.io/platforms/javascript/" />,
               link: <ExternalLink href="https://docs.sentry.io/platforms/javascript/" />,
             }
             }
           )}
           )}
@@ -101,7 +101,7 @@ function HeaderCell({column, sort}: Props) {
           tooltip={tct(
           tooltip={tct(
             'A dead click is a user click that does not result in any page activity after 7 seconds. Requires SDK version >= [minSDK]. [link:Learn more.]',
             'A dead click is a user click that does not result in any page activity after 7 seconds. Requires SDK version >= [minSDK]. [link:Learn more.]',
             {
             {
-              minSDK: MIN_DEAD_RAGE_CLICK_SDK,
+              minSDK: MIN_DEAD_RAGE_CLICK_SDK.minVersion,
               link: <ExternalLink href="https://docs.sentry.io/platforms/javascript/" />,
               link: <ExternalLink href="https://docs.sentry.io/platforms/javascript/" />,
             }
             }
           )}
           )}

+ 1 - 41
static/app/views/replays/replayTable/index.tsx

@@ -3,17 +3,14 @@ import styled from '@emotion/styled';
 import {Location} from 'history';
 import {Location} from 'history';
 
 
 import {Alert} from 'sentry/components/alert';
 import {Alert} from 'sentry/components/alert';
-import ExternalLink from 'sentry/components/links/externalLink';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import PanelTable from 'sentry/components/panels/panelTable';
 import PanelTable from 'sentry/components/panels/panelTable';
-import {t, tct} from 'sentry/locale';
+import {t} from 'sentry/locale';
 import EventView from 'sentry/utils/discover/eventView';
 import EventView from 'sentry/utils/discover/eventView';
 import type {Sort} from 'sentry/utils/discover/fields';
 import type {Sort} from 'sentry/utils/discover/fields';
 import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
 import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
 import {useLocation} from 'sentry/utils/useLocation';
 import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
-import usePageFilters from 'sentry/utils/usePageFilters';
-import useProjectSdkNeedsUpdate from 'sentry/utils/useProjectSdkNeedsUpdate';
 import {useRoutes} from 'sentry/utils/useRoutes';
 import {useRoutes} from 'sentry/utils/useRoutes';
 import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData';
 import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData';
 import HeaderCell from 'sentry/views/replays/replayTable/headerCell';
 import HeaderCell from 'sentry/views/replays/replayTable/headerCell';
@@ -31,8 +28,6 @@ import {
 import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
 import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
 import type {ReplayListRecord} from 'sentry/views/replays/types';
 import type {ReplayListRecord} from 'sentry/views/replays/types';
 
 
-export const MIN_DEAD_RAGE_CLICK_SDK = '7.60.1';
-
 type Props = {
 type Props = {
   fetchError: undefined | Error;
   fetchError: undefined | Error;
   isFetching: boolean;
   isFetching: boolean;
@@ -60,16 +55,6 @@ function ReplayTable({
   const newLocation = useLocation();
   const newLocation = useLocation();
   const organization = useOrganization();
   const organization = useOrganization();
 
 
-  const {
-    selection: {projects},
-  } = usePageFilters();
-
-  const needSDKUpgrade = useProjectSdkNeedsUpdate({
-    minVersion: MIN_DEAD_RAGE_CLICK_SDK,
-    organization,
-    projectId: projects.map(String),
-  });
-
   const location: Location = saveLocation
   const location: Location = saveLocation
     ? {
     ? {
         pathname: '',
         pathname: '',
@@ -106,31 +91,6 @@ function ReplayTable({
     );
     );
   }
   }
 
 
-  if (
-    needSDKUpgrade.needsUpdate &&
-    (visibleColumns.includes(ReplayColumn.MOST_DEAD_CLICKS) ||
-      visibleColumns.includes(ReplayColumn.MOST_RAGE_CLICKS))
-  ) {
-    return (
-      <StyledPanelTable
-        headers={tableHeaders}
-        visibleColumns={visibleColumns}
-        data-test-id="replay-table"
-        gridRows={undefined}
-        loader={<LoadingIndicator style={{margin: '54px auto'}} />}
-        disablePadding
-      >
-        <StyledAlert type="info" showIcon>
-          {tct('[data] requires [sdkPrompt]. [link:Upgrade now.]', {
-            data: <strong>Rage and dead clicks</strong>,
-            sdkPrompt: <strong>{t('SDK version >= 7.60.1')}</strong>,
-            link: <ExternalLink href="https://docs.sentry.io/platforms/javascript/" />,
-          })}
-        </StyledAlert>
-      </StyledPanelTable>
-    );
-  }
-
   const referrer = getRouteStringFromRoutes(routes);
   const referrer = getRouteStringFromRoutes(routes);
   const eventView = EventView.fromLocation(location);
   const eventView = EventView.fromLocation(location);
 
 

File diff suppressed because it is too large
+ 40 - 0
static/images/spot/replay-dead-rage-changelog.svg


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