Skip to content

Commit

Permalink
feat: connect error rating buttons to telemetry API (#74496)
Browse files Browse the repository at this point in the history
We're adding a new feature to the error overlay that allows developers
to rate the current error message. Developers can rate the error message
by clicking on the thumb up or down icon in the error overlay footer.

This pull request adds the click handler for the error rating buttons.
When the buttons are clicked, the click handler calls a previously
implemented internal API endpoint. The endpoint is connected to the
usual Next.js Telemetry.

To test the UI, run `pnpm storybook` and go to
http://localhost:6006/?path=/story/erroroverlaylayout--default.



https://github.com/user-attachments/assets/6daf6e64-e8bb-4e5e-b282-082086f1e529
  • Loading branch information
gaojude authored Jan 6, 2025
1 parent 571aafe commit fca7358
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 117 deletions.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,44 +1,62 @@
import { useState } from 'react'

import { useState, useCallback } from 'react'
import { ThumbsUp } from '../../../../icons/thumbs/thumbs-up'
import { ThumbsDown } from '../../../../icons/thumbs/thumbs-down'
import { ErrorFeedbackToast } from './error-feedback-toast'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function ErrorFeedback({ errorCode }: { errorCode: string }) {
const [voted, setVoted] = useState<'good' | 'bad' | null>(null)
const [isToastVisible, setIsToastVisible] = useState(false)
interface ErrorFeedbackProps {
errorCode: string
}

export function ErrorFeedback({ errorCode }: ErrorFeedbackProps) {
const [voted, setVoted] = useState<boolean | null>(null)
const hasVoted = voted !== null

// TODO: make API call to /__nextjs_error_feedback
const handleFeedback = (value: 'good' | 'bad') => {
setVoted(value)
setIsToastVisible(true)
}
const handleFeedback = useCallback(
async (wasHelpful: boolean) => {
try {
const response = await fetch(
`${process.env.__NEXT_ROUTER_BASEPATH || ''}/__nextjs_error_feedback?errorCode=${errorCode}&wasHelpful=${wasHelpful}`
)

if (!response.ok) {
// Handle non-2xx HTTP responses here if needed
console.error('Failed to record feedback on the server.')
}

setVoted(wasHelpful)
} catch (error) {
console.error('Failed to record feedback:', error)
}
},
[errorCode]
)

return (
<>
<div className="error-feedback">
<p>Was this helpful?</p>
<button
onClick={() => handleFeedback('good')}
disabled={hasVoted}
className={`feedback-button ${voted === 'good' ? 'voted' : ''}`}
>
<ThumbsUp />
</button>
<button
onClick={() => handleFeedback('bad')}
disabled={hasVoted}
className={`feedback-button ${voted === 'bad' ? 'voted' : ''}`}
>
<ThumbsDown />
</button>
{hasVoted ? (
<p className="error-feedback-thanks">Thanks for your feedback!</p>
) : (
<>
<p>Was this helpful?</p>
<button
aria-label="Mark as helpful"
onClick={() => handleFeedback(true)}
disabled={hasVoted}
className={`feedback-button ${voted === true ? 'voted' : ''}`}
>
<ThumbsUp />
</button>
<button
aria-label="Mark as not helpful"
onClick={() => handleFeedback(false)}
disabled={hasVoted}
className={`feedback-button ${voted === false ? 'voted' : ''}`}
>
<ThumbsDown />
</button>
</>
)}
</div>
<ErrorFeedbackToast
isVisible={isToastVisible}
setIsVisible={setIsToastVisible}
/>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ const styles = css`
gap: var(--size-gap);
}
.error-feedback-thanks {
height: 1.5rem; /* 24px */
display: flex;
align-items: center;
padding-right: 4px; /* To match the 4px inner padding of the thumbs up and down icons */
}
.feedback-button {
background: none;
border: none;
Expand Down Expand Up @@ -69,37 +76,6 @@ const styles = css`
.thumbs-down-icon {
color: var(--color-gray-900);
}
.error-feedback-toast {
width: 420px;
height: auto;
overflow: hidden;
border: 0;
padding: var(--size-gap-double);
border-radius: var(--rounded-xl);
background: var(--color-blue-700);
bottom: var(--size-gap);
right: var(--size-gap);
left: auto;
}
.error-feedback-toast-text {
display: flex;
align-items: center;
justify-content: space-between;
color: var(--color-font);
}
.error-feedback-toast-hide-button {
width: var(--size-gap-quad);
height: var(--size-gap-quad);
border: none;
background: none;
&:focus {
outline: none;
}
color: var(--color-font);
}
`

export { styles }
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react'
import { ErrorOverlayLayout } from './ErrorOverlayLayout'
import { ErrorOverlayLayout } from './error-overlay-layout'
import { withShadowPortal } from '../../../storybook/with-shadow-portal'

const meta: Meta<typeof ErrorOverlayLayout> = {
Expand All @@ -18,6 +18,7 @@ export const Default: Story = {
args: {
errorType: 'Build Error',
errorMessage: 'Failed to compile',
errorCode: 'E001',
versionInfo: {
installed: '15.0.0',
staleness: 'fresh',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* @jest-environment jsdom
*/
/* eslint-disable import/no-extraneous-dependencies */
import { render, screen, fireEvent, act } from '@testing-library/react'
import { ErrorOverlayLayout } from './error-overlay-layout'
import '@testing-library/jest-dom'

// Mock maintain--tab-focus module
jest.mock('../../../components/Overlay/maintain--tab-focus', () => ({
__esModule: true,
default: jest.fn(() => ({
disengage: jest.fn(),
})),
}))

const renderTestComponent = () => {
return render(
<ErrorOverlayLayout
errorType="Build Error"
errorMessage="Failed to compile"
errorCode="E001"
error={new Error('Sample error')}
isBuildError={true}
onClose={() => {}}
>
Module not found: Cannot find module './missing-module'
</ErrorOverlayLayout>
)
}

describe('ErrorOverlayLayout Component', () => {
beforeEach(() => {
// Mock fetch
global.fetch = jest.fn(() => {
return Promise.resolve({
ok: true,
headers: new Headers(),
redirected: false,
status: 200,
statusText: 'OK',
type: 'basic',
url: '',
json: () => Promise.resolve({}),
text: () => Promise.resolve(''),
blob: () => Promise.resolve(new Blob()),
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
formData: () => Promise.resolve(new FormData()),
clone: () => new Response(),
} as Response)
}) as jest.Mock
})

test('renders ErrorOverlayLayout with provided props', () => {
renderTestComponent()
expect(screen.getByText('Failed to compile')).toBeInTheDocument()
expect(
screen.getByText(
"Module not found: Cannot find module './missing-module'"
)
).toBeInTheDocument()
})

test('sends feedback when clicking helpful button', async () => {
renderTestComponent()

expect(
screen.queryByText('Thanks for your feedback!')
).not.toBeInTheDocument()

// Click helpful button
await act(async () => {
fireEvent.click(screen.getByLabelText('Mark as helpful'))
})

expect(fetch).toHaveBeenCalledWith(
'/__nextjs_error_feedback?errorCode=E001&wasHelpful=true'
)

expect(screen.getByText('Thanks for your feedback!')).toBeInTheDocument()
})

test('sends feedback when clicking not helpful button', async () => {
renderTestComponent()

expect(
screen.queryByText('Thanks for your feedback!')
).not.toBeInTheDocument()

await act(async () => {
fireEvent.click(screen.getByLabelText('Mark as not helpful'))
})

expect(fetch).toHaveBeenCalledWith(
'/__nextjs_error_feedback?errorCode=E001&wasHelpful=false'
)

expect(screen.getByText('Thanks for your feedback!')).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react'
import type { VersionInfo } from '../../../../../../server/dev/parse-version-info'
import { Terminal } from '../components/Terminal'
import { noop as css } from '../helpers/noop-template'
import { ErrorOverlayLayout } from '../components/Errors/ErrorOverlayLayout/ErrorOverlayLayout'
import { ErrorOverlayLayout } from '../components/Errors/error-overlay-layout/error-overlay-layout'

export type BuildErrorProps = { message: string; versionInfo?: VersionInfo }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from '../helpers/console-error'
import { extractNextErrorCode } from '../../../../../../lib/error-telemetry-utils'
import { ErrorIndicator } from '../components/Errors/ErrorIndicator/ErrorIndicator'
import { ErrorOverlayLayout } from '../components/Errors/ErrorOverlayLayout/ErrorOverlayLayout'
import { ErrorOverlayLayout } from '../components/Errors/error-overlay-layout/error-overlay-layout'

export type SupportedErrorEvent = {
id: number
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { VersionInfo } from '../../../../../../server/dev/parse-version-info'
import { useCallback } from 'react'
import { HotlinkedText } from '../components/hot-linked-text'
import { ErrorOverlayLayout } from '../components/Errors/ErrorOverlayLayout/ErrorOverlayLayout'
import { ErrorOverlayLayout } from '../components/Errors/error-overlay-layout/error-overlay-layout'

type RootLayoutMissingTagsErrorProps = {
missingTags: string[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export function ThumbsDown() {
className="thumbs-down-icon"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M5.89531 12.7603C5.72984 12.8785 5.5 12.7602 5.5 12.5569V9.75C5.5 8.7835 4.7165 8 3.75 8H1.5V1.5H11.1884C11.762 1.5 12.262 1.89037 12.4011 2.44683L13.4011 6.44683C13.5984 7.23576 13.0017 8 12.1884 8H8.25H7.5V8.75V11.4854C7.5 11.5662 7.46101 11.6419 7.39531 11.6889L5.89531 12.7603ZM4 12.5569C4 13.9803 5.6089 14.8082 6.76717 13.9809L8.26717 12.9095C8.72706 12.581 9 12.0506 9 11.4854V9.5H12.1884C13.9775 9.5 15.2903 7.81868 14.8563 6.08303L13.8563 2.08303C13.5503 0.858816 12.4503 0 11.1884 0H0.75H0V0.75V8.75V9.5H0.75H3.75C3.88807 9.5 4 9.61193 4 9.75V12.5569Z"
fill="currentColor"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export function ThumbsUp() {
<g id="thumb-up-16">
<path
id="Union"
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M6.89531 2.23959C6.72984 2.1214 6.5 2.23968 6.5 2.44303V5.24989C6.5 6.21639 5.7165 6.99989 4.75 6.99989H2.5V13.4999H12.1884C12.762 13.4999 13.262 13.1095 13.4011 12.5531L14.4011 8.55306C14.5984 7.76412 14.0017 6.99989 13.1884 6.99989H9.25H8.5V6.24989V3.51446C8.5 3.43372 8.46101 3.35795 8.39531 3.31102L6.89531 2.23959ZM5 2.44303C5 1.01963 6.6089 0.191656 7.76717 1.01899L9.26717 2.09042C9.72706 2.41892 10 2.94929 10 3.51446V5.49989H13.1884C14.9775 5.49989 16.2903 7.18121 15.8563 8.91686L14.8563 12.9169C14.5503 14.1411 13.4503 14.9999 12.1884 14.9999H1.75H1V14.2499V6.24989V5.49989H1.75H4.75C4.88807 5.49989 5 5.38796 5 5.24989V2.44303Z"
fill="currentColor"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { styles as codeFrame } from '../components/CodeFrame/styles'
import { styles as dialog } from '../components/Dialog'
import { styles as errorLayout } from '../components/Errors/ErrorOverlayLayout/ErrorOverlayLayout'
import { styles as errorLayout } from '../components/Errors/error-overlay-layout/error-overlay-layout'
import { styles as bottomStacks } from '../components/Errors/error-overlay-bottom-stacks/error-overlay-bottom-stacks'
import { styles as pagination } from '../components/Errors/ErrorPagination/styles'
import { styles as overlay } from '../components/Overlay/styles'
Expand Down

0 comments on commit fca7358

Please sign in to comment.