diff --git a/services/real-time/app/js/Router.js b/services/real-time/app/js/Router.js
index 8aaad2a164..37845015b0 100644
--- a/services/real-time/app/js/Router.js
+++ b/services/real-time/app/js/Router.js
@@ -169,20 +169,27 @@ module.exports = Router = {
}
return
}
-
+ const isDebugging = !!client.handshake?.query?.debugging
const projectId = client.handshake?.query?.projectId
- try {
- Joi.assert(projectId, JOI_OBJECT_ID)
- } catch (error) {
- metrics.inc('socket-io.connection', 1, {
- status: client.transport,
- method: projectId ? 'bad-project-id' : 'missing-project-id',
- })
- client.emit('connectionRejected', {
- message: 'missing/bad ?projectId=... query flag on handshake',
- })
- client.disconnect()
- return
+
+ if (isDebugging) {
+ client.connectedAt = Date.now()
+ }
+
+ if (!isDebugging) {
+ try {
+ Joi.assert(projectId, JOI_OBJECT_ID)
+ } catch (error) {
+ metrics.inc('socket-io.connection', 1, {
+ status: client.transport,
+ method: projectId ? 'bad-project-id' : 'missing-project-id',
+ })
+ client.emit('connectionRejected', {
+ message: 'missing/bad ?projectId=... query flag on handshake',
+ })
+ client.disconnect()
+ return
+ }
}
// The client.id is security sensitive. Generate a publicId for sending to other clients.
@@ -198,7 +205,10 @@ module.exports = Router = {
})
metrics.gauge('socket-io.clients', io.sockets.clients().length)
- logger.debug({ session, clientId: client.id }, 'client connected')
+ logger.debug(
+ { session, clientId: client.id, isDebugging },
+ 'client connected'
+ )
let user
if (session && session.passport && session.passport.user) {
@@ -222,7 +232,30 @@ module.exports = Router = {
callback(HOSTNAME)
})
}
+ client.on('debug', (data, callback) => {
+ if (typeof callback !== 'function') {
+ return Router._handleInvalidArguments(client, 'debug', arguments)
+ }
+ logger.debug({ clientId: client.id }, 'received debug message')
+
+ const response = {
+ serverTime: Date.now(),
+ data,
+ client: {
+ publicId: client.publicId,
+ remoteIp: client.remoteIp,
+ userAgent: client.userAgent,
+ connected: !client.disconnected,
+ connectedAt: client.connectedAt,
+ },
+ server: {
+ hostname: settings.exposeHostname ? HOSTNAME : undefined,
+ },
+ }
+
+ callback(response)
+ })
const joinProject = function (callback) {
WebsocketController.joinProject(
client,
@@ -245,6 +278,12 @@ module.exports = Router = {
metrics.inc('socket-io.disconnect', 1, { status: client.transport })
metrics.gauge('socket-io.clients', io.sockets.clients().length)
+ if (client.isDebugging) {
+ const duration = Date.now() - client.connectedAt
+ metrics.timing('socket-io.debugging.duration', duration)
+ logger.debug({ duration }, 'debug client disconnected')
+ }
+
WebsocketController.leaveProject(io, client, function (err) {
if (err) {
Router._handleError(function () {}, err, client, 'leaveProject')
@@ -435,19 +474,21 @@ module.exports = Router = {
)
})
- joinProject((err, project, permissionsLevel, protocolVersion) => {
- if (err) {
- client.emit('connectionRejected', err)
- client.disconnect()
- return
- }
- client.emit('joinProjectResponse', {
- publicId: client.publicId,
- project,
- permissionsLevel,
- protocolVersion,
+ if (!isDebugging) {
+ joinProject((err, project, permissionsLevel, protocolVersion) => {
+ if (err) {
+ client.emit('connectionRejected', err)
+ client.disconnect()
+ return
+ }
+ client.emit('joinProjectResponse', {
+ publicId: client.publicId,
+ project,
+ permissionsLevel,
+ protocolVersion,
+ })
})
- })
+ }
})
},
}
diff --git a/services/web/app/src/Features/SocketDiagnostics/SocketDiagnostics.mjs b/services/web/app/src/Features/SocketDiagnostics/SocketDiagnostics.mjs
new file mode 100644
index 0000000000..74672bde4e
--- /dev/null
+++ b/services/web/app/src/Features/SocketDiagnostics/SocketDiagnostics.mjs
@@ -0,0 +1,11 @@
+import { expressify } from '@overleaf/promise-utils'
+
+const index = async (req, res) => {
+ res.render('project/editor/socket_diagnostics')
+}
+
+const SocketDiagnostics = {
+ index: expressify(index),
+}
+
+export default SocketDiagnostics
diff --git a/services/web/app/src/router.mjs b/services/web/app/src/router.mjs
index 44175b455a..125fdfd385 100644
--- a/services/web/app/src/router.mjs
+++ b/services/web/app/src/router.mjs
@@ -66,6 +66,7 @@ import logger from '@overleaf/logger'
import _ from 'lodash'
import { plainTextResponse } from './infrastructure/Response.js'
import PublicAccessLevels from './Features/Authorization/PublicAccessLevels.js'
+import SocketDiagnostics from './Features/SocketDiagnostics/SocketDiagnostics.mjs'
const ClsiCookieManager = ClsiCookieManagerFactory(
Settings.apis.clsi != null ? Settings.apis.clsi.backendGroupName : undefined
)
@@ -231,6 +232,8 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
webRouter.get('/account-suspended', UserPagesController.accountSuspended)
+ webRouter.get('/socket-diagnostics', SocketDiagnostics.index)
+
if (Settings.enableLegacyLogin) {
AuthenticationController.addEndpointToLoginWhitelist('/login/legacy')
webRouter.get('/login/legacy', UserPagesController.loginPage)
diff --git a/services/web/app/views/project/editor/socket_diagnostics.pug b/services/web/app/views/project/editor/socket_diagnostics.pug
new file mode 100644
index 0000000000..7093bc8343
--- /dev/null
+++ b/services/web/app/views/project/editor/socket_diagnostics.pug
@@ -0,0 +1,20 @@
+extends ../../layout-marketing
+
+block vars
+ - var suppressNavbar = true
+ - var suppressFooter = true
+ - var suppressGoogleAnalytics = true
+ - bootstrap5PageStatus = 'enabled'
+ - isWebsiteRedesign = 'true'
+
+block entrypointVar
+ - entrypoint = 'pages/socket-diagnostics'
+
+block append meta
+
+block content
+ main.content.content-alt#main-content
+ #socket-diagnostics
+
+block prepend foot-scripts
+ script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js', defer=deferScripts)
diff --git a/services/web/frontend/js/features/socket-diagnostics/components/diagnostic-component.tsx b/services/web/frontend/js/features/socket-diagnostics/components/diagnostic-component.tsx
new file mode 100644
index 0000000000..7adddc5329
--- /dev/null
+++ b/services/web/frontend/js/features/socket-diagnostics/components/diagnostic-component.tsx
@@ -0,0 +1,71 @@
+import React from 'react'
+import classnames from 'classnames'
+import type { ConnectionStatus } from './types'
+import { Badge, Button } from 'react-bootstrap-5'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
+import MaterialIcon from '@/shared/components/material-icon'
+
+const variants = {
+ connected: 'success',
+ connecting: 'warning',
+ disconnected: 'danger',
+}
+
+export const ConnectionBadge = ({ state }: { state: ConnectionStatus }) => (
+