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 12 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.
373 changes: 373 additions & 0 deletions packages/error-view/LICENSE

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions packages/error-view/README.md
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`
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.
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>
40 changes: 40 additions & 0 deletions packages/error-view/index.tsx
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 = {
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 }
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.

20 changes: 20 additions & 0 deletions packages/error-view/package.json
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": []
}
7 changes: 7 additions & 0 deletions packages/error-view/props.js
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.',
},
}
25 changes: 25 additions & 0 deletions packages/error-view/style.module.css
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));
}
29 changes: 29 additions & 0 deletions packages/error-view/use-error-page-analytics.ts
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])
}
8 changes: 7 additions & 1 deletion pages/api/fetch-registry-data/[...name].js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
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.

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]
}
}

Expand Down
19 changes: 10 additions & 9 deletions swingset-extensions/release-details/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`
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.

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 || {}
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.

registryData may not be defined, needed to handle that case.

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
Expand Down Expand Up @@ -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>
Expand Down