Browse Source

feat(hydration error): Improve rendering of hydration error diff modal (#77206)

This updates the Hydration Error diff modal to be taller, and so that
the Slider tool scales inside the available space

<img width="1618" alt="SCR-20240909-ngvx"
src="https://github.com/user-attachments/assets/06ed9d26-5c6b-4fba-9628-c8b8cbbec4e3">
Ryan Albrecht 6 months ago
parent
commit
100579f3e2

+ 1 - 0
static/app/components/events/eventHydrationDiff/replayDiffContent.tsx

@@ -54,6 +54,7 @@ export default function ReplayDiffContent({event, group, orgSlug, replaySlug}: P
       <ErrorBoundary mini>
       <ErrorBoundary mini>
         <ReplayGroupContextProvider groupId={group?.id} eventId={event.id}>
         <ReplayGroupContextProvider groupId={group?.id} eventId={event.id}>
           <ReplaySliderDiff
           <ReplaySliderDiff
+            minHeight="355px"
             leftOffsetMs={leftOffsetMs}
             leftOffsetMs={leftOffsetMs}
             replay={replay}
             replay={replay}
             rightOffsetMs={rightOffsetMs}
             rightOffsetMs={rightOffsetMs}

+ 14 - 3
static/app/components/replays/breadcrumbs/openReplayComparisonButton.tsx

@@ -78,7 +78,18 @@ export function OpenReplayComparisonButton({
 }
 }
 
 
 const modalCss = css`
 const modalCss = css`
-  width: 95vw;
-  min-height: 80vh;
-  max-height: 95vh;
+  /* Swap typical modal margin and padding
+   * We want a minimal space around the modal (hence, 30px 16px)
+   * But this space should also be clickable, so it's not the padding.
+   */
+  margin: 30px 16px !important;
+  padding: 0 !important;
+  height: calc(100% - 60px);
+  width: calc(100% - 32px);
+  display: flex;
+  & > * {
+    flex-grow: 1;
+    display: grid;
+    grid-template-rows: max-content 1fr;
+  }
 `;
 `;

+ 37 - 14
static/app/components/replays/breadcrumbs/replayComparisonModal.tsx

@@ -1,11 +1,12 @@
 import styled from '@emotion/styled';
 import styled from '@emotion/styled';
 
 
 import type {ModalRenderProps} from 'sentry/actionCreators/modal';
 import type {ModalRenderProps} from 'sentry/actionCreators/modal';
+import Alert from 'sentry/components/alert';
 import FeatureBadge from 'sentry/components/badge/featureBadge';
 import FeatureBadge from 'sentry/components/badge/featureBadge';
 import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
 import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
 import {useGlobalModal} from 'sentry/components/globalModal/useGlobalModal';
 import {useGlobalModal} from 'sentry/components/globalModal/useGlobalModal';
 import ReplayDiffChooser from 'sentry/components/replays/diff/replayDiffChooser';
 import ReplayDiffChooser from 'sentry/components/replays/diff/replayDiffChooser';
-import {tct} from 'sentry/locale';
+import {t, tct} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {space} from 'sentry/styles/space';
 import type {Organization} from 'sentry/types/organization';
 import type {Organization} from 'sentry/types/organization';
 import type ReplayReader from 'sentry/utils/replays/replayReader';
 import type ReplayReader from 'sentry/utils/replays/replayReader';
@@ -30,6 +31,8 @@ export default function ReplayComparisonModal({
   // We need these to interact with feedback opened while a modal is active.
   // We need these to interact with feedback opened while a modal is active.
   const {focusTrap} = useGlobalModal();
   const {focusTrap} = useGlobalModal();
 
 
+  const isSameTimestamp = leftOffsetMs === rightOffsetMs;
+
   return (
   return (
     <OrganizationContext.Provider value={organization}>
     <OrganizationContext.Provider value={organization}>
       <Header closeButton>
       <Header closeButton>
@@ -53,20 +56,33 @@ export default function ReplayComparisonModal({
         </ModalHeader>
         </ModalHeader>
       </Header>
       </Header>
       <Body>
       <Body>
-        <StyledParagraph>
-          {tct(
-            'This modal helps with debugging hydration errors by diffing the dom before and after the app hydrated. [boldBefore:Before Hydration] refers to the html rendered on the server. [boldAfter:After Hydration] refers to the html rendered on the client. This feature is actively being developed; please share any questions or feedback to the discussion linked above.',
-            {
-              boldBefore: <strong />,
-              boldAfter: <strong />,
-            }
+        <Grid>
+          <StyledParagraph>
+            {tct(
+              'This modal helps with debugging hydration errors by diffing the dom before and after the app hydrated. [boldBefore:Before Hydration] refers to the html rendered on the server. [boldAfter:After Hydration] refers to the html rendered on the client. This feature is actively being developed; please share any questions or feedback to the discussion linked above.',
+              {
+                boldBefore: <strong />,
+                boldAfter: <strong />,
+              }
+            )}
+          </StyledParagraph>
+
+          {isSameTimestamp ? (
+            <Alert type="warning" showIcon>
+              {t(
+                "Cannot display diff for this hydration error. Sentry wasn't able to identify the correct event."
+              )}
+            </Alert>
+          ) : (
+            <div />
           )}
           )}
-        </StyledParagraph>
-        <ReplayDiffChooser
-          replay={replay}
-          leftOffsetMs={leftOffsetMs}
-          rightOffsetMs={rightOffsetMs}
-        />
+
+          <ReplayDiffChooser
+            replay={replay}
+            leftOffsetMs={leftOffsetMs}
+            rightOffsetMs={rightOffsetMs}
+          />
+        </Grid>
       </Body>
       </Body>
     </OrganizationContext.Provider>
     </OrganizationContext.Provider>
   );
   );
@@ -79,6 +95,13 @@ const ModalHeader = styled('div')`
   flex-direction: row;
   flex-direction: row;
 `;
 `;
 
 
+const Grid = styled('div')`
+  height: 100%;
+  display: grid;
+  grid-template-rows: max-content max-content 1fr;
+  align-items: start;
+`;
+
 const StyledParagraph = styled('p')`
 const StyledParagraph = styled('p')`
   padding-top: ${space(0.5)};
   padding-top: ${space(0.5)};
   margin-bottom: ${space(1)};
   margin-bottom: ${space(1)};

+ 13 - 18
static/app/components/replays/diff/replayDiffChooser.tsx

@@ -1,11 +1,10 @@
-import Alert from 'sentry/components/alert';
-import {Flex} from 'sentry/components/container/flex';
+import styled from '@emotion/styled';
+
 import {ReplaySideBySideImageDiff} from 'sentry/components/replays/diff/replaySideBySideImageDiff';
 import {ReplaySideBySideImageDiff} from 'sentry/components/replays/diff/replaySideBySideImageDiff';
 import {ReplaySliderDiff} from 'sentry/components/replays/diff/replaySliderDiff';
 import {ReplaySliderDiff} from 'sentry/components/replays/diff/replaySliderDiff';
 import {ReplayTextDiff} from 'sentry/components/replays/diff/replayTextDiff';
 import {ReplayTextDiff} from 'sentry/components/replays/diff/replayTextDiff';
-import {TabList, TabPanels, Tabs} from 'sentry/components/tabs';
+import {TabList, TabPanels, TabStateProvider} from 'sentry/components/tabs';
 import {t} from 'sentry/locale';
 import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import type ReplayReader from 'sentry/utils/replays/replayReader';
 import type ReplayReader from 'sentry/utils/replays/replayReader';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
@@ -29,24 +28,14 @@ export default function ReplayDiffChooser({
   replay,
   replay,
   rightOffsetMs,
   rightOffsetMs,
 }: Props) {
 }: Props) {
-  const isSameTimestamp = leftOffsetMs === rightOffsetMs;
-
   const organization = useOrganization();
   const organization = useOrganization();
   const onTabChange = (tabKey: DiffType) => {
   const onTabChange = (tabKey: DiffType) => {
     trackAnalytics('replay.hydration-modal.tab-change', {tabKey, organization});
     trackAnalytics('replay.hydration-modal.tab-change', {tabKey, organization});
   };
   };
 
 
   return (
   return (
-    <Tabs<DiffType> defaultValue={defaultTab} onChange={onTabChange}>
-      {isSameTimestamp ? (
-        <Alert type="warning" showIcon>
-          {t(
-            "Sentry wasn't able to identify the correct event to display a diff for this hydration error."
-          )}
-        </Alert>
-      ) : null}
-
-      <Flex gap={space(1)} column>
+    <Grid>
+      <TabStateProvider<DiffType> defaultValue={defaultTab} onChange={onTabChange}>
         <TabList>
         <TabList>
           <TabList.Item key={DiffType.SLIDER}>{t('Slider Diff')}</TabList.Item>
           <TabList.Item key={DiffType.SLIDER}>{t('Slider Diff')}</TabList.Item>
           <TabList.Item key={DiffType.VISUAL}>{t('Side By Side Diff')}</TabList.Item>
           <TabList.Item key={DiffType.VISUAL}>{t('Side By Side Diff')}</TabList.Item>
@@ -76,7 +65,13 @@ export default function ReplayDiffChooser({
             />
             />
           </TabPanels.Item>
           </TabPanels.Item>
         </TabPanels>
         </TabPanels>
-      </Flex>
-    </Tabs>
+      </TabStateProvider>
+    </Grid>
   );
   );
 }
 }
+
+const Grid = styled('div')`
+  display: grid;
+  grid-template-rows: max-content 1fr;
+  height: 100%;
+`;

+ 15 - 7
static/app/components/replays/diff/replaySliderDiff.tsx

@@ -21,13 +21,20 @@ interface Props {
   leftOffsetMs: number;
   leftOffsetMs: number;
   replay: null | ReplayReader;
   replay: null | ReplayReader;
   rightOffsetMs: number;
   rightOffsetMs: number;
+  minHeight?: `${number}px` | `${number}%`;
 }
 }
 
 
-export function ReplaySliderDiff({leftOffsetMs, replay, rightOffsetMs}: Props) {
+export function ReplaySliderDiff({
+  minHeight = '0px',
+  leftOffsetMs,
+  replay,
+  rightOffsetMs,
+}: Props) {
   const positionedRef = useRef<HTMLDivElement>(null);
   const positionedRef = useRef<HTMLDivElement>(null);
   const viewDimensions = useDimensions({elementRef: positionedRef});
   const viewDimensions = useDimensions({elementRef: positionedRef});
 
 
   const width = toPixels(viewDimensions.width);
   const width = toPixels(viewDimensions.width);
+
   return (
   return (
     <Fragment>
     <Fragment>
       <Header>
       <Header>
@@ -43,7 +50,7 @@ export function ReplaySliderDiff({leftOffsetMs, replay, rightOffsetMs}: Props) {
         </Tooltip>
         </Tooltip>
       </Header>
       </Header>
       <WithPadding>
       <WithPadding>
-        <Positioned ref={positionedRef}>
+        <Positioned style={{minHeight}} ref={positionedRef}>
           {viewDimensions.width ? (
           {viewDimensions.width ? (
             <DiffSides
             <DiffSides
               leftOffsetMs={leftOffsetMs}
               leftOffsetMs={leftOffsetMs}
@@ -107,8 +114,8 @@ function DiffSides({leftOffsetMs, replay, rightOffsetMs, viewDimensions, width})
           <Cover style={{width}}>
           <Cover style={{width}}>
             <Placement style={{width}}>
             <Placement style={{width}}>
               <ReplayPlayerStateContextProvider>
               <ReplayPlayerStateContextProvider>
-                <NegativeSpaceContainer>
-                  <ReplayPlayerMeasurer measure="width">
+                <NegativeSpaceContainer style={{height: '100%'}}>
+                  <ReplayPlayerMeasurer measure="both">
                     {style => <ReplayPlayer style={style} offsetMs={leftOffsetMs} />}
                     {style => <ReplayPlayer style={style} offsetMs={leftOffsetMs} />}
                   </ReplayPlayerMeasurer>
                   </ReplayPlayerMeasurer>
                 </NegativeSpaceContainer>
                 </NegativeSpaceContainer>
@@ -118,8 +125,8 @@ function DiffSides({leftOffsetMs, replay, rightOffsetMs, viewDimensions, width})
           <Cover ref={rightSideElem} style={{width: 0}}>
           <Cover ref={rightSideElem} style={{width: 0}}>
             <Placement style={{width}}>
             <Placement style={{width}}>
               <ReplayPlayerStateContextProvider>
               <ReplayPlayerStateContextProvider>
-                <NegativeSpaceContainer>
-                  <ReplayPlayerMeasurer measure="width">
+                <NegativeSpaceContainer style={{height: '100%'}}>
+                  <ReplayPlayerMeasurer measure="both">
                     {style => <ReplayPlayer style={style} offsetMs={rightOffsetMs} />}
                     {style => <ReplayPlayer style={style} offsetMs={rightOffsetMs} />}
                   </ReplayPlayerMeasurer>
                   </ReplayPlayerMeasurer>
                 </NegativeSpaceContainer>
                 </NegativeSpaceContainer>
@@ -136,10 +143,11 @@ function DiffSides({leftOffsetMs, replay, rightOffsetMs, viewDimensions, width})
 const WithPadding = styled(NegativeSpaceContainer)`
 const WithPadding = styled(NegativeSpaceContainer)`
   padding-block: ${space(1.5)};
   padding-block: ${space(1.5)};
   overflow: visible;
   overflow: visible;
+  height: 100%;
 `;
 `;
 
 
 const Positioned = styled('div')`
 const Positioned = styled('div')`
-  min-height: 335px;
+  height: 100%;
   position: relative;
   position: relative;
   width: 100%;
   width: 100%;
 `;
 `;