Browse Source

ref(replay): move configuration resources card and remove old banners (#64095)

The card appears **on hover:**

<img width="412" alt="SCR-20240129-lchy"
src="https://github.com/getsentry/sentry/assets/56095982/376f2634-9e94-4fba-8116-92c4164202e4">


https://github.com/getsentry/sentry/assets/56095982/40a5d149-4712-4229-a5d7-668c89652f64

- removed the "learn how to unmask..." banner on breadcrumbs tab
- removed the network setup banner on network tab
- relocated the configuration resource docs to the top right of replay
details (now includes an additional resource about canvas)
- analytics are being tracked for each resource clicked

followup needed: https://github.com/getsentry/team-replay/issues/370
Michelle Zhang 1 year ago
parent
commit
2987f05a5d

+ 126 - 0
static/app/components/replays/configureReplayCard.tsx

@@ -0,0 +1,126 @@
+import type {ReactNode} from 'react';
+import {ClassNames} from '@emotion/react';
+import styled from '@emotion/styled';
+
+import {Button, LinkButton} from 'sentry/components/button';
+import {Hovercard} from 'sentry/components/hovercard';
+import {IconOpen, IconQuestion} from 'sentry/icons';
+import {t, tct} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {trackAnalytics} from 'sentry/utils/analytics';
+import useOrganization from 'sentry/utils/useOrganization';
+
+function Resource({
+  title,
+  subtitle,
+  link,
+}: {
+  link: string;
+  subtitle: ReactNode;
+  title: string;
+}) {
+  const organization = useOrganization();
+  return (
+    <StyledLinkButton
+      icon={<IconOpen />}
+      borderless
+      external
+      href={link}
+      onClick={() => {
+        trackAnalytics('replay.details-resource-docs-clicked', {
+          organization,
+          title,
+        });
+      }}
+    >
+      <ButtonContent>
+        <ButtonTitle>{title}</ButtonTitle>
+        <ButtonSubtitle>{subtitle}</ButtonSubtitle>
+      </ButtonContent>
+    </StyledLinkButton>
+  );
+}
+
+function ResourceButtons() {
+  return (
+    <ButtonContainer>
+      <Resource
+        title={t('General')}
+        subtitle={t('Configure sampling rates and recording thresholds')}
+        link="https://docs.sentry.io/platforms/javascript/session-replay/configuration/#general-integration-configuration"
+      />
+      <Resource
+        title={t('Element Masking/Blocking')}
+        subtitle={t('Unmask text (****) and unblock media (img, svg, video, etc.)')}
+        link="https://docs.sentry.io/platforms/javascript/session-replay/privacy/"
+      />
+      <Resource
+        title={t('Network Details')}
+        subtitle={t('Capture request and response headers or bodies')}
+        link="https://docs.sentry.io/platforms/javascript/session-replay/configuration/#network-details"
+      />
+      <Resource
+        title={t('Canvas Support')}
+        subtitle={tct(
+          'Opt-in to record HTML [code:canvas] elements, added in SDK version 7.98.0',
+          {code: <code />}
+        )}
+        link="https://docs.sentry.io/platforms/javascript/session-replay/#canvas-recording"
+      />
+    </ButtonContainer>
+  );
+}
+
+export default function ConfigureReplayCard() {
+  return (
+    <ClassNames>
+      {({css}) => (
+        <Hovercard
+          body={<ResourceButtons />}
+          bodyClassName={css`
+              padding: ${space(1)};
+            `}
+          position="top-end"
+        >
+          <Button
+            size="sm"
+            icon={<IconQuestion />}
+            aria-label={t('replay configure resources')}
+          >
+            {t('Configure Replay')}
+          </Button>
+        </Hovercard>
+      )}
+    </ClassNames>
+  );
+}
+
+const ButtonContainer = styled('div')`
+  display: flex;
+  flex-direction: column;
+  gap: ${space(1)};
+  align-items: flex-start;
+`;
+
+const ButtonContent = styled('div')`
+  display: flex;
+  flex-direction: column;
+  text-align: left;
+  white-space: pre-line;
+  gap: ${space(0.25)};
+`;
+
+const ButtonTitle = styled('div')`
+  font-weight: normal;
+`;
+
+const ButtonSubtitle = styled('div')`
+  color: ${p => p.theme.gray300};
+  font-weight: normal;
+  font-size: ${p => p.theme.fontSizeSmall};
+`;
+
+const StyledLinkButton = styled(LinkButton)`
+  padding: ${space(1)};
+  height: auto;
+`;

+ 0 - 119
static/app/components/replays/replayPlayer.tsx

@@ -1,21 +1,14 @@
 import {useCallback, useEffect, useRef, useState} from 'react';
-import {ClassNames} from '@emotion/react';
 import styled from '@emotion/styled';
 import {useResizeObserver} from '@react-aria/utils';
 
-import {Button, LinkButton} from 'sentry/components/button';
 import NegativeSpaceContainer from 'sentry/components/container/negativeSpaceContainer';
-import {Hovercard} from 'sentry/components/hovercard';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import BufferingOverlay from 'sentry/components/replays/player/bufferingOverlay';
 import FastForwardBadge from 'sentry/components/replays/player/fastForwardBadge';
 import {useReplayContext} from 'sentry/components/replays/replayContext';
-import {IconOpen, IconQuestion} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import useOrganization from 'sentry/utils/useOrganization';
-import useIsFullscreen from 'sentry/utils/window/useIsFullscreen';
 
 import PlayerDOMAlert from './playerDOMAlert';
 
@@ -79,7 +72,6 @@ function BasePlayerRoot({className, isPreview = false}: Props) {
   });
 
   useVideoSizeLogger({videoDimensions, windowDimensions});
-  const isFullscreen = useIsFullscreen();
 
   // Create the `rrweb` instance which creates an iframe inside `viewEl`
   useEffect(() => initRoot(viewEl.current), [initRoot]);
@@ -121,80 +113,6 @@ function BasePlayerRoot({className, isPreview = false}: Props) {
     }
   }, [windowDimensions, videoDimensions]);
 
-  function Resource({
-    title,
-    subtitle,
-    link,
-  }: {
-    link: string;
-    subtitle: string;
-    title: string;
-  }) {
-    const organization = useOrganization();
-    return (
-      <StyledLinkButton
-        icon={<IconOpen />}
-        borderless
-        external
-        href={link}
-        onClick={() => {
-          trackAnalytics('replay.details-resource-docs-clicked', {
-            organization,
-            title,
-          });
-        }}
-      >
-        <ButtonContent>
-          <ButtonTitle>{title}</ButtonTitle>
-          <ButtonSubtitle>{subtitle}</ButtonSubtitle>
-        </ButtonContent>
-      </StyledLinkButton>
-    );
-  }
-
-  function ResourceButtons() {
-    return (
-      <ButtonContainer>
-        <Resource
-          title={t('General')}
-          subtitle={t('Configure sampling rates and recording thresholds')}
-          link="https://docs.sentry.io/platforms/javascript/session-replay/configuration/#general-integration-configuration"
-        />
-        <Resource
-          title={t('Element Masking/Blocking')}
-          subtitle={t('Unmask text (****) and unblock media (img, svg, video, etc.)')}
-          link="https://docs.sentry.io/platforms/javascript/session-replay/privacy/"
-        />
-        <Resource
-          title={t('Network Details')}
-          subtitle={t('Capture request and response headers or bodies')}
-          link="https://docs.sentry.io/platforms/javascript/session-replay/configuration/#network-details"
-        />
-      </ButtonContainer>
-    );
-  }
-
-  function ResourceCard() {
-    return (
-      <ResourceCardContainer>
-        <ClassNames>
-          {({css}) => (
-            <Hovercard
-              body={<ResourceButtons />}
-              bodyClassName={css`
-                padding: ${space(1)};
-              `}
-              header={t('Documentation Resources')}
-              position="top-end"
-            >
-              <Button icon={<IconQuestion />} aria-label={t('replay resources')} />
-            </Hovercard>
-          )}
-        </ClassNames>
-      </ResourceCardContainer>
-    );
-  }
-
   return (
     <NegativeSpaceContainer ref={windowEl} className="sentry-block">
       <div ref={viewEl} className={className} />
@@ -202,7 +120,6 @@ function BasePlayerRoot({className, isPreview = false}: Props) {
       {isBuffering ? <PositionedBuffering /> : null}
       {isPreview ? null : <PlayerDOMAlert />}
       {isFetching ? <PositionedLoadingIndicator /> : null}
-      {!(isFullscreen || isPreview) && <ResourceCard />}
     </NegativeSpaceContainer>
   );
 }
@@ -336,40 +253,4 @@ const SentryPlayerRoot = styled(PlayerRoot)`
   }
 `;
 
-const ResourceCardContainer = styled('div')`
-  position: absolute;
-  bottom: ${space(1)};
-  right: 8px;
-`;
-
-const ButtonContainer = styled('div')`
-  display: flex;
-  flex-direction: column;
-  gap: ${space(1)};
-  align-items: flex-start;
-`;
-
-const ButtonContent = styled('div')`
-  display: flex;
-  flex-direction: column;
-  text-align: left;
-  white-space: pre-line;
-  gap: ${space(0.25)};
-`;
-
-const ButtonTitle = styled('div')`
-  font-weight: normal;
-`;
-
-const ButtonSubtitle = styled('div')`
-  color: ${p => p.theme.gray300};
-  font-weight: normal;
-  font-size: ${p => p.theme.fontSizeSmall};
-`;
-
-const StyledLinkButton = styled(LinkButton)`
-  padding: ${space(1)};
-  height: auto;
-`;
-
 export default SentryPlayerRoot;

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

@@ -26,7 +26,6 @@ export type ReplayEventParameters = {
     chosen_layout: LayoutKey;
     default_layout: LayoutKey;
   };
-  'replay.details-mask-banner-link-clicked': {};
   'replay.details-network-panel-closed': {
     is_sdk_setup: boolean;
   };
@@ -118,7 +117,6 @@ export const replayEventMap: Record<ReplayEventKey, string | null> = {
   'replay.details-data-loaded': 'Replay Details Data Loaded',
   'replay.details-has-hydration-error': 'Replay Details Has Hydration Error',
   'replay.details-layout-changed': 'Changed Replay Details Layout',
-  'replay.details-mask-banner-link-clicked': 'Clicked Replay Details Masking Banner Link',
   'replay.details-network-panel-closed': 'Closed Replay Network Details Panel',
   'replay.details-network-panel-opened': 'Opened Replay Network Details Panel',
   'replay.details-network-tab-changed': 'Changed Replay Network Details Tab',

+ 1 - 44
static/app/views/replays/detail/breadcrumbs/index.tsx

@@ -1,22 +1,14 @@
 import {useEffect, useMemo, useRef, useState} from 'react';
 import type {ListRowProps} from 'react-virtualized';
 import {AutoSizer, CellMeasurer, List as ReactVirtualizedList} from 'react-virtualized';
-import styled from '@emotion/styled';
 
-import Alert from 'sentry/components/alert';
-import {Button} from 'sentry/components/button';
-import ExternalLink from 'sentry/components/links/externalLink';
 import Placeholder from 'sentry/components/placeholder';
 import JumpButtons from 'sentry/components/replays/jumpButtons';
 import {useReplayContext} from 'sentry/components/replays/replayContext';
 import useJumpButtons from 'sentry/components/replays/useJumpButtons';
-import {IconClose} from 'sentry/icons';
-import {t, tct} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
-import {trackAnalytics} from 'sentry/utils/analytics';
+import {t} from 'sentry/locale';
 import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
 import useExtractedDomNodes from 'sentry/utils/replays/hooks/useExtractedDomNodes';
-import useDismissAlert from 'sentry/utils/useDismissAlert';
 import useOrganization from 'sentry/utils/useOrganization';
 import useVirtualizedInspector from 'sentry/views/replays/detail//useVirtualizedInspector';
 import BreadcrumbFilters from 'sentry/views/replays/detail/breadcrumbs/breadcrumbFilters';
@@ -31,8 +23,6 @@ import TabItemContainer from 'sentry/views/replays/detail/tabItemContainer';
 import useVirtualizedList from 'sentry/views/replays/detail/useVirtualizedList';
 import useVirtualListDimentionChange from 'sentry/views/replays/detail/useVirtualListDimentionChange';
 
-const LOCAL_STORAGE_KEY = 'replay-details-mask-config-instructions-dismissed';
-
 // Ensure this object is created once as it is an input to
 // `useVirtualizedList`'s memoization
 const cellMeasurer = {
@@ -41,7 +31,6 @@ const cellMeasurer = {
 };
 
 function Breadcrumbs() {
-  const {dismiss, isDismissed} = useDismissAlert({key: LOCAL_STORAGE_KEY});
   const {currentTime, replay, startTimeOffsetMs, durationMs} = useReplayContext();
   const organization = useOrganization();
   const hasPerfTab = organization.features.includes('session-replay-trace-table');
@@ -144,34 +133,6 @@ function Breadcrumbs() {
       <FilterLoadingIndicator isLoading={isFetchingExtractions || isFetchingTraces}>
         <BreadcrumbFilters frames={frames} {...filterProps} />
       </FilterLoadingIndicator>
-      {isDismissed ? null : (
-        <StyledAlert
-          type="info"
-          showIcon
-          trailingItems={
-            <Button
-              aria-label={t('Dismiss banner')}
-              icon={<IconClose />}
-              onClick={dismiss}
-              size="zero"
-              borderless
-            />
-          }
-        >
-          {tct('Learn how to unmask text (****) and unblock media [link:here].', {
-            link: (
-              <ExternalLink
-                href="https://docs.sentry.io/platforms/javascript/session-replay/privacy/"
-                onClick={() => {
-                  trackAnalytics('replay.details-mask-banner-link-clicked', {
-                    organization,
-                  });
-                }}
-              />
-            ),
-          })}
-        </StyledAlert>
-      )}
       <TabItemContainer data-test-id="replay-details-breadcrumbs-tab">
         {frames ? (
           <AutoSizer onResize={updateList}>
@@ -218,8 +179,4 @@ function Breadcrumbs() {
   );
 }
 
-const StyledAlert = styled(Alert)`
-  margin-bottom: ${space(1)};
-`;
-
 export default Breadcrumbs;

+ 0 - 64
static/app/views/replays/detail/network/details/onboarding.tsx

@@ -1,11 +1,9 @@
 import styled from '@emotion/styled';
 
 import Alert from 'sentry/components/alert';
-import {Button} from 'sentry/components/button';
 import {CodeSnippet} from 'sentry/components/codeSnippet';
 import ExternalLink from 'sentry/components/links/externalLink';
 import TextCopyInput from 'sentry/components/textCopyInput';
-import {IconClose, IconInfo} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {SpanFrame} from 'sentry/utils/replays/types';
@@ -22,68 +20,6 @@ export const useDismissReqRespBodiesAlert = () => {
   });
 };
 
-export function ReqRespBodiesAlert({
-  isNetworkDetailsSetup,
-}: {
-  isNetworkDetailsSetup: boolean;
-}) {
-  const {dismiss, isDismissed} = useDismissReqRespBodiesAlert();
-
-  if (isDismissed) {
-    return null;
-  }
-
-  const message = isNetworkDetailsSetup
-    ? tct(
-        'Click on a [fetch] or [xhr] request to see request and response bodies. [link].',
-        {
-          fetch: <code>fetch</code>,
-          xhr: <code>xhr</code>,
-          link: (
-            <ExternalLink
-              href="https://docs.sentry.io/platforms/javascript/session-replay/configuration/#network-details"
-              onClick={dismiss}
-            >
-              {t('Learn More')}
-            </ExternalLink>
-          ),
-        }
-      )
-    : tct('Start collecting the body of requests and responses. [link].', {
-        link: (
-          <ExternalLink
-            href="https://docs.sentry.io/platforms/javascript/session-replay/configuration/#network-details"
-            onClick={dismiss}
-          >
-            {t('Learn More')}
-          </ExternalLink>
-        ),
-      });
-  return (
-    <StyledAlert
-      icon={<IconInfo />}
-      opaque={false}
-      showIcon
-      type="info"
-      trailingItems={
-        <StyledButton priority="link" size="sm" onClick={dismiss}>
-          <IconClose color="gray500" size="sm" />
-        </StyledButton>
-      }
-    >
-      {message}
-    </StyledAlert>
-  );
-}
-
-const StyledAlert = styled(Alert)`
-  margin-bottom: ${space(1)};
-`;
-
-const StyledButton = styled(Button)`
-  color: inherit;
-`;
-
 export function UnsupportedOp({type}: {type: 'headers' | 'bodies'}) {
   const title =
     type === 'bodies'

+ 0 - 2
static/app/views/replays/detail/network/index.tsx

@@ -18,7 +18,6 @@ import useOrganization from 'sentry/utils/useOrganization';
 import FilterLoadingIndicator from 'sentry/views/replays/detail/filterLoadingIndicator';
 import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
 import NetworkDetails from 'sentry/views/replays/detail/network/details';
-import {ReqRespBodiesAlert} from 'sentry/views/replays/detail/network/details/onboarding';
 import NetworkFilters from 'sentry/views/replays/detail/network/networkFilters';
 import NetworkHeaderCell, {
   COLUMN_COUNT,
@@ -162,7 +161,6 @@ function NetworkList() {
       <FilterLoadingIndicator isLoading={!replay}>
         <NetworkFilters networkFrames={networkFrames} {...filterProps} />
       </FilterLoadingIndicator>
-      <ReqRespBodiesAlert isNetworkDetailsSetup={isNetworkDetailsSetup} />
       <GridTable ref={containerRef} data-test-id="replay-details-network-tab">
         <SplitPanel
           style={{

+ 2 - 0
static/app/views/replays/detail/page.tsx

@@ -4,6 +4,7 @@ import styled from '@emotion/styled';
 import UserBadge from 'sentry/components/idBadge/userBadge';
 import FullViewport from 'sentry/components/layouts/fullViewport';
 import * as Layout from 'sentry/components/layouts/thirds';
+import ConfigureReplayCard from 'sentry/components/replays/configureReplayCard';
 import DeleteButton from 'sentry/components/replays/header/deleteButton';
 import DetailsPageBreadcrumbs from 'sentry/components/replays/header/detailsPageBreadcrumbs';
 import FeedbackButton from 'sentry/components/replays/header/feedbackButton';
@@ -39,6 +40,7 @@ function Page({children, orgSlug, replayRecord, projectSlug, replayErrors}: Prop
       <ButtonActionsWrapper>
         <ShareButton />
         <FeedbackButton />
+        <ConfigureReplayCard />
         {replayRecord?.id && projectSlug && (
           <DeleteButton replayId={replayRecord.id} projectSlug={projectSlug} />
         )}