diff --git a/packages/next/client/dev/error-overlay/hot-dev-client.js b/packages/next/client/dev/error-overlay/hot-dev-client.js index 501e4a3f396ae..e4563ab13d804 100644 --- a/packages/next/client/dev/error-overlay/hot-dev-client.js +++ b/packages/next/client/dev/error-overlay/hot-dev-client.js @@ -34,7 +34,7 @@ import { onFullRefreshNeeded, } from 'next/dist/compiled/@next/react-dev-overlay/client' import stripAnsi from 'next/dist/compiled/strip-ansi' -import { addMessageListener } from './websocket' +import { addMessageListener, sendMessage } from './websocket' import formatWebpackMessages from './format-webpack-messages' // This alternative WebpackDevServer combines the functionality of: @@ -46,6 +46,8 @@ import formatWebpackMessages from './format-webpack-messages' // that looks similar to our console output. The error overlay is inspired by: // https://github.com/glenjamin/webpack-hot-middleware +window.__nextDevClientId = Math.round(Math.random() * 100 + Date.now()) + let hadRuntimeError = false let customHmrEventHandler export default function connect() { @@ -188,8 +190,17 @@ function onFastRefresh(hasUpdates) { } if (startLatency) { - const latency = Date.now() - startLatency + const endLatency = Date.now() + const latency = endLatency - startLatency console.log(`[Fast Refresh] done in ${latency}ms`) + sendMessage( + JSON.stringify({ + event: 'client-hmr-latency', + id: window.__nextDevClientId, + startTime: startLatency, + endTime: endLatency, + }) + ) if (self.__NEXT_HMR_LATENCY_CB) { self.__NEXT_HMR_LATENCY_CB(latency) } @@ -220,14 +231,34 @@ function processMessage(e) { const { errors, warnings } = obj const hasErrors = Boolean(errors && errors.length) if (hasErrors) { + sendMessage( + JSON.stringify({ + event: 'client-error', + errorCount: errors.length, + clientId: window.__nextDevClientId, + }) + ) return handleErrors(errors) } const hasWarnings = Boolean(warnings && warnings.length) if (hasWarnings) { + sendMessage( + JSON.stringify({ + event: 'client-warning', + warningCount: warnings.length, + clientId: window.__nextDevClientId, + }) + ) return handleWarnings(warnings) } + sendMessage( + JSON.stringify({ + event: 'client-success', + clientId: window.__nextDevClientId, + }) + ) return handleSuccess() } default: { diff --git a/packages/next/client/dev/webpack-hot-middleware-client.js b/packages/next/client/dev/webpack-hot-middleware-client.js index 2a411b68833b5..9272f87f7b603 100644 --- a/packages/next/client/dev/webpack-hot-middleware-client.js +++ b/packages/next/client/dev/webpack-hot-middleware-client.js @@ -1,15 +1,29 @@ import connect from './error-overlay/hot-dev-client' +import { sendMessage } from './error-overlay/websocket' export default () => { const devClient = connect() devClient.subscribeToHmrEvent((obj) => { if (obj.action === 'reloadPage') { + sendMessage( + JSON.stringify({ + event: 'client-reload-page', + clientId: window.__nextDevClientId, + }) + ) return window.location.reload() } if (obj.action === 'removedPage') { const [page] = obj.data if (page === window.next.router.pathname) { + sendMessage( + JSON.stringify({ + event: 'client-removed-page', + clientId: window.__nextDevClientId, + page, + }) + ) return window.location.reload() } return @@ -20,6 +34,13 @@ export default () => { page === window.next.router.pathname && typeof window.next.router.components[page] === 'undefined' ) { + sendMessage( + JSON.stringify({ + event: 'client-added-page', + clientId: window.__nextDevClientId, + page, + }) + ) return window.location.reload() } return diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 74e93f2745db3..733094ec74783 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -288,6 +288,77 @@ export default class HotReloader { wsServer.handleUpgrade(req, req.socket, head, (client) => { this.webpackHotMiddleware?.onHMR(client) this.onDemandEntries?.onHMR(client) + + client.addEventListener('message', ({ data }) => { + data = typeof data !== 'string' ? data.toString() : data + + try { + const payload = JSON.parse(data) + + let traceChild: + | { + name: string + startTime?: bigint + endTime?: bigint + attrs?: Record + } + | undefined + + switch (payload.event) { + case 'client-hmr-latency': { + traceChild = { + name: payload.event, + startTime: BigInt(payload.startTime * 1000 * 1000), + endTime: BigInt(payload.endTime * 1000 * 1000), + } + break + } + case 'client-reload-page': + case 'client-success': { + traceChild = { + name: payload.event, + } + break + } + case 'client-error': { + traceChild = { + name: payload.event, + attrs: { errorCount: payload.errorCount }, + } + break + } + case 'client-warning': { + traceChild = { + name: payload.event, + attrs: { warningCount: payload.warningCount }, + } + break + } + case 'client-removed-page': + case 'client-added-page': { + traceChild = { + name: payload.event, + attrs: { page: payload.page || '' }, + } + break + } + default: { + break + } + } + + if (traceChild) { + this.hotReloaderSpan.manualTraceChild( + traceChild.name, + traceChild.startTime || process.hrtime.bigint(), + traceChild.endTime || process.hrtime.bigint(), + { ...traceChild.attrs, clientId: payload.id } + ) + } + } catch (_) { + // invalid WebSocket message + } + }) }) } diff --git a/test/development/basic/hmr.test.ts b/test/development/basic/hmr.test.ts index 19cb86d2ed03f..bb54ce9f5b62b 100644 --- a/test/development/basic/hmr.test.ts +++ b/test/development/basic/hmr.test.ts @@ -766,4 +766,11 @@ describe('basic HMR', () => { } }) }) + + it('should have client HMR events in trace file', async () => { + const traceData = await next.readFile('.next/trace') + expect(traceData).toContain('client-hmr-latency') + expect(traceData).toContain('client-error') + expect(traceData).toContain('client-success') + }) })