Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: connect error rating buttons to telemetry API #74496

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

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' : ''}`}
Copy link
Member

Choose a reason for hiding this comment

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

We should aria-disable this while we process feedback to avoid duplicate submissions. Not disabled because that causes focus loss.

Or do we handle duplicate submissions already on the backend?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will follow up

Copy link
Contributor Author

@gaojude gaojude Jan 6, 2025

Choose a reason for hiding this comment

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

Addressing this comment in #74563
apology again it auto-merged
@eps1lon

>
<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
Loading