Browse Source

fix(replays): Only show the Replay Onboarding for Backend & Frontend platforms, not Mobile (#49354)

Problem:

We were showing this onboarding banner for all projects, but it's not
relevant to iOS or Android, or desktop platforms because replay cannot
capture html replays.
It is however relevant to all server-side platforms, because they could
be linked to a web frontend, and therefore linked to a replay.

Here's the banner that's being shown/hidden (banner is unchanged):
![CleanShot 2023-05-17 at 13 50
07](https://github.com/getsentry/sentry/assets/187460/403d4d2d-dc1f-4d96-953f-371e5bebac30)



Follows https://github.com/getsentry/sentry/pull/48100
Ryan Albrecht 1 year ago
parent
commit
b4193f6dbc

+ 20 - 1
static/app/components/events/eventReplay/index.tsx

@@ -5,6 +5,8 @@ import LazyLoad from 'sentry/components/lazyLoad';
 import {Organization} from 'sentry/types';
 import {Event} from 'sentry/types/event';
 import {useHasOrganizationSentAnyReplayEvents} from 'sentry/utils/replays/hooks/useReplayOnboarding';
+import {projectCanLinkToReplay} from 'sentry/utils/replays/projectSupportsReplay';
+import useProjects from 'sentry/utils/useProjects';
 
 type Props = {
   event: Event;
@@ -13,6 +15,20 @@ type Props = {
   replayId: undefined | string;
 };
 
+function useProjectFromSlug({
+  organization,
+  projectSlug,
+}: {
+  organization: Organization;
+  projectSlug: string;
+}) {
+  const {fetching, projects} = useProjects({
+    slugs: [projectSlug],
+    orgId: organization.slug,
+  });
+  return fetching ? undefined : projects[0];
+}
+
 export default function EventReplay({replayId, organization, projectSlug, event}: Props) {
   const hasReplaysFeature = organization.features.includes('session-replay');
   const {hasOrgSentReplays, fetching} = useHasOrganizationSentAnyReplayEvents();
@@ -20,7 +36,10 @@ export default function EventReplay({replayId, organization, projectSlug, event}
   const onboardingPanel = useCallback(() => import('./replayInlineOnboardingPanel'), []);
   const replayPreview = useCallback(() => import('./replayPreview'), []);
 
-  if (!hasReplaysFeature || fetching) {
+  const project = useProjectFromSlug({organization, projectSlug});
+  const isReplayRelated = projectCanLinkToReplay(project);
+
+  if (!hasReplaysFeature || fetching || !isReplayRelated) {
     return null;
   }
 

+ 15 - 1
static/app/components/events/interfaces/breadcrumbs/breadcrumbs.spec.tsx

@@ -4,12 +4,14 @@ import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary
 import {textWithMarkupMatcher} from 'sentry-test/utils';
 
 import {Breadcrumbs} from 'sentry/components/events/interfaces/breadcrumbs';
+import {Project} from 'sentry/types';
 import {BreadcrumbLevelType, BreadcrumbType} from 'sentry/types/breadcrumbs';
 import {
   useHasOrganizationSentAnyReplayEvents,
   useReplayOnboardingSidebarPanel,
 } from 'sentry/utils/replays/hooks/useReplayOnboarding';
 import ReplayReader from 'sentry/utils/replays/replayReader';
+import useProjects from 'sentry/utils/useProjects';
 
 const mockReplay = ReplayReader.factory({
   replayRecord: TestStubs.ReplayRecord({}),
@@ -17,6 +19,7 @@ const mockReplay = ReplayReader.factory({
   attachments: TestStubs.ReplaySegmentInit({}),
 });
 
+jest.mock('sentry/utils/useProjects');
 jest.mock('sentry/utils/replays/hooks/useReplayOnboarding');
 
 jest.mock('screenfull', () => ({
@@ -43,6 +46,8 @@ jest.mock('sentry/utils/replays/hooks/useReplayData', () => {
 describe('Breadcrumbs', () => {
   let props: React.ComponentProps<typeof Breadcrumbs>;
 
+  const MockUseProjects = useProjects as jest.MockedFunction<typeof useProjects>;
+
   const MockUseReplayOnboardingSidebarPanel =
     useReplayOnboardingSidebarPanel as jest.MockedFunction<
       typeof useReplayOnboardingSidebarPanel
@@ -54,6 +59,15 @@ describe('Breadcrumbs', () => {
     >;
 
   beforeEach(() => {
+    MockUseProjects.mockReturnValue({
+      fetchError: null,
+      fetching: false,
+      hasMore: false,
+      initiallyLoaded: false,
+      onSearch: () => Promise.resolve(),
+      placeholders: [],
+      projects: [TestStubs.Project({platform: 'javascript'}) as Project],
+    });
     MockUseHasOrganizationSentAnyReplayEvents.mockReturnValue({
       hasOrgSentReplays: false,
       fetching: false,
@@ -61,6 +75,7 @@ describe('Breadcrumbs', () => {
     MockUseReplayOnboardingSidebarPanel.mockReturnValue({
       activateSidebar: jest.fn(),
     });
+
     props = {
       organization: TestStubs.Organization(),
       projectSlug: 'project-slug',
@@ -345,7 +360,6 @@ describe('Breadcrumbs', () => {
           }}
         />
       );
-
       const breadcrumbsBefore = screen.getAllByTestId(/crumb/i);
       expect(breadcrumbsBefore).toHaveLength(4); // Virtual exception crumb added to 3 in props
 

+ 58 - 0
static/app/utils/replays/projectSupportsReplay.spec.tsx

@@ -0,0 +1,58 @@
+import type {PlatformKey} from 'sentry/data/platformCategories';
+import type {MinimalProject} from 'sentry/types';
+import projectSupportsReplay, {
+  projectCanLinkToReplay,
+} from 'sentry/utils/replays/projectSupportsReplay';
+
+function mockProject(platform: PlatformKey): MinimalProject {
+  return {
+    id: '1',
+    slug: 'test-project',
+    platform,
+  };
+}
+
+describe('projectSupportsReplay & projectCanLinkToReplay', () => {
+  it.each([
+    'javascript-angular' as PlatformKey,
+    'javascript-nextjs' as PlatformKey,
+    'javascript-react' as PlatformKey,
+    'javascript' as PlatformKey,
+  ])('should SUPPORT & LINK frontend platform %s', platform => {
+    const project = mockProject(platform);
+    expect(projectSupportsReplay(project)).toBeTruthy();
+    expect(projectCanLinkToReplay(project)).toBeTruthy();
+  });
+
+  it.each(['javascript-angularjs' as PlatformKey])(
+    'should FAIL for old, unsupported frontend framework %s',
+    platform => {
+      const project = mockProject(platform);
+      expect(projectSupportsReplay(project)).toBeFalsy();
+      expect(projectCanLinkToReplay(project)).toBeFalsy();
+    }
+  );
+
+  it.each([
+    'dotnet' as PlatformKey,
+    'java' as PlatformKey,
+    'node' as PlatformKey,
+    'php' as PlatformKey,
+    'rust' as PlatformKey,
+  ])('should NOT SUPPORT but CAN LINK for Backend framework %s', platform => {
+    const project = mockProject(platform);
+    expect(projectSupportsReplay(project)).toBeFalsy();
+    expect(projectCanLinkToReplay(project)).toBeTruthy();
+  });
+
+  it.each([
+    'apple-macos' as PlatformKey,
+    'electron' as PlatformKey,
+    'flutter' as PlatformKey,
+    'unity' as PlatformKey,
+  ])('should FAIL for Desktop framework %s', platform => {
+    const project = mockProject(platform);
+    expect(projectSupportsReplay(project)).toBeFalsy();
+    expect(projectCanLinkToReplay(project)).toBeFalsy();
+  });
+});

+ 22 - 1
static/app/utils/replays/projectSupportsReplay.tsx

@@ -1,8 +1,29 @@
-import {replayPlatforms} from 'sentry/data/platformCategories';
+import {backend, replayPlatforms} from 'sentry/data/platformCategories';
 import type {MinimalProject} from 'sentry/types';
 
+/**
+ * Are you able to send a Replay into the project?
+ *
+ * Basically: is this a frontend project
+ */
 function projectSupportsReplay(project: MinimalProject) {
   return Boolean(project.platform && replayPlatforms.includes(project.platform));
 }
 
+/**
+ * Can this project be related to a Replay?
+ *
+ * Basically: is this a backend or frontend project
+ */
+export function projectCanLinkToReplay(project: undefined | MinimalProject) {
+  if (!project || !project.platform) {
+    return false;
+  }
+
+  return (
+    replayPlatforms.includes(project.platform) ||
+    backend.some(val => val === project.platform)
+  );
+}
+
 export default projectSupportsReplay;