Skip to content
This repository was archived by the owner on Dec 17, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-flies-notice.md
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.
18 changes: 17 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

373 changes: 373 additions & 0 deletions packages/error-view/LICENSE

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions packages/error-view/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 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`
Copy link
Copy Markdown
Contributor Author

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.mdx file.


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.

## `use404Redirects`

The `use404Redirects` hook can be used on a `page/404.js` page to handle redirects that aren't triggered via client-side navigations. It determines if the current page results in a server-side redirect by performing a HEAD request against the current URL, and performing a client-side navigation to the resulting URL.

### Usage

```tsx
import ErrorPage, { use404Redirects } from '@hashicorp/react-error-view'

export default function NotFound() {
use404Redirects()

return <ErrorView statusCode={404} />
}
```
83 changes: 83 additions & 0 deletions packages/error-view/__tests__/use-404-redirects.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import nock from 'nock'
import { render, waitFor } from '@testing-library/react'
import * as Router from 'next/router'
import use404Redirects from '../use-404-redirects'

function HookTestComponent() {
use404Redirects()
return <div>Testing the use404Redirects hook...</div>
}

const useRouter = jest.spyOn(Router, 'useRouter')

describe('use404Redirects', () => {
let originalLocation: typeof window.location
let originalAnalytics: typeof window.analytics

beforeAll(() => {
originalLocation = window.location
originalAnalytics = window.analytics
})

beforeEach(() => {
nock('http://local.test')
.head('/testing')
.reply(308, '', { Location: '/new-destination' })
.head('/new-destination')
.reply(200, '')
.head('/valid')
.reply(200, '')

delete (window as any).location
window.location = {
// We use an absolute URL here since fetch in Jest can't use relative URLs
pathname: 'http://local.test/testing',
href: 'http://local.test/testing',
} as $TSFixMe
})

afterEach(() => {
jest.resetAllMocks()
window.location = originalLocation
window.analytics = originalAnalytics
})

afterAll(() => {
nock.restore()
})

it('tracks event and calls router.replace with resolved URL', async () => {
const useRouterMethods = { replace: jest.fn() } as $TSFixMe
useRouter.mockImplementationOnce(() => useRouterMethods)

window.analytics = { track: jest.fn() } as $TSFixMe

// Render and assert
render(<HookTestComponent />)
await waitFor(() =>
expect(useRouterMethods.replace).toHaveBeenCalledWith('/new-destination')
)
expect(window.analytics.track).toHaveBeenCalledTimes(1)
expect(window.analytics.track).toBeCalledWith('http://local.test/testing', {
category: 'Client-side Redirect',
label: 'http://local.test/new-destination',
})
})

it('does not call router.replace when current URL == resolved URL', async () => {
const useRouterMethods = { replace: jest.fn() } as $TSFixMe
useRouter.mockImplementationOnce(() => useRouterMethods)

window.analytics = { track: jest.fn() } as $TSFixMe

window.location = {
pathname: 'http://local.test/valid',
href: 'http://local.test/valid',
} as $TSFixMe

// Render and assert
render(<HookTestComponent />)
await waitFor(() => expect(useRouterMethods.replace).not.toHaveBeenCalled())
expect(window.analytics.track).not.toHaveBeenCalled()
})
})
26 changes: 26 additions & 0 deletions packages/error-view/__tests__/use-error-page-analytics.test.tsx
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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the ... as $TSFixMe I'd end up with TypeScript getting mad that this doesn't match the type defined in https://github.com/hashicorp/web-platform-packages/blob/6062939845d5d841d78afade8225e891cd428aa3/packages/types/index.d.ts#L34. Hopefully this makes sense, but wanted to flag in case anyone has a better workaround.

// 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
})
})
57 changes: 57 additions & 0 deletions packages/error-view/docs.mdx
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>
42 changes: 42 additions & 0 deletions packages/error-view/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react'
import Link from 'next/link'
import useErrorPageAnalytics from './use-error-page-analytics'
import use404Redirects from './use-404-redirects'
import s from './style.module.css'

export interface ErrorPageProps {
/** Error code to be recorded via window.analytics.track. */
statusCode: number
}

const CONTENT_DICT = {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 useErrorPageAnalytics in case we need to do something more custom (in terms of either content or styles).

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, use404Redirects }
export default ErrorPage
102 changes: 102 additions & 0 deletions packages/error-view/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading