diff --git a/packages/replay-next/components/errors/InlineErrorFallback.tsx b/packages/replay-next/components/errors/InlineErrorFallback.tsx index 68ece65eebc..e7a02dc9f43 100644 --- a/packages/replay-next/components/errors/InlineErrorFallback.tsx +++ b/packages/replay-next/components/errors/InlineErrorFallback.tsx @@ -1,8 +1,10 @@ import { CSSProperties, ReactNode } from "react"; +import { withSessionTimeoutCheck } from "replay-next/components/errors/withSessionTimeoutCheck"; + import styles from "./InlineErrorFallback.module.css"; -export function InlineErrorFallback({ +export const InlineErrorFallback = withSessionTimeoutCheck(function InlineErrorFallback({ className, message, style, @@ -29,4 +31,4 @@ export function InlineErrorFallback({ ); -} +}); diff --git a/packages/replay-next/components/errors/withSessionTimeoutCheck.ts b/packages/replay-next/components/errors/withSessionTimeoutCheck.ts new file mode 100644 index 00000000000..54d55e047a5 --- /dev/null +++ b/packages/replay-next/components/errors/withSessionTimeoutCheck.ts @@ -0,0 +1,30 @@ +import assert from "assert"; +import { ComponentType, createElement, useContext } from "react"; +import { ErrorBoundaryContext } from "react-error-boundary"; + +import { ProtocolError, isCommandError } from "shared/utils/error"; + +export function withSessionTimeoutCheck(component: ComponentType) { + const Wrapper = (props: Props) => { + const context = useContext(ErrorBoundaryContext); + assert(context, "ErrorBoundaryContext is not available"); + + const { didCatch, error } = context; + if (didCatch) { + if ( + isCommandError(error, ProtocolError.UnknownSession) || + isCommandError(error, ProtocolError.SessionDestroyed) + ) { + throw error; + } + } + + return createElement(component, props); + }; + + // Format for display in DevTools + const name = component.displayName || component.name || "Unknown"; + Wrapper.displayName = `withSessionTimeoutCheck(${name})`; + + return Wrapper; +} diff --git a/src/ui/components/Errors/RootErrorBoundary.tsx b/src/ui/components/Errors/RootErrorBoundary.tsx index becc96717ea..a57effb7038 100644 --- a/src/ui/components/Errors/RootErrorBoundary.tsx +++ b/src/ui/components/Errors/RootErrorBoundary.tsx @@ -3,7 +3,9 @@ import { ErrorBoundary, ErrorBoundaryProps } from "react-error-boundary"; import { UnexpectedErrorForm } from "replay-next/components/errors/UnexpectedErrorForm"; import { ReplayClientInterface } from "shared/client/types"; +import { ProtocolError, isCommandError } from "shared/utils/error"; import { setExpectedError, setUnexpectedError } from "ui/actions/errors"; +import { getDisconnectionError } from "ui/actions/session"; import { useGetUserInfo } from "ui/hooks/users"; import { getExpectedError, getUnexpectedError } from "ui/reducers/app"; import { useAppDispatch, useAppSelector } from "ui/setup/hooks"; @@ -29,7 +31,12 @@ export function RootErrorBoundary({ const currentUserInfo = useGetUserInfo(); const onError = (error: Error, info: ErrorInfo) => { - if (error instanceof Error && error.name === "ChunkLoadError") { + if ( + isCommandError(error, ProtocolError.UnknownSession) || + isCommandError(error, ProtocolError.SessionDestroyed) + ) { + dispatch(setExpectedError(getDisconnectionError())); + } else if (error instanceof Error && error.name === "ChunkLoadError") { dispatch( setExpectedError({ message: "Replay updated",