socket-diagnostics.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import React, { useEffect } from 'react'
  2. import type { ConnectionStatus } from './types'
  3. import { useSocketManager } from './use-socket-manager'
  4. import {
  5. ActionButton,
  6. ConnectionBadge,
  7. DiagnosticItem,
  8. ErrorAlert,
  9. } from './diagnostic-component'
  10. import { Col, Container, Row } from 'react-bootstrap-5'
  11. import MaterialIcon from '@/shared/components/material-icon'
  12. import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
  13. import { CopyToClipboard } from '@/shared/components/copy-to-clipboard'
  14. type NetworkInformation = {
  15. downlink: number
  16. effectiveType: string
  17. rtt: number
  18. saveData: boolean
  19. type: string
  20. }
  21. const navigatorInfo = (): string[] => {
  22. if (!('connection' in navigator)) {
  23. return ['Network Information API not supported']
  24. }
  25. const connection = navigator.connection as NetworkInformation
  26. return [
  27. `Downlink: ${connection.downlink} Mbps`,
  28. `Effective Type: ${connection.effectiveType}`,
  29. `Round Trip Time: ${connection.rtt} ms`,
  30. `Save Data: ${connection.saveData ? 'Enabled' : 'Disabled'}`,
  31. `Platform: ${navigator.platform}`,
  32. // @ts-ignore
  33. `Device Memory: ${navigator.deviceMemory}`,
  34. `Hardware Concurrency: ${navigator.hardwareConcurrency}`,
  35. ]
  36. }
  37. const useCurrentTime = () => {
  38. const [, setTime] = React.useState(new Date())
  39. useEffect(() => {
  40. const interval = setInterval(() => setTime(new Date()), 1000)
  41. return () => clearInterval(interval)
  42. }, [])
  43. }
  44. type DiagnosticProps = {
  45. icon: string
  46. label: string
  47. text: string[]
  48. type?: 'success' | 'danger'
  49. }
  50. export const SocketDiagnostics = () => {
  51. const {
  52. socketState,
  53. debugInfo,
  54. disconnectSocket,
  55. forceReconnect,
  56. socket,
  57. autoping,
  58. setAutoping,
  59. } = useSocketManager()
  60. useCurrentTime()
  61. const now = new Date()
  62. const getConnectionState = (): ConnectionStatus => {
  63. if (socketState.connected) return 'connected'
  64. if (socketState.connecting) return 'connecting'
  65. return 'disconnected'
  66. }
  67. const lastReceivedS = debugInfo.lastReceived
  68. ? Math.round((now.getTime() - debugInfo.lastReceived) / 1000)
  69. : null
  70. const isLate =
  71. !!debugInfo.unansweredSince &&
  72. now.getTime() - debugInfo.unansweredSince >= 3000
  73. const diagnosticProps: DiagnosticProps[] = [
  74. {
  75. icon: 'network_ping',
  76. label: 'Ping Count',
  77. text: [
  78. `${debugInfo.received} / ${debugInfo.sent}`,
  79. lastReceivedS !== null ? `Last received ${lastReceivedS}s ago` : null,
  80. ].filter(Boolean) as string[],
  81. type: isLate === null ? undefined : isLate ? 'danger' : 'success',
  82. },
  83. {
  84. icon: 'schedule',
  85. label: 'Latency',
  86. text: [
  87. debugInfo.latency
  88. ? `${debugInfo.latency} ms\nMax: ${debugInfo.maxLatency} ms`
  89. : '-',
  90. ],
  91. type: debugInfo.latency && debugInfo.latency < 450 ? 'success' : 'danger',
  92. },
  93. {
  94. icon: 'difference',
  95. label: 'Clock Delta',
  96. text: [
  97. debugInfo.clockDelta === null
  98. ? '-'
  99. : `${Math.round(debugInfo.clockDelta / 1000)}s`,
  100. ],
  101. type:
  102. debugInfo.clockDelta !== null && Math.abs(debugInfo.clockDelta) < 1500
  103. ? 'success'
  104. : 'danger',
  105. },
  106. {
  107. icon: 'signal_cellular_alt',
  108. label: 'Online',
  109. text: [debugInfo.onLine?.toString() ?? '-'],
  110. type: debugInfo.onLine ? 'success' : 'danger',
  111. },
  112. {
  113. icon: 'schedule',
  114. label: 'Current time',
  115. text: [now.toUTCString()],
  116. },
  117. {
  118. icon: 'hourglass',
  119. label: 'Connection time',
  120. text: [
  121. debugInfo.client?.connectedAt
  122. ? `${new Date(debugInfo.client.connectedAt).toUTCString()} (${Math.round(
  123. (Date.now() - debugInfo.client.connectedAt) / 1000
  124. )}s)`
  125. : '-',
  126. ],
  127. },
  128. {
  129. icon: 'local_shipping',
  130. label: 'Transport',
  131. text: [socket?.socket.transport?.name ?? '-'],
  132. },
  133. {
  134. icon: 'badge',
  135. label: 'Client Public ID',
  136. text: [debugInfo.client?.publicId ?? '-'],
  137. },
  138. {
  139. icon: 'pin',
  140. label: 'IP Address',
  141. text: [debugInfo.client?.remoteIp ?? '-'],
  142. },
  143. {
  144. icon: 'web',
  145. label: 'User agent',
  146. text: [debugInfo.client?.userAgent ?? '-'],
  147. },
  148. {
  149. icon: 'directions_boat',
  150. label: 'Navigator info',
  151. text: navigatorInfo(),
  152. },
  153. ]
  154. const diagnosticItems = diagnosticProps.map(item => (
  155. <DiagnosticItem
  156. key={item.label}
  157. icon={item.icon}
  158. label={item.label}
  159. value={item.text.map((t, i) => (
  160. <div key={i}>{t}</div>
  161. ))}
  162. type={item.type}
  163. />
  164. ))
  165. const cutAtIndex = 7
  166. const leftItems = diagnosticItems.slice(0, cutAtIndex)
  167. const rightItems = diagnosticItems.slice(cutAtIndex)
  168. return (
  169. <Container>
  170. <h1>Socket Diagnostics</h1>
  171. <ConnectionBadge state={getConnectionState()} />
  172. <div className="d-flex flex-wrap gap-4 mt-3 align-items-center">
  173. <ActionButton
  174. label="Reconnect"
  175. icon="refresh"
  176. onClick={forceReconnect}
  177. />
  178. <ActionButton
  179. label="Disconnect"
  180. icon="close"
  181. onClick={disconnectSocket}
  182. disabled={!socketState.connected}
  183. />
  184. <OLFormCheckbox
  185. label="Auto ping"
  186. id="autoping"
  187. checked={autoping}
  188. onChange={e => setAutoping(e.target.checked)}
  189. />
  190. </div>
  191. {socketState.lastError && <ErrorAlert message={socketState.lastError} />}
  192. <div className="card p-4 mt-3">
  193. <div className="d-flex flex-wrap gap-4 row-gap-1 justify-content-between align-items-center">
  194. <h3 className="text-lg">
  195. <MaterialIcon type="speed" /> Connection Stats
  196. </h3>
  197. <div className="ms-auto">
  198. <CopyToClipboard
  199. content={diagnosticProps
  200. .map(item => [`${item.label}:`, ...item.text].join('\n'))
  201. .join('\n\n')}
  202. tooltipId="copy-debug-info"
  203. kind="text"
  204. />
  205. </div>
  206. </div>
  207. <Row>
  208. <Col md={6}>{leftItems}</Col>
  209. <Col md={6}>{rightItems}</Col>
  210. </Row>
  211. </div>
  212. </Container>
  213. )
  214. }