Browse Source

feat(replays): Add link to View Replay next to the Trace Navigator on Issue Details (#43608)

This PR replaces the old 'View Replay' button on the Issue Details page
with a prefix for the trace navigator that shows a button to scroll down
and see the replay preview. Also we've added a text link to directly
open the Replay Details page without first scrolling or anything.

**Before:**
You'd see the button, and the trace navigator would be doing it's thing
(appearing or not)

![SCR-20230124-eey](https://user-images.githubusercontent.com/187460/214334630-4e736a2f-7b80-491d-9479-02ceaa65442a.png)

**After:**
Now you're getting a ReplayNode whenever the project is configured for
replays (if it's sent at least one replay). Also the "View Replay"
deeplink is there too.

![SCR-20230124-edg](https://user-images.githubusercontent.com/187460/214335117-873565a7-2c00-4616-8a8f-5b15b2eece11.png)

Some other variations of how it looks with Trace Navigator:

![SCR-20230124-ecv](https://user-images.githubusercontent.com/187460/214335170-f94b49a5-4886-43d7-9331-0456e1353eb3.png)

![SCR-20230124-ed3](https://user-images.githubusercontent.com/187460/214335171-1cd03b2d-038b-45f5-8009-d8bb9b245c18.png)

![SCR-20230124-ed9](https://user-images.githubusercontent.com/187460/214335172-29ea627d-2c05-4ec6-b3ab-0e217c4cd54b.png)

Fixes #39708
Ryan Albrecht 2 years ago
parent
commit
f0cdf95ee9

+ 2 - 1
static/app/components/quickTrace/styles.tsx

@@ -44,7 +44,8 @@ const nodeColors = (theme: Theme) => ({
   },
 });
 
-export const EventNode = styled(Tag)`
+export const EventNode = styled(Tag)<{disabled?: boolean}>`
+  cursor: ${p => (p.disabled ? 'default' : 'pointer')};
   span {
     display: flex;
     color: ${p => nodeColors(p.theme)[p.type || 'white'].color};

+ 30 - 32
static/app/views/issueDetails/eventToolbar.tsx

@@ -3,7 +3,6 @@ import styled from '@emotion/styled';
 import {Location} from 'history';
 import moment from 'moment-timezone';
 
-import {Button} from 'sentry/components/button';
 import DateTime from 'sentry/components/dateTime';
 import {DataSection} from 'sentry/components/events/styles';
 import FileSize from 'sentry/components/fileSize';
@@ -12,7 +11,7 @@ import ExternalLink from 'sentry/components/links/externalLink';
 import Link from 'sentry/components/links/link';
 import NavigationButtonGroup from 'sentry/components/navigationButtonGroup';
 import {Tooltip} from 'sentry/components/tooltip';
-import {IconPlay, IconWarning} from 'sentry/icons';
+import {IconWarning} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
 import {Group, Organization, Project} from 'sentry/types';
@@ -20,6 +19,9 @@ import {Event} from 'sentry/types/event';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import {shouldUse24Hours} from 'sentry/utils/dates';
 import getDynamicText from 'sentry/utils/getDynamicText';
+import LinkContainer from 'sentry/views/issueDetails/linkContainer';
+import ReplayLink from 'sentry/views/issueDetails/quickTrace/replayLink';
+import ReplayNode from 'sentry/views/issueDetails/quickTrace/replayNode';
 import {TraceLink} from 'sentry/views/issueDetails/quickTrace/traceLink';
 
 import EventCreatedTooltip from './eventCreatedTooltip';
@@ -31,7 +33,6 @@ type Props = {
   location: Location;
   organization: Organization;
   project: Project;
-  hasReplay?: boolean;
 };
 
 class GroupEventToolbar extends Component<Props> {
@@ -51,9 +52,11 @@ class GroupEventToolbar extends Component<Props> {
     const is24Hours = shouldUse24Hours();
     const evt = this.props.event;
 
-    const {group, organization, location, project, hasReplay} = this.props;
+    const {group, organization, location, project, event} = this.props;
     const groupId = group.id;
     const isReplayEnabled = organization.features.includes('session-replay-ui');
+    const projectHasReplay = project.hasReplays;
+    const replayId = event?.tags?.find(({key}) => key === 'replayId')?.value;
 
     const baseEventsPath = `/organizations/${organization.slug}/issues/${groupId}/events/`;
 
@@ -102,13 +105,16 @@ class GroupEventToolbar extends Component<Props> {
               {isOverLatencyThreshold && <StyledIconWarning color="warningText" />}
             </Tooltip>
             <TraceLink event={evt} />
+            {isReplayEnabled && projectHasReplay && replayId ? (
+              <ReplayLink
+                organization={organization}
+                projectSlug={project.slug}
+                replayId={replayId}
+                event={evt}
+              />
+            ) : null}
           </div>
           <NavigationContainer>
-            {hasReplay && isReplayEnabled ? (
-              <Button href="#breadcrumbs" size="sm" icon={<IconPlay size="xs" />}>
-                {t('Replay')}
-              </Button>
-            ) : null}
             <NavigationButtonGroup
               hasPrevious={!!evt.previousEventID}
               hasNext={!!evt.nextEventID}
@@ -138,12 +144,17 @@ class GroupEventToolbar extends Component<Props> {
             />
           </NavigationContainer>
         </HeadingAndNavWrapper>
-        <QuickTrace
-          event={evt}
-          group={group}
-          organization={organization}
-          location={location}
-        />
+        <TraceWithReplayWrapper>
+          {isReplayEnabled && projectHasReplay ? (
+            <ReplayNode hasReplay={Boolean(replayId)} />
+          ) : null}
+          <QuickTrace
+            event={evt}
+            group={group}
+            organization={organization}
+            location={location}
+          />
+        </TraceWithReplayWrapper>
         <StyledGlobalAppStoreConnectUpdateAlert
           project={project}
           organization={organization}
@@ -192,23 +203,6 @@ const StyledGlobalAppStoreConnectUpdateAlert = styled(GlobalAppStoreConnectUpdat
   margin: ${space(0.5)} 0;
 `;
 
-const LinkContainer = styled('span')`
-  margin-left: ${space(1)};
-  padding-left: ${space(1)};
-  position: relative;
-  font-weight: normal;
-
-  &:before {
-    display: block;
-    position: absolute;
-    content: '';
-    left: 0;
-    top: 2px;
-    height: 14px;
-    border-left: 1px solid ${p => p.theme.border};
-  }
-`;
-
 const NavigationContainer = styled('div')`
   display: flex;
   align-items: center;
@@ -216,4 +210,8 @@ const NavigationContainer = styled('div')`
   gap: 0 ${space(1)};
 `;
 
+const TraceWithReplayWrapper = styled('div')`
+  display: flex;
+`;
+
 export default GroupEventToolbar;

+ 0 - 3
static/app/views/issueDetails/groupEventDetails/groupEventDetails.tsx

@@ -239,8 +239,6 @@ class GroupEventDetails extends Component<GroupEventDetailsProps, State> {
     const {activity: activities} = group;
     const mostRecentActivity = getGroupMostRecentActivity(activities);
 
-    const hasReplay = Boolean(event?.tags?.find(({key}) => key === 'replayId')?.value);
-
     return (
       <TransactionProfileIdProvider
         projectId={event?.projectID}
@@ -276,7 +274,6 @@ class GroupEventDetails extends Component<GroupEventDetailsProps, State> {
                             organization={organization}
                             location={location}
                             project={project}
-                            hasReplay={hasReplay}
                           />
                         )}
                         {this.renderReprocessedBox(

+ 22 - 0
static/app/views/issueDetails/linkContainer.tsx

@@ -0,0 +1,22 @@
+import styled from '@emotion/styled';
+
+import space from 'sentry/styles/space';
+
+const LinkContainer = styled('span')`
+  margin-left: ${space(1)};
+  padding-left: ${space(1)};
+  position: relative;
+  font-weight: normal;
+
+  &:before {
+    display: block;
+    position: absolute;
+    content: '';
+    left: 0;
+    top: 2px;
+    height: 14px;
+    border-left: 1px solid ${p => p.theme.border};
+  }
+`;
+
+export default LinkContainer;

+ 35 - 0
static/app/views/issueDetails/quickTrace/replayLink.tsx

@@ -0,0 +1,35 @@
+import Link from 'sentry/components/links/link';
+import {t} from 'sentry/locale';
+import {Event, Organization} from 'sentry/types';
+import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
+import {useRoutes} from 'sentry/utils/useRoutes';
+import LinkContainer from 'sentry/views/issueDetails/linkContainer';
+
+type Props = {
+  organization: Organization;
+  projectSlug: string;
+  replayId: string;
+  event?: Event;
+};
+
+function ReplayLink({organization, projectSlug, replayId, event}: Props) {
+  const routes = useRoutes();
+
+  const replaySlug = `${projectSlug}:${replayId}`;
+  const fullReplayUrl = {
+    pathname: `/organizations/${organization.slug}/replays/${replaySlug}/`,
+    query: {
+      referrer: getRouteStringFromRoutes(routes),
+      t_main: 'console',
+      event_t: event?.dateCreated,
+    },
+  };
+
+  return (
+    <LinkContainer>
+      <Link to={fullReplayUrl}>{t('View Replay')}</Link>
+    </LinkContainer>
+  );
+}
+
+export default ReplayLink;

+ 45 - 0
static/app/views/issueDetails/quickTrace/replayNode.tsx

@@ -0,0 +1,45 @@
+import styled from '@emotion/styled';
+
+import {EventNode} from 'sentry/components/quickTrace/styles';
+import {IconPlay} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import space from 'sentry/styles/space';
+import {useLocation} from 'sentry/utils/useLocation';
+
+type Props = {
+  hasReplay: boolean;
+};
+
+function ReplayNode({hasReplay}: Props) {
+  const location = useLocation();
+  if (hasReplay) {
+    return (
+      <EventNodeReplay
+        icon={<IconPlay size="xs" />}
+        onClick={() => document.getElementById('breadcrumbs')?.scrollIntoView()}
+        to={{...location, hash: '#breadcrumbs'}}
+        type="black"
+      >
+        {t('Replay')}
+      </EventNodeReplay>
+    );
+  }
+  return (
+    <EventNodeReplay
+      disabled
+      icon={null}
+      tooltipText={t('Replay cannot be found')}
+      type="white"
+    >
+      {t('???')}
+    </EventNodeReplay>
+  );
+}
+
+export const EventNodeReplay = styled(EventNode)`
+  display: inline-flex;
+  margin-right: ${space(1)};
+  margin-top: ${space(0.5)};
+`;
+
+export default ReplayNode;

+ 1 - 18
static/app/views/issueDetails/quickTrace/traceLink.tsx

@@ -1,14 +1,13 @@
 import {useCallback, useContext} from 'react';
 import {Link} from 'react-router';
-import styled from '@emotion/styled';
 
 import {generateTraceTarget} from 'sentry/components/quickTrace/utils';
 import {t} from 'sentry/locale';
-import space from 'sentry/styles/space';
 import {Event} from 'sentry/types';
 import {trackAnalyticsEvent} from 'sentry/utils/analytics';
 import {QuickTraceContext} from 'sentry/utils/performance/quickTrace/quickTraceContext';
 import useOrganization from 'sentry/utils/useOrganization';
+import LinkContainer from 'sentry/views/issueDetails/linkContainer';
 
 type TraceLinkProps = {
   event: Event;
@@ -43,19 +42,3 @@ export function TraceLink({event}: TraceLinkProps) {
     </LinkContainer>
   );
 }
-
-const LinkContainer = styled('span')`
-  margin-left: ${space(1)};
-  padding-left: ${space(1)};
-  position: relative;
-
-  &:before {
-    display: block;
-    position: absolute;
-    content: '';
-    left: 0;
-    top: 2px;
-    height: 14px;
-    border-left: 1px solid ${p => p.theme.border};
-  }
-`;