-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(replay): Visually compare & diff react hydration errors on the R…
…eplay Details page (#61477) Reads the new `session-replay-show-hydration-errors` feature flag which controls if we show `replay.hydrate-error` crumbs on the Replay Details page. These crumbs have a poor design right now, but they have a button which will open a Modal that shows a visual side-by-side comparison of before & after the hydration error happened. Also, it shows a diff of the html. <img width="1426" alt="SCR-20231208-ninr" src="https://github.com/getsentry/sentry/assets/187460/2b5b172d-024d-4535-98d9-86493405c523"> Depends on #61612 See related SDK change: getsentry/sentry-javascript#9759 fixes #61613 --------- Co-authored-by: Scott Cooper <scttcper@gmail.com>
- Loading branch information
Showing
17 changed files
with
438 additions
and
135 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
static/app/components/replays/breadcrumbs/openReplayComparisonButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import {Fragment, lazy, Suspense} from 'react'; | ||
import {css} from '@emotion/react'; | ||
|
||
import {openModal} from 'sentry/actionCreators/modal'; | ||
import {Button} from 'sentry/components/button'; | ||
import LoadingIndicator from 'sentry/components/loadingIndicator'; | ||
import {t} from 'sentry/locale'; | ||
import ReplayReader from 'sentry/utils/replays/replayReader'; | ||
import useOrganization from 'sentry/utils/useOrganization'; | ||
|
||
const LazyComparisonModal = lazy( | ||
() => import('sentry/components/replays/breadcrumbs/replayComparisonModal') | ||
); | ||
|
||
interface Props { | ||
leftTimestamp: number; | ||
replay: null | ReplayReader; | ||
rightTimestamp: number; | ||
} | ||
|
||
export function OpenReplayComparisonButton({ | ||
leftTimestamp, | ||
replay, | ||
rightTimestamp, | ||
}: Props) { | ||
const organization = useOrganization(); | ||
|
||
return ( | ||
<Button | ||
role="button" | ||
size="xs" | ||
onClick={() => { | ||
openModal( | ||
deps => ( | ||
<Suspense | ||
fallback={ | ||
<Fragment> | ||
<deps.Header closeButton> | ||
<deps.Header>{t('Hydration Error')}</deps.Header> | ||
</deps.Header> | ||
<deps.Body> | ||
<LoadingIndicator /> | ||
</deps.Body> | ||
</Fragment> | ||
} | ||
> | ||
<LazyComparisonModal | ||
replay={replay} | ||
organization={organization} | ||
leftTimestamp={leftTimestamp} | ||
rightTimestamp={rightTimestamp} | ||
{...deps} | ||
/> | ||
</Suspense> | ||
), | ||
{modalCss} | ||
); | ||
}} | ||
> | ||
{t('Open Hydration Diff')} | ||
</Button> | ||
); | ||
} | ||
|
||
const modalCss = css` | ||
width: 95vw; | ||
min-height: 80vh; | ||
max-height: 95vh; | ||
`; |
145 changes: 145 additions & 0 deletions
145
static/app/components/replays/breadcrumbs/replayComparisonModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import {useEffect, useState} from 'react'; | ||
import styled from '@emotion/styled'; | ||
import {DiffEditor} from '@monaco-editor/react'; | ||
import beautify from 'js-beautify'; | ||
|
||
import {ModalRenderProps} from 'sentry/actionCreators/modal'; | ||
import {Flex} from 'sentry/components/profiling/flex'; | ||
import { | ||
Provider as ReplayContextProvider, | ||
useReplayContext, | ||
} from 'sentry/components/replays/replayContext'; | ||
import ReplayPlayer from 'sentry/components/replays/replayPlayer'; | ||
import {TabList} from 'sentry/components/tabs'; | ||
import {t} from 'sentry/locale'; | ||
import ConfigStore from 'sentry/stores/configStore'; | ||
import {useLegacyStore} from 'sentry/stores/useLegacyStore'; | ||
import {space} from 'sentry/styles/space'; | ||
import {Organization} from 'sentry/types'; | ||
import ReplayReader from 'sentry/utils/replays/replayReader'; | ||
import {OrganizationContext} from 'sentry/views/organizationContext'; | ||
|
||
interface Props extends ModalRenderProps { | ||
leftTimestamp: number; | ||
organization: Organization; | ||
replay: null | ReplayReader; | ||
rightTimestamp: number; | ||
} | ||
|
||
export default function ReplayComparisonModal({ | ||
Body, | ||
Header, | ||
leftTimestamp, | ||
organization, | ||
replay, | ||
rightTimestamp, | ||
}: Props) { | ||
const fetching = false; | ||
|
||
const config = useLegacyStore(ConfigStore); | ||
const isDark = config.theme === 'dark'; | ||
|
||
const [activeTab, setActiveTab] = useState<'visual' | 'html'>('html'); | ||
|
||
const [leftBody, setLeftBody] = useState(null); | ||
const [rightBody, setRightBody] = useState(null); | ||
|
||
return ( | ||
<OrganizationContext.Provider value={organization}> | ||
<Header closeButton> | ||
<h4>{t('Hydration Error')}</h4> | ||
</Header> | ||
<Body> | ||
<Flex gap={space(2)} column> | ||
<TabList | ||
hideBorder | ||
selectedKey={activeTab} | ||
onSelectionChange={tab => setActiveTab(tab as 'visual' | 'html')} | ||
> | ||
<TabList.Item key="html">Html Diff</TabList.Item> | ||
<TabList.Item key="visual">Visual Diff</TabList.Item> | ||
</TabList> | ||
<Flex | ||
gap={space(2)} | ||
style={{ | ||
// Using css to hide since the splitdiff uses the html from the iframes | ||
// TODO: This causes a bit of a flash when switching tabs | ||
display: activeTab === 'visual' ? undefined : 'none', | ||
}} | ||
> | ||
<ReplayContextProvider | ||
isFetching={fetching} | ||
replay={replay} | ||
initialTimeOffsetMs={{offsetMs: leftTimestamp - 1}} | ||
> | ||
<ComparisonSideWrapper id="leftSide"> | ||
<ReplaySide | ||
selector="#leftSide iframe" | ||
expectedTime={leftTimestamp - 1} | ||
onLoad={setLeftBody} | ||
/> | ||
</ComparisonSideWrapper> | ||
</ReplayContextProvider> | ||
<ReplayContextProvider | ||
isFetching={fetching} | ||
replay={replay} | ||
initialTimeOffsetMs={{offsetMs: rightTimestamp + 1}} | ||
> | ||
<ComparisonSideWrapper id="rightSide"> | ||
<ReplaySide | ||
selector="#rightSide iframe" | ||
expectedTime={rightTimestamp + 1} | ||
onLoad={setRightBody} | ||
/> | ||
</ComparisonSideWrapper> | ||
</ReplayContextProvider> | ||
</Flex> | ||
{activeTab === 'html' && leftBody && rightBody ? ( | ||
<div> | ||
<DiffEditor | ||
height="60vh" | ||
theme={isDark ? 'vs-dark' : 'light'} | ||
language="html" | ||
original={leftBody} | ||
modified={rightBody} | ||
options={{ | ||
// Options - https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IDiffEditorConstructionOptions.html | ||
scrollBeyondLastLine: false, | ||
readOnly: true, | ||
}} | ||
/> | ||
</div> | ||
) : null} | ||
</Flex> | ||
</Body> | ||
</OrganizationContext.Provider> | ||
); | ||
} | ||
|
||
function ReplaySide({expectedTime, selector, onLoad}) { | ||
const {currentTime} = useReplayContext(); | ||
|
||
useEffect(() => { | ||
if (currentTime === expectedTime) { | ||
setTimeout(() => { | ||
const iframe = document.querySelector(selector) as HTMLIFrameElement; | ||
const body = iframe.contentWindow?.document.body; | ||
if (body) { | ||
onLoad( | ||
beautify.html(body.innerHTML, { | ||
indent_size: 2, | ||
wrap_line_length: 80, | ||
}) | ||
); | ||
} | ||
}, 0); | ||
} | ||
}, [currentTime, expectedTime, selector, onLoad]); | ||
return <ReplayPlayer isPreview />; | ||
} | ||
|
||
const ComparisonSideWrapper = styled('div')` | ||
display: contents; | ||
flex-grow: 1; | ||
max-width: 50%; | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.