-
Notifications
You must be signed in to change notification settings - Fork 14
feat: add error-view component #493
Changes from 12 commits
70deae2
76751ea
d29e4a7
1854bca
34efb60
4df7e87
88c5857
1084f0a
98f6cdd
d392ccc
bc8dece
9fd11bc
7b52c88
a0817c0
5ca636c
87f2f03
34c8c9c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@hashicorp/react-error-view': patch | ||
| --- | ||
|
|
||
| Adds the react-error-view component. |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Error View | ||
|
|
||
| A component used to display an error view. For further details on this component, see [docs.mdx](./docs.mdx). These docs can also be viewed through our [Swingset instance](https://react-components.vercel.app). | ||
|
|
||
| ## `useErrorPageAnalytics` | ||
|
|
||
| The `useErrorPageAnalytics` hook is used within the `ErrorView` component, and is provided as a named export from `@hashicorp/react-error-view`. This hook is intended for use cases where the opinionated, `g-grid-container`-based styles of the default `ErrorView` component are not suited to a project. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { render, waitFor } from '@testing-library/react' | ||
| import useErrorPageAnalytics from '../use-error-page-analytics' | ||
|
|
||
| function HookTestComponent({ statusCode }) { | ||
| useErrorPageAnalytics(statusCode) | ||
| return <div>Testing the useErrorPageAnalytics hook...</div> | ||
| } | ||
|
|
||
| describe('useErrorPageAnalytics', () => { | ||
| it('calls window.analytics.track with the provided error code', async () => { | ||
| // Mock window.analytics | ||
| const forMockRestore = window.analytics | ||
| // $TSFixMe loosens window.analytics type to diverge from Segment type | ||
| // defined in @hashicorp/platform-types | ||
| window.analytics = { track: jest.fn() } as $TSFixMe | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without the |
||
| // Render and assert | ||
| render(<HookTestComponent statusCode={404} />) | ||
| await waitFor(() => expect(window.analytics.track).toHaveBeenCalledTimes(1)) | ||
| expect(window.analytics.track).toBeCalledWith(window.location.href, { | ||
| category: '404 Response', | ||
| label: 'No Referrer', | ||
| }) | ||
| // Cleanup | ||
| window.analytics = forMockRestore | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| --- | ||
| componentName: 'ErrorView' | ||
| --- | ||
|
|
||
| A component used to display an error view, within layouts that use `g-grid-container`. | ||
|
|
||
| <LiveComponent>{` | ||
| <ErrorView statusCode={404} /> | ||
| `}</LiveComponent> | ||
|
|
||
| ### `useErrorPageAnalytics` | ||
|
|
||
| This component leverages a `useErrorPageAnalytics` hook internally, which reports the page error using `window.location.track()`. | ||
|
|
||
| > Note that `window.location.track()` is initialized in `ConsentManager`. | ||
|
|
||
| This hook is intended for use cases where the opinionated, `g-grid-container`-based styles of the default `ErrorView` component are not suited to a project. | ||
|
|
||
| ```jsx | ||
| /** | ||
| * eg 404.tsx or 500.tsx | ||
| * ref: https://nextjs.org/docs/advanced-features/custom-error-page | ||
| */ | ||
| import { useErrorPageAnalytics } from '@hashicorp/react-error-view' | ||
| import s from './custom-styles.module.css' | ||
|
|
||
| export default function CustomFourOhFourPage(): React.ReactElement { | ||
| useErrorPageAnalytics(404) | ||
|
|
||
| return ( | ||
| <p> | ||
| Oops, something went wrong! We logged an error. Please enjoy this "Back to | ||
| Home" link. | ||
| </p> | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| <UsageDetails packageJson={packageJson} /> | ||
|
|
||
| ### Props | ||
|
|
||
| <PropsTable props={componentProps} /> | ||
|
|
||
| ### Examples | ||
|
|
||
| `statusCode={404}` | ||
|
|
||
| <LiveComponent>{` | ||
| <ErrorView statusCode={404} /> | ||
| `}</LiveComponent> | ||
|
|
||
| Fallback (for `500` pages, and any non-`404` statusCode) | ||
|
|
||
| <LiveComponent>{` | ||
| <ErrorView statusCode={500} /> | ||
| `}</LiveComponent> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import s from './style.module.css' | ||
| import Link from 'next/link' | ||
| import useErrorPageAnalytics from './use-error-page-analytics' | ||
|
|
||
| interface ErrorPageProps { | ||
| /** Error code to be recorded via window.analytics.track. */ | ||
| statusCode: number | ||
| } | ||
|
|
||
| const CONTENT_DICT = { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Went with a very fixed content approach for this component. I think this will satisfy most of our existing use cases, and have provided the |
||
| 404: { | ||
| heading: 'Not Found', | ||
| message: "We're sorry, but we can't find the page you're looking for.", | ||
| }, | ||
| fallback: { | ||
| heading: 'Something went wrong.', | ||
| message: | ||
| "We're sorry, but the requested page isn't available right now. We've logged this as an error, and will look into it. Please check back soon.", | ||
| }, | ||
| } | ||
|
|
||
| function ErrorPage({ statusCode }: ErrorPageProps): React.ReactElement { | ||
| useErrorPageAnalytics(statusCode) | ||
|
|
||
| const { heading, message } = CONTENT_DICT[statusCode] || CONTENT_DICT.fallback | ||
| return ( | ||
| <div className={s.root}> | ||
| <h1 className={s.heading}>{heading}</h1> | ||
| <p>{message}</p> | ||
| <p> | ||
| <Link href="/"> | ||
| <a className={s.link}>Back to Home</a> | ||
| </Link> | ||
| </p> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export { useErrorPageAnalytics } | ||
| export default ErrorPage | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "name": "@hashicorp/react-error-view", | ||
| "description": "error view for use on docs sites", | ||
| "version": "0.0.0", | ||
| "author": "HashiCorp", | ||
| "contributors": ["Zach Shilton"], | ||
| "main": "index.tsx", | ||
| "license": "MPL-2.0", | ||
| "peerDependencies": { | ||
| "@hashicorp/mktg-global-styles": ">=3.x", | ||
| "react": ">=16.x" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "scripts": { | ||
| "test": "echo \"Error: no test specified\" && exit 1" | ||
| }, | ||
| "keywords": [] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| module.exports = { | ||
| statusCode: { | ||
| type: 'number', | ||
| description: | ||
| 'Integer representing an HTTP response status code. Passing `404` will render "Not Found" vibes, passing any other error code will render generic error vibes.', | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| .root { | ||
| composes: .g-grid-container from global; | ||
| display: flex; | ||
| flex-direction: column; | ||
| justify-content: center; | ||
| margin: 64px auto; | ||
|
|
||
| /* max-width overrides g-grid-container default */ | ||
| max-width: 784px; | ||
| min-height: 50vh; | ||
| padding-inline: 32px; | ||
| text-align: center; | ||
|
|
||
| @media (--large) { | ||
| padding-inline: 24px; | ||
| } | ||
| } | ||
|
|
||
| .heading { | ||
| composes: g-type-display-3 from global; | ||
| } | ||
|
|
||
| .link { | ||
| color: var(--highlight-color, var(--brand)); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { useEffect } from 'react' | ||
|
|
||
| /** | ||
| * Given an error category to record, | ||
| * make a call to window.analytics.track on mount and | ||
| * when the provided statusCode changes, in order to record the | ||
| * the specified error at the current window.location.href. | ||
| * | ||
| * Relies on window.analytics.track() being a valid function | ||
| * which can be called as window.analytics.track(href, { category, label }). | ||
| */ | ||
| export default function useErrorPageAnalytics( | ||
| /** The type of error. Used to send a "{statusCode} Response" | ||
| * category value to window.analytics.track, if that's possible. */ | ||
| statusCode: number | ||
| ): void { | ||
| useEffect(() => { | ||
| if ( | ||
| typeof window !== 'undefined' && | ||
| typeof window?.analytics?.track === 'function' && | ||
| typeof window?.document?.referrer === 'string' && | ||
| typeof window?.location?.href === 'string' | ||
| ) | ||
| window.analytics.track(window.location.href, { | ||
| category: `${statusCode} Response`, | ||
| label: window.document.referrer || 'No Referrer', | ||
| }) | ||
| }, [statusCode]) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,13 +19,19 @@ async function fetchRegistryData(packageName) { | |
| // Otherwise, refetch | ||
| try { | ||
| const response = await fetch(registryUrl) | ||
| // If status is not 200, throw an error | ||
| if (response.status !== 200) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ran into an issue here since the package isn't published yet. I think it makes sense here to bail and throw an error if the npm registry URL responds with anything other than a successful response. |
||
| throw new Error( | ||
| `registry.npmjs.org responded with a status code of "${response.status}". This package may not be published; or there may be some other issue with the npm registry; or this may be an issue with our fetchRegistryData API route.` | ||
| ) | ||
| } | ||
| const responseData = await response.json() | ||
| // Update our cache | ||
| PKG_JSON_CACHE.set(registryUrl, responseData) | ||
| return [null, responseData] | ||
| } catch (error) { | ||
| console.error(error) | ||
| return [error, null] | ||
| return [error.toString(), null] | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,18 +28,19 @@ function ReleaseDetails({ packageJson = {} }) { | |
| const [error, registryData] = await response.json() | ||
| if (error) { | ||
| let msg = `Error fetching registry data for ${name}. ` | ||
| msg += `Full error: ${JSON.stringify(error)}` | ||
| msg += `error.toString(): ${JSON.stringify(error)}` | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Felt this format might be more helpful and transparent for when there are errors. |
||
| console.error(msg) | ||
| dataToSet.error = error | ||
| } else { | ||
| // Sort stable versions in descending order | ||
| const sortedStableVersions = semverSort( | ||
| Object.keys(registryData.versions) | ||
| ).filter((v) => { | ||
| const z = v.match(/(\d+)\.(\d+)\.(.+)$/)[3] | ||
| const isStable = parseInt(z).toString() === z | ||
| return isStable | ||
| }) | ||
| const versions = registryData.versions || {} | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const sortedStableVersions = semverSort(Object.keys(versions)).filter( | ||
| (v) => { | ||
| const z = v.match(/(\d+)\.(\d+)\.(.+)$/)[3] | ||
| const isStable = parseInt(z).toString() === z | ||
| return isStable | ||
| } | ||
| ) | ||
| dataToSet.versions = sortedStableVersions | ||
| } | ||
| // Avoid trying to setData if the component is not mounted | ||
|
|
@@ -111,7 +112,7 @@ function VersionSection({ label, versions, isLoading, name, error }) { | |
| {isLoading | ||
| ? `Loading ${label} releases...` | ||
| : error | ||
| ? 'Error fetching registry data. Check the console.' | ||
| ? error | ||
| : `No matching versions found for ${label}.`} | ||
| </span> | ||
| </div> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kept this README pretty minimal, to avoid duplicating content in the
docs.mdxfile.