Skip to content

Commit 276f4d3

Browse files
gaojudepull[bot]
authored andcommitted
feat: connect error rating buttons to telemetry API (#74496)
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
1 parent 870fdce commit 276f4d3

File tree

13 files changed

+166
-117
lines changed

13 files changed

+166
-117
lines changed

packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/Errors/error-overlay-footer/error-feedback/error-feedback-toast.stories.tsx

-17
This file was deleted.

packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/Errors/error-overlay-footer/error-feedback/error-feedback-toast.tsx

-29
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,62 @@
1-
import { useState } from 'react'
2-
1+
import { useState, useCallback } from 'react'
32
import { ThumbsUp } from '../../../../icons/thumbs/thumbs-up'
43
import { ThumbsDown } from '../../../../icons/thumbs/thumbs-down'
5-
import { ErrorFeedbackToast } from './error-feedback-toast'
64

7-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
8-
export function ErrorFeedback({ errorCode }: { errorCode: string }) {
9-
const [voted, setVoted] = useState<'good' | 'bad' | null>(null)
10-
const [isToastVisible, setIsToastVisible] = useState(false)
5+
interface ErrorFeedbackProps {
6+
errorCode: string
7+
}
8+
9+
export function ErrorFeedback({ errorCode }: ErrorFeedbackProps) {
10+
const [voted, setVoted] = useState<boolean | null>(null)
1111
const hasVoted = voted !== null
1212

13-
// TODO: make API call to /__nextjs_error_feedback
14-
const handleFeedback = (value: 'good' | 'bad') => {
15-
setVoted(value)
16-
setIsToastVisible(true)
17-
}
13+
const handleFeedback = useCallback(
14+
async (wasHelpful: boolean) => {
15+
try {
16+
const response = await fetch(
17+
`${process.env.__NEXT_ROUTER_BASEPATH || ''}/__nextjs_error_feedback?errorCode=${errorCode}&wasHelpful=${wasHelpful}`
18+
)
19+
20+
if (!response.ok) {
21+
// Handle non-2xx HTTP responses here if needed
22+
console.error('Failed to record feedback on the server.')
23+
}
24+
25+
setVoted(wasHelpful)
26+
} catch (error) {
27+
console.error('Failed to record feedback:', error)
28+
}
29+
},
30+
[errorCode]
31+
)
1832

1933
return (
2034
<>
2135
<div className="error-feedback">
22-
<p>Was this helpful?</p>
23-
<button
24-
onClick={() => handleFeedback('good')}
25-
disabled={hasVoted}
26-
className={`feedback-button ${voted === 'good' ? 'voted' : ''}`}
27-
>
28-
<ThumbsUp />
29-
</button>
30-
<button
31-
onClick={() => handleFeedback('bad')}
32-
disabled={hasVoted}
33-
className={`feedback-button ${voted === 'bad' ? 'voted' : ''}`}
34-
>
35-
<ThumbsDown />
36-
</button>
36+
{hasVoted ? (
37+
<p className="error-feedback-thanks">Thanks for your feedback!</p>
38+
) : (
39+
<>
40+
<p>Was this helpful?</p>
41+
<button
42+
aria-label="Mark as helpful"
43+
onClick={() => handleFeedback(true)}
44+
disabled={hasVoted}
45+
className={`feedback-button ${voted === true ? 'voted' : ''}`}
46+
>
47+
<ThumbsUp />
48+
</button>
49+
<button
50+
aria-label="Mark as not helpful"
51+
onClick={() => handleFeedback(false)}
52+
disabled={hasVoted}
53+
className={`feedback-button ${voted === false ? 'voted' : ''}`}
54+
>
55+
<ThumbsDown />
56+
</button>
57+
</>
58+
)}
3759
</div>
38-
<ErrorFeedbackToast
39-
isVisible={isToastVisible}
40-
setIsVisible={setIsToastVisible}
41-
/>
4260
</>
4361
)
4462
}

packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/Errors/error-overlay-footer/styles.ts

+7-31
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ const styles = css`
3232
gap: var(--size-gap);
3333
}
3434
35+
.error-feedback-thanks {
36+
height: 1.5rem; /* 24px */
37+
display: flex;
38+
align-items: center;
39+
padding-right: 4px; /* To match the 4px inner padding of the thumbs up and down icons */
40+
}
41+
3542
.feedback-button {
3643
background: none;
3744
border: none;
@@ -69,37 +76,6 @@ const styles = css`
6976
.thumbs-down-icon {
7077
color: var(--color-gray-900);
7178
}
72-
73-
.error-feedback-toast {
74-
width: 420px;
75-
height: auto;
76-
overflow: hidden;
77-
border: 0;
78-
padding: var(--size-gap-double);
79-
border-radius: var(--rounded-xl);
80-
background: var(--color-blue-700);
81-
bottom: var(--size-gap);
82-
right: var(--size-gap);
83-
left: auto;
84-
}
85-
86-
.error-feedback-toast-text {
87-
display: flex;
88-
align-items: center;
89-
justify-content: space-between;
90-
color: var(--color-font);
91-
}
92-
93-
.error-feedback-toast-hide-button {
94-
width: var(--size-gap-quad);
95-
height: var(--size-gap-quad);
96-
border: none;
97-
background: none;
98-
&:focus {
99-
outline: none;
100-
}
101-
color: var(--color-font);
102-
}
10379
`
10480

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

55
const meta: Meta<typeof ErrorOverlayLayout> = {
@@ -18,6 +18,7 @@ export const Default: Story = {
1818
args: {
1919
errorType: 'Build Error',
2020
errorMessage: 'Failed to compile',
21+
errorCode: 'E001',
2122
versionInfo: {
2223
installed: '15.0.0',
2324
staleness: 'fresh',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
/* eslint-disable import/no-extraneous-dependencies */
5+
import { render, screen, fireEvent, act } from '@testing-library/react'
6+
import { ErrorOverlayLayout } from './error-overlay-layout'
7+
import '@testing-library/jest-dom'
8+
9+
// Mock maintain--tab-focus module
10+
jest.mock('../../../components/Overlay/maintain--tab-focus', () => ({
11+
__esModule: true,
12+
default: jest.fn(() => ({
13+
disengage: jest.fn(),
14+
})),
15+
}))
16+
17+
const renderTestComponent = () => {
18+
return render(
19+
<ErrorOverlayLayout
20+
errorType="Build Error"
21+
errorMessage="Failed to compile"
22+
errorCode="E001"
23+
error={new Error('Sample error')}
24+
isBuildError={true}
25+
onClose={() => {}}
26+
>
27+
Module not found: Cannot find module './missing-module'
28+
</ErrorOverlayLayout>
29+
)
30+
}
31+
32+
describe('ErrorOverlayLayout Component', () => {
33+
beforeEach(() => {
34+
// Mock fetch
35+
global.fetch = jest.fn(() => {
36+
return Promise.resolve({
37+
ok: true,
38+
headers: new Headers(),
39+
redirected: false,
40+
status: 200,
41+
statusText: 'OK',
42+
type: 'basic',
43+
url: '',
44+
json: () => Promise.resolve({}),
45+
text: () => Promise.resolve(''),
46+
blob: () => Promise.resolve(new Blob()),
47+
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
48+
formData: () => Promise.resolve(new FormData()),
49+
clone: () => new Response(),
50+
} as Response)
51+
}) as jest.Mock
52+
})
53+
54+
test('renders ErrorOverlayLayout with provided props', () => {
55+
renderTestComponent()
56+
expect(screen.getByText('Failed to compile')).toBeInTheDocument()
57+
expect(
58+
screen.getByText(
59+
"Module not found: Cannot find module './missing-module'"
60+
)
61+
).toBeInTheDocument()
62+
})
63+
64+
test('sends feedback when clicking helpful button', async () => {
65+
renderTestComponent()
66+
67+
expect(
68+
screen.queryByText('Thanks for your feedback!')
69+
).not.toBeInTheDocument()
70+
71+
// Click helpful button
72+
await act(async () => {
73+
fireEvent.click(screen.getByLabelText('Mark as helpful'))
74+
})
75+
76+
expect(fetch).toHaveBeenCalledWith(
77+
'/__nextjs_error_feedback?errorCode=E001&wasHelpful=true'
78+
)
79+
80+
expect(screen.getByText('Thanks for your feedback!')).toBeInTheDocument()
81+
})
82+
83+
test('sends feedback when clicking not helpful button', async () => {
84+
renderTestComponent()
85+
86+
expect(
87+
screen.queryByText('Thanks for your feedback!')
88+
).not.toBeInTheDocument()
89+
90+
await act(async () => {
91+
fireEvent.click(screen.getByLabelText('Mark as not helpful'))
92+
})
93+
94+
expect(fetch).toHaveBeenCalledWith(
95+
'/__nextjs_error_feedback?errorCode=E001&wasHelpful=false'
96+
)
97+
98+
expect(screen.getByText('Thanks for your feedback!')).toBeInTheDocument()
99+
})
100+
})

packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/BuildError.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react'
22
import type { VersionInfo } from '../../../../../../server/dev/parse-version-info'
33
import { Terminal } from '../components/Terminal'
44
import { noop as css } from '../helpers/noop-template'
5-
import { ErrorOverlayLayout } from '../components/Errors/ErrorOverlayLayout/ErrorOverlayLayout'
5+
import { ErrorOverlayLayout } from '../components/Errors/error-overlay-layout/error-overlay-layout'
66

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

packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/Errors.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
} from '../helpers/console-error'
2626
import { extractNextErrorCode } from '../../../../../../lib/error-telemetry-utils'
2727
import { ErrorIndicator } from '../components/Errors/ErrorIndicator/ErrorIndicator'
28-
import { ErrorOverlayLayout } from '../components/Errors/ErrorOverlayLayout/ErrorOverlayLayout'
28+
import { ErrorOverlayLayout } from '../components/Errors/error-overlay-layout/error-overlay-layout'
2929

3030
export type SupportedErrorEvent = {
3131
id: number

packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/RootLayoutMissingTagsError.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { VersionInfo } from '../../../../../../server/dev/parse-version-info'
22
import { useCallback } from 'react'
33
import { HotlinkedText } from '../components/hot-linked-text'
4-
import { ErrorOverlayLayout } from '../components/Errors/ErrorOverlayLayout/ErrorOverlayLayout'
4+
import { ErrorOverlayLayout } from '../components/Errors/error-overlay-layout/error-overlay-layout'
55

66
type RootLayoutMissingTagsErrorProps = {
77
missingTags: string[]

packages/next/src/client/components/react-dev-overlay/_experimental/internal/icons/thumbs/thumbs-down.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ export function ThumbsDown() {
99
className="thumbs-down-icon"
1010
>
1111
<path
12-
fill-rule="evenodd"
13-
clip-rule="evenodd"
12+
fillRule="evenodd"
13+
clipRule="evenodd"
1414
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"
1515
fill="currentColor"
1616
/>

packages/next/src/client/components/react-dev-overlay/_experimental/internal/icons/thumbs/thumbs-up.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export function ThumbsUp() {
1111
<g id="thumb-up-16">
1212
<path
1313
id="Union"
14-
fill-rule="evenodd"
15-
clip-rule="evenodd"
14+
fillRule="evenodd"
15+
clipRule="evenodd"
1616
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"
1717
fill="currentColor"
1818
/>

packages/next/src/client/components/react-dev-overlay/_experimental/internal/styles/ComponentStyles.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { styles as codeFrame } from '../components/CodeFrame/styles'
22
import { styles as dialog } from '../components/Dialog'
3-
import { styles as errorLayout } from '../components/Errors/ErrorOverlayLayout/ErrorOverlayLayout'
3+
import { styles as errorLayout } from '../components/Errors/error-overlay-layout/error-overlay-layout'
44
import { styles as bottomStacks } from '../components/Errors/error-overlay-bottom-stacks/error-overlay-bottom-stacks'
55
import { styles as pagination } from '../components/Errors/ErrorPagination/styles'
66
import { styles as overlay } from '../components/Overlay/styles'

0 commit comments

Comments
 (0)