Browse Source

feat(issue-details): Show static replay when error is not within the replay (#64827)

This covers an edge case when the error event is not within the bounds
of the replay.
Malachi Willey 1 year ago
parent
commit
acb068acde

+ 14 - 0
static/app/components/events/eventReplay/replayClipPreview.tsx

@@ -6,6 +6,7 @@ import {Alert} from 'sentry/components/alert';
 import {LinkButton} from 'sentry/components/button';
 import ButtonBar from 'sentry/components/buttonBar';
 import ErrorBoundary from 'sentry/components/errorBoundary';
+import {StaticReplayPreview} from 'sentry/components/events/eventReplay/staticReplayPreview';
 import Panel from 'sentry/components/panels/panel';
 import Placeholder from 'sentry/components/placeholder';
 import {Flex} from 'sentry/components/profiling/flex';
@@ -191,6 +192,19 @@ function ReplayClipPreview({
     );
   }
 
+  if (replay.getDurationMs() <= 0) {
+    return (
+      <StaticReplayPreview
+        analyticsContext={analyticsContext}
+        isFetching={false}
+        replay={replay}
+        replayId={replayId}
+        fullReplayButtonProps={fullReplayButtonProps}
+        initialTimeOffsetMs={0}
+      />
+    );
+  }
+
   return (
     <ReplayContextProvider
       isFetching={fetching}

+ 5 - 18
static/app/components/events/eventReplay/replayPreview.spec.tsx

@@ -9,8 +9,6 @@ import {render as baseRender, screen} from 'sentry-test/reactTestingLibrary';
 import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader';
 import ReplayReader from 'sentry/utils/replays/replayReader';
 import type RequestError from 'sentry/utils/requestError/requestError';
-import {OrganizationContext} from 'sentry/views/organizationContext';
-import {RouteContext} from 'sentry/views/routeContext';
 
 import ReplayPreview from './replayPreview';
 
@@ -65,7 +63,7 @@ mockUseReplayReader.mockImplementation(() => {
 });
 
 const render: typeof baseRender = children => {
-  const {router, routerContext} = initializeOrg({
+  const {routerContext} = initializeOrg({
     router: {
       routes: [
         {path: '/'},
@@ -79,21 +77,10 @@ const render: typeof baseRender = children => {
     },
   });
 
-  return baseRender(
-    <RouteContext.Provider
-      value={{
-        router,
-        location: router.location,
-        params: router.params,
-        routes: router.routes,
-      }}
-    >
-      <OrganizationContext.Provider value={OrganizationFixture()}>
-        {children}
-      </OrganizationContext.Provider>
-    </RouteContext.Provider>,
-    {context: routerContext}
-  );
+  return baseRender(children, {
+    context: routerContext,
+    organization: OrganizationFixture({slug: mockOrgSlug}),
+  });
 };
 
 const defaultProps = {

+ 12 - 70
static/app/components/events/eventReplay/replayPreview.tsx

@@ -1,26 +1,20 @@
 import type {ComponentProps} from 'react';
-import {Fragment, useMemo} from 'react';
+import {useMemo} from 'react';
 import styled from '@emotion/styled';
 
 import {Alert} from 'sentry/components/alert';
-import {LinkButton} from 'sentry/components/button';
+import type {LinkButton} from 'sentry/components/button';
+import {StaticReplayPreview} from 'sentry/components/events/eventReplay/staticReplayPreview';
 import Placeholder from 'sentry/components/placeholder';
 import {Flex} from 'sentry/components/profiling/flex';
 import MissingReplayAlert from 'sentry/components/replays/alerts/missingReplayAlert';
-import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
-import ReplayPlayer from 'sentry/components/replays/replayPlayer';
-import ReplayProcessingError from 'sentry/components/replays/replayProcessingError';
-import {IconDelete, IconPlay} from 'sentry/icons';
+import {IconDelete} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
-import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
-import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
+import type {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
 import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader';
 import type RequestError from 'sentry/utils/requestError/requestError';
 import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
-import {useRoutes} from 'sentry/utils/useRoutes';
-import {normalizeUrl} from 'sentry/utils/withDomainRequired';
-import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
 import type {ReplayRecord} from 'sentry/views/replays/types';
 
 type Props = {
@@ -62,7 +56,6 @@ function ReplayPreview({
   orgSlug,
   replaySlug,
 }: Props) {
-  const routes = useRoutes();
   const {fetching, replay, replayRecord, fetchError, replayId} = useReplayReader({
     orgSlug,
     replaySlug,
@@ -106,70 +99,19 @@ function ReplayPreview({
     );
   }
 
-  const fullReplayUrl = {
-    pathname: normalizeUrl(`/organizations/${orgSlug}/replays/${replayId}/`),
-    query: {
-      referrer: getRouteStringFromRoutes(routes),
-      t_main: focusTab ?? TabKey.ERRORS,
-      t: initialTimeOffsetMs / 1000,
-    },
-  };
-
   return (
-    <ReplayContextProvider
+    <StaticReplayPreview
+      focusTab={focusTab}
       isFetching={fetching}
-      replay={replay}
-      initialTimeOffsetMs={{offsetMs: initialTimeOffsetMs}}
       analyticsContext={analyticsContext}
-    >
-      <PlayerContainer data-test-id="player-container">
-        {replay?.hasProcessingErrors() ? (
-          <ReplayProcessingError processingErrors={replay.processingErrors()} />
-        ) : (
-          <Fragment>
-            <StaticPanel>
-              <ReplayPlayer isPreview />
-            </StaticPanel>
-
-            <CTAOverlay>
-              <LinkButton
-                {...fullReplayButtonProps}
-                icon={<IconPlay />}
-                priority="primary"
-                to={fullReplayUrl}
-              >
-                {t('Open Replay')}
-              </LinkButton>
-            </CTAOverlay>
-          </Fragment>
-        )}
-      </PlayerContainer>
-    </ReplayContextProvider>
+      replay={replay}
+      replayId={replayId}
+      fullReplayButtonProps={fullReplayButtonProps}
+      initialTimeOffsetMs={initialTimeOffsetMs}
+    />
   );
 }
 
-const PlayerContainer = styled(FluidHeight)`
-  position: relative;
-  background: ${p => p.theme.background};
-  gap: ${space(1)};
-  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 StyledPlaceholder = styled(Placeholder)`
   margin-bottom: ${space(2)};
 `;

+ 108 - 0
static/app/components/events/eventReplay/staticReplayPreview.tsx

@@ -0,0 +1,108 @@
+import {type ComponentProps, Fragment, useMemo} from 'react';
+import styled from '@emotion/styled';
+
+import {LinkButton} from 'sentry/components/button';
+import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
+import ReplayPlayer from 'sentry/components/replays/replayPlayer';
+import ReplayProcessingError from 'sentry/components/replays/replayProcessingError';
+import {IconPlay} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
+import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
+import type ReplayReader from 'sentry/utils/replays/replayReader';
+import useOrganization from 'sentry/utils/useOrganization';
+import {useRoutes} from 'sentry/utils/useRoutes';
+import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
+
+type StaticReplayPreviewProps = {
+  analyticsContext: string;
+  initialTimeOffsetMs: number;
+  isFetching: boolean;
+  replay: ReplayReader | null;
+  replayId: string;
+  focusTab?: TabKey;
+  fullReplayButtonProps?: Partial<ComponentProps<typeof LinkButton>>;
+};
+
+export function StaticReplayPreview({
+  analyticsContext,
+  initialTimeOffsetMs,
+  isFetching,
+  focusTab,
+  replayId,
+  fullReplayButtonProps,
+  replay,
+}: StaticReplayPreviewProps) {
+  const organization = useOrganization();
+  const routes = useRoutes();
+  const fullReplayUrl = {
+    pathname: `/organizations/${organization.slug}/replays/${replayId}/`,
+    query: {
+      referrer: getRouteStringFromRoutes(routes),
+      t_main: focusTab ?? TabKey.ERRORS,
+      t: initialTimeOffsetMs / 1000,
+    },
+  };
+
+  const offset = useMemo(
+    () => ({
+      offsetMs: initialTimeOffsetMs,
+    }),
+    [initialTimeOffsetMs]
+  );
+
+  return (
+    <ReplayContextProvider
+      isFetching={isFetching}
+      replay={replay}
+      initialTimeOffsetMs={offset}
+      analyticsContext={analyticsContext}
+    >
+      <PlayerContainer data-test-id="player-container">
+        {replay?.hasProcessingErrors() ? (
+          <ReplayProcessingError processingErrors={replay.processingErrors()} />
+        ) : (
+          <Fragment>
+            <StaticPanel>
+              <ReplayPlayer isPreview />
+            </StaticPanel>
+
+            <CTAOverlay>
+              <LinkButton
+                {...fullReplayButtonProps}
+                icon={<IconPlay />}
+                priority="primary"
+                to={fullReplayUrl}
+              >
+                {t('Open Replay')}
+              </LinkButton>
+            </CTAOverlay>
+          </Fragment>
+        )}
+      </PlayerContainer>
+    </ReplayContextProvider>
+  );
+}
+
+const PlayerContainer = styled(FluidHeight)`
+  position: relative;
+  background: ${p => p.theme.background};
+  gap: ${space(1)};
+  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);
+`;