Skip to content

Commit

Permalink
feat(react-dev-overlay): export getErrorByType and add preventDisplay…
Browse files Browse the repository at this point in the history
… prop (#34237)

Report full parsed runtime errors over error bus, accepts preventDisplay prop to avoid showing error messages inline, while still reporting errors over the bus.

Basically, we want to handle the parsed error/stack differently in Next Live, showing a modal that sits above the content and allows users to report the issue to us. We want to have that stack trace in the issue report, so I added a new event `unhandled-error-full`.

The `preventDisplay` prop then just lets us output our own modal instead of showing the error inline, but still renders the `<RuntimeErrors />` component so it can fetch the stack and report it over the bus.

This isn't *beautiful* code per-se, but I think doing it really right would require a pretty intense re-structure of this module. I think ideally we'd have export a function to fetch of the stack that we can just expose separately - that fetch currently happens in a sub-sub-component (DevOverlay > Errors > RuntimeError). But that re-write is pretty high effort, would still require much of what we do here anyway, and would just to get a slightly less awkward API in a not very high-use area. So leaving it as-is for now, happy to revisit though if we want.

## Feature

- [x] Related issues linked using `fixes #number`

Fixes an issue with Next Live #290.

## Documentation / Examples

- [x] Make sure the linting passes by running `yarn lint`


Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
  • Loading branch information
natew and ijjk committed Feb 18, 2022
1 parent e251a8b commit 97b964a
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 40 deletions.
2 changes: 1 addition & 1 deletion packages/next/compiled/@next/react-dev-overlay/client.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/react-dev-overlay/src/client.ts
Expand Up @@ -87,6 +87,7 @@ function onRefresh() {
Bus.emit({ type: Bus.TYPE_REFRESH })
}

export { getErrorByType } from './internal/helpers/getErrorByType'
export { getNodeError } from './internal/helpers/nodeStackFrames'
export { default as ReactDevOverlay } from './internal/ReactDevOverlay'
export {
Expand Down
28 changes: 27 additions & 1 deletion packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx
Expand Up @@ -58,8 +58,14 @@ function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState {
}
}

type ErrorType = 'runtime' | 'build' | 'full-refresh'

const ReactDevOverlay: React.FunctionComponent = function ReactDevOverlay({
children,
preventDisplay,
}: {
children?: React.ReactNode
preventDisplay?: ErrorType[]
}) {
const [state, dispatch] = React.useReducer<
React.Reducer<OverlayState, Bus.BusEvent>
Expand Down Expand Up @@ -90,6 +96,7 @@ const ReactDevOverlay: React.FunctionComponent = function ReactDevOverlay({
const isAboutToFullRefresh = state.isAboutToFullRefresh

const isMounted = hasBuildError || hasRuntimeErrors || isAboutToFullRefresh

return (
<React.Fragment>
<ErrorBoundary onError={onComponentError}>
Expand All @@ -101,7 +108,16 @@ const ReactDevOverlay: React.FunctionComponent = function ReactDevOverlay({
<Base />
<ComponentStyles />

{hasBuildError ? (
{shouldPreventDisplay(
hasBuildError
? 'build'
: hasRuntimeErrors
? 'runtime'
: isAboutToFullRefresh
? 'full-refresh'
: null,
preventDisplay
) ? null : hasBuildError ? (
<BuildError message={state.buildError!} />
) : hasRuntimeErrors ? (
<Errors errors={state.errors} />
Expand All @@ -114,4 +130,14 @@ const ReactDevOverlay: React.FunctionComponent = function ReactDevOverlay({
)
}

const shouldPreventDisplay = (
errorType?: ErrorType | null,
preventType?: ErrorType[] | null
) => {
if (!preventType || !errorType) {
return false
}
return preventType.includes(errorType)
}

export default ReactDevOverlay
38 changes: 1 addition & 37 deletions packages/react-dev-overlay/src/internal/container/Errors.tsx
Expand Up @@ -14,12 +14,9 @@ import {
import { LeftRightDialogHeader } from '../components/LeftRightDialogHeader'
import { Overlay } from '../components/Overlay'
import { Toast } from '../components/Toast'
import { getErrorByType, ReadyRuntimeError } from '../helpers/getErrorByType'
import { isNodeError } from '../helpers/nodeStackFrames'
import { noop as css } from '../helpers/noop-template'
import {
getOriginalStackFrames,
OriginalStackFrame,
} from '../helpers/stack-frame'
import { RuntimeError } from './RuntimeError'

export type SupportedErrorEvent = {
Expand All @@ -28,13 +25,6 @@ export type SupportedErrorEvent = {
}
export type ErrorsProps = { errors: SupportedErrorEvent[] }

export type ReadyRuntimeError = {
id: number

runtime: true
error: Error
frames: OriginalStackFrame[]
}
type ReadyErrorEvent = ReadyRuntimeError

function getErrorSignature(ev: SupportedErrorEvent): string {
Expand Down Expand Up @@ -82,32 +72,6 @@ const HotlinkedText: React.FC<{
)
}

async function getErrorByType(
ev: SupportedErrorEvent
): Promise<ReadyErrorEvent> {
const { id, event } = ev
switch (event.type) {
case TYPE_UNHANDLED_ERROR:
case TYPE_UNHANDLED_REJECTION: {
return {
id,
runtime: true,
error: event.reason,
frames: await getOriginalStackFrames(
isNodeError(event.reason),
event.frames
),
}
}
default: {
break
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: never = event
throw new Error('type system invariant violation')
}

export const Errors: React.FC<ErrorsProps> = function Errors({ errors }) {
const [lookups, setLookups] = React.useState(
{} as { [eventId: string]: ReadyErrorEvent }
Expand Down
@@ -1,9 +1,9 @@
import * as React from 'react'
import { StackFrame } from 'stacktrace-parser'
import { CodeFrame } from '../components/CodeFrame'
import { ReadyRuntimeError } from '../helpers/getErrorByType'
import { noop as css } from '../helpers/noop-template'
import { getFrameSource, OriginalStackFrame } from '../helpers/stack-frame'
import { ReadyRuntimeError } from './Errors'

export type RuntimeErrorProps = { error: ReadyRuntimeError }

Expand Down
37 changes: 37 additions & 0 deletions packages/react-dev-overlay/src/internal/helpers/getErrorByType.ts
@@ -0,0 +1,37 @@
import { TYPE_UNHANDLED_ERROR, TYPE_UNHANDLED_REJECTION } from '../bus'
import { SupportedErrorEvent } from '../container/Errors'
import { isNodeError } from './nodeStackFrames'
import { getOriginalStackFrames, OriginalStackFrame } from './stack-frame'

export type ReadyRuntimeError = {
id: number
runtime: true
error: Error
frames: OriginalStackFrame[]
}

export async function getErrorByType(
ev: SupportedErrorEvent
): Promise<ReadyRuntimeError> {
const { id, event } = ev
switch (event.type) {
case TYPE_UNHANDLED_ERROR:
case TYPE_UNHANDLED_REJECTION: {
return {
id,
runtime: true,
error: event.reason,
frames: await getOriginalStackFrames(
isNodeError(event.reason),
event.frames
),
}
}
default: {
break
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: never = event
throw new Error('type system invariant violation')
}

0 comments on commit 97b964a

Please sign in to comment.