Skip to content

Commit

Permalink
add middleware handler for error code telemetry
Browse files Browse the repository at this point in the history
  • Loading branch information
gaojude committed Dec 18, 2024
1 parent b766599 commit d1d9d53
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { getNextErrorFeedbackMiddleware } from './get-next-error-feedback-middleware'
import type { IncomingMessage, ServerResponse } from 'http'

describe('getNextErrorFeedbackMiddleware', () => {
const mockTelemetry = {
record: jest.fn().mockResolvedValue({}),
} as any

let mockReq: Partial<IncomingMessage>
let mockRes: Partial<ServerResponse>
let mockNext: jest.Mock

beforeEach(() => {
mockReq = {
url: '/__nextjs_error_feedback?errorCode=TEST_ERROR&wasHelpful=true',
}
mockRes = {
setHeader: jest.fn(),
end: jest.fn(),
}
mockNext = jest.fn()
jest.clearAllMocks()
})

it('calls next() if path does not match', async () => {
mockReq.url = '/some-other-path'

await getNextErrorFeedbackMiddleware(mockTelemetry)(
mockReq as IncomingMessage,
mockRes as ServerResponse,
mockNext
)

expect(mockNext).toHaveBeenCalled()
expect(mockTelemetry.record).not.toHaveBeenCalled()
})

it('records telemetry when feedback is submitted', async () => {
await getNextErrorFeedbackMiddleware(mockTelemetry)(
mockReq as IncomingMessage,
mockRes as ServerResponse,
mockNext
)

expect(mockTelemetry.record).toHaveBeenCalledWith({
eventName: 'NEXT_ERROR_FEEDBACK',
payload: {
errorCode: 'TEST_ERROR',
wasHelpful: true,
},
})
expect(mockRes.statusCode).toBe(204)
})

it('returns 400 if params are missing', async () => {
mockReq.url = '/__nextjs_error_feedback'

await getNextErrorFeedbackMiddleware(mockTelemetry)(
mockReq as IncomingMessage,
mockRes as ServerResponse,
mockNext
)

expect(mockRes.statusCode).toBe(400)
expect(mockTelemetry.record).not.toHaveBeenCalled()
})

it('returns 500 if telemetry recording fails', async () => {
mockTelemetry.record.mockRejectedValueOnce(new Error('Failed to record'))

await getNextErrorFeedbackMiddleware(mockTelemetry)(
mockReq as IncomingMessage,
mockRes as ServerResponse,
mockNext
)

expect(mockRes.statusCode).toBe(500)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { eventErrorFeedback } from '../../../../telemetry/events/error-feedback'
import { badRequest, internalServerError, noContent } from './shared'

import type { ServerResponse, IncomingMessage } from 'http'
import type { Telemetry } from '../../../../telemetry/storage'

// Handles HTTP requests to /__nextjs_error_feedback endpoint for collecting user feedback on error messages
export function getNextErrorFeedbackMiddleware(telemetry: Telemetry) {
return async function (
req: IncomingMessage,
res: ServerResponse,
next: () => void
): Promise<void> {
const { pathname, searchParams } = new URL(`http://n${req.url}`)

if (pathname !== '/__nextjs_error_feedback') {
return next()
}

try {
const errorCode = searchParams.get('errorCode')
const wasHelpful = searchParams.get('wasHelpful')

if (!errorCode || !wasHelpful) {
return badRequest(res)
}

await telemetry.record(
eventErrorFeedback({
errorCode,
wasHelpful: wasHelpful === 'true',
})
)

return noContent(res)
} catch (error) {
return internalServerError(res)
}
}
}
2 changes: 2 additions & 0 deletions packages/next/src/server/dev/hot-reloader-turbopack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import {
setBundlerFindSourceMapImplementation,
type ModernSourceMapPayload,
} from '../patch-error-inspect'
import { getNextErrorFeedbackMiddleware } from '../../client/components/react-dev-overlay/server/get-next-error-feedback-middleware'
// import { getSupportedBrowsers } from '../../build/utils'

const wsServer = new ws.Server({ noServer: true })
Expand Down Expand Up @@ -627,6 +628,7 @@ export async function createHotReloaderTurbopack(
const middlewares = [
getOverlayMiddleware(project),
getSourceMapMiddleware(project),
getNextErrorFeedbackMiddleware(opts.telemetry),
]

const versionInfoPromise = getVersionInfo(
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/dev/hot-reloader-webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import type { WebpackError } from 'webpack'
import { PAGE_TYPES } from '../../lib/page-types'
import { FAST_REFRESH_RUNTIME_RELOAD } from './messages'
import { getNodeDebugType } from '../lib/utils'
import { getNextErrorFeedbackMiddleware } from '../../client/components/react-dev-overlay/server/get-next-error-feedback-middleware'

const MILLISECONDS_IN_NANOSECOND = BigInt(1_000_000)
const isTestMode = !!(
Expand Down Expand Up @@ -1519,6 +1520,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
serverStats: () => this.serverStats,
edgeServerStats: () => this.edgeServerStats,
}),
getNextErrorFeedbackMiddleware(this.telemetry),
]
}

Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/server/dev/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,8 @@ export default class DevServer extends Server {
if (
request.url.includes('/_next/static') ||
request.url.includes('/__nextjs_original-stack-frame') ||
request.url.includes('/__nextjs_source-map')
request.url.includes('/__nextjs_source-map') ||
request.url.includes('/__nextjs_error_feedback')
) {
return { finished: false }
}
Expand Down
27 changes: 27 additions & 0 deletions packages/next/src/telemetry/events/error-feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export const eventNameErrorFeedback = 'NEXT_ERROR_FEEDBACK'

export type EventErrorFeedback = {
errorCode: string
wasHelpful: boolean
}

/**
* Records telemetry for error feedback.
*
* @example
* ```ts
* telemetry.record(eventErrorFeedback({
* errorCode: 'E1',
* wasHelpful: true
* }))
* ```
*/
export function eventErrorFeedback(event: EventErrorFeedback): {
eventName: string
payload: EventErrorFeedback
} {
return {
eventName: eventNameErrorFeedback,
payload: event,
}
}

0 comments on commit d1d9d53

Please sign in to comment.