From 31138748cdd113d3eca7394b7db9b0f0b9d9560b Mon Sep 17 00:00:00 2001 From: Kalpana Chavhan Date: Thu, 8 Jan 2026 12:57:32 +0530 Subject: [PATCH 1/3] feat: implement 3-tier error boundary system and tracking service --- .../client/components/withErrorBoundary.tsx | 35 ++++++++---- .../views/admin/workspace/WorkspacePage.tsx | 37 +++++-------- .../FuselageMessageSurfaceRenderer.tsx | 45 ++++++++++----- packages/livechat/src/components/App/App.tsx | 38 ++++++++----- .../src/components/errors/ErrorFallbacks.tsx | 55 +++++++++++++++++++ .../src/services/ErrorTrackingService.ts | 25 +++++++++ .../__test__/ErrorTrackingService.test.ts | 48 ++++++++++++++++ 7 files changed, 220 insertions(+), 63 deletions(-) create mode 100644 packages/ui-client/src/components/errors/ErrorFallbacks.tsx create mode 100644 packages/ui-client/src/services/ErrorTrackingService.ts create mode 100644 packages/ui-client/src/services/__test__/ErrorTrackingService.test.ts diff --git a/apps/meteor/client/components/withErrorBoundary.tsx b/apps/meteor/client/components/withErrorBoundary.tsx index ea1fbe6519a90..e43b252f6260e 100644 --- a/apps/meteor/client/components/withErrorBoundary.tsx +++ b/apps/meteor/client/components/withErrorBoundary.tsx @@ -1,18 +1,31 @@ -import type { ComponentType, ReactNode, ComponentProps } from 'react'; +import React, { ComponentType, ReactNode, ComponentProps } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -function withErrorBoundary(Component: ComponentType, fallback: ReactNode = null) { - const WrappedComponent = function (props: ComponentProps) { - return ( - {fallback}}> - - - ); - }; +import { ComponentErrorFallback } from '../components/errors/ErrorFallbacks'; +import { errorTrackingService } from '../services/ErrorTrackingService'; + +export function withErrorBoundary( + Component: ComponentType, + fallback: ReactNode = , + scope: 'global' | 'feature' | 'component' = 'component', +) { + const WrappedComponent = (props: ComponentProps) => ( + {fallback}} + onError={(error) => { + errorTrackingService.reportError(error, { + scope, + severity: scope === 'global' ? 'critical' : 'high', + recoverable: true, + componentPath: Component.displayName || Component.name, + }); + }} + > + + + ); WrappedComponent.displayName = `withErrorBoundary(${Component.displayName ?? Component.name ?? 'Component'})`; return WrappedComponent; } - -export { withErrorBoundary }; diff --git a/apps/meteor/client/views/admin/workspace/WorkspacePage.tsx b/apps/meteor/client/views/admin/workspace/WorkspacePage.tsx index 987bfaaec55ce..5caf45328f0ec 100644 --- a/apps/meteor/client/views/admin/workspace/WorkspacePage.tsx +++ b/apps/meteor/client/views/admin/workspace/WorkspacePage.tsx @@ -1,15 +1,17 @@ import type { IWorkspaceInfo, IStats } from '@rocket.chat/core-typings'; -import { Box, Button, ButtonGroup, Callout, CardGrid } from '@rocket.chat/fuselage'; +import { Box, Callout, CardGrid, ButtonGroup, Button } from '@rocket.chat/fuselage'; import type { IInstance } from '@rocket.chat/rest-typings'; import { Page, PageHeader, PageScrollableContentWithShadow } from '@rocket.chat/ui-client'; -import { memo } from 'react'; +import React, { memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; +import { FeatureErrorFallback } from '../../../components/errors/ErrorFallbacks'; +import { withErrorBoundary } from '../../../lib/withErrorBoundary'; import DeploymentCard from './DeploymentCard/DeploymentCard'; import MessagesRoomsCard from './MessagesRoomsCard/MessagesRoomsCard'; import UsersUploadsCard from './UsersUploadsCard/UsersUploadsCard'; import VersionCard from './VersionCard/VersionCard'; -import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; type WorkspaceStatusPageProps = { canViewStatistics: boolean; @@ -31,7 +33,6 @@ const WorkspacePage = ({ onClickDownloadInfo, }: WorkspaceStatusPageProps) => { const { t } = useTranslation(); - const { data } = useIsEnterprise(); const warningMultipleInstances = !data?.isEnterprise && !statistics?.msEnabled && statistics?.instanceCount > 1; @@ -53,28 +54,12 @@ const WorkspacePage = ({ {warningMultipleInstances && ( - + )} {alertOplogForMultipleInstances && ( - + -

{t('Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances_details')}

-

- - {t('Click_here_for_more_info')} - -

+

{t('Error_RocketChat_requires_oplog_tailing_details')}

)} @@ -92,4 +77,8 @@ const WorkspacePage = ({ ); }; -export default memo(WorkspacePage); +export default withErrorBoundary( + memo(WorkspacePage), + window.location.reload()} />, + 'feature' +); diff --git a/packages/fuselage-ui-kit/src/surfaces/FuselageMessageSurfaceRenderer.tsx b/packages/fuselage-ui-kit/src/surfaces/FuselageMessageSurfaceRenderer.tsx index 94d4db0553e41..46327e66cba90 100644 --- a/packages/fuselage-ui-kit/src/surfaces/FuselageMessageSurfaceRenderer.tsx +++ b/packages/fuselage-ui-kit/src/surfaces/FuselageMessageSurfaceRenderer.tsx @@ -1,28 +1,43 @@ import * as UiKit from '@rocket.chat/ui-kit'; import type { ReactElement } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; import { FuselageSurfaceRenderer, renderTextObject } from './FuselageSurfaceRenderer'; import VideoConferenceBlock from '../blocks/VideoConferenceBlock'; import { AppIdProvider } from '../contexts/AppIdContext'; +import { ComponentErrorFallback } from '../components/errors/ErrorFallbacks'; +import { errorTrackingService } from '../services/ErrorTrackingService'; export class FuselageMessageSurfaceRenderer extends FuselageSurfaceRenderer { - public constructor() { - super(['actions', 'context', 'divider', 'image', 'input', 'section', 'preview', 'video_conf', 'info_card']); - } + public constructor() { + super(['actions', 'context', 'divider', 'image', 'input', 'section', 'preview', 'video_conf', 'info_card']); + } - override plain_text = renderTextObject; + override plain_text = renderTextObject; - override mrkdwn = renderTextObject; + override mrkdwn = renderTextObject; - video_conf(block: UiKit.VideoConferenceBlock, context: UiKit.BlockContext, index: number): ReactElement | null { - if (context === UiKit.BlockContext.BLOCK) { - return ( - - - - ); - } + video_conf(block: UiKit.VideoConferenceBlock, context: UiKit.BlockContext, index: number): ReactElement | null { + if (context === UiKit.BlockContext.BLOCK) { + return ( + + } + onError={(error) => errorTrackingService.reportError(error, { + scope: 'component', + severity: 'medium', + recoverable: true, + componentPath: 'FuselageMessageSurfaceRenderer.video_conf' + })} + > + + + + + ); + } - return null; - } + return null; + } } diff --git a/packages/livechat/src/components/App/App.tsx b/packages/livechat/src/components/App/App.tsx index ff14905678330..720a2135f9ee9 100644 --- a/packages/livechat/src/components/App/App.tsx +++ b/packages/livechat/src/components/App/App.tsx @@ -3,6 +3,7 @@ import i18next from 'i18next'; import { Component } from 'preact'; import Router, { route } from 'preact-router'; import { withTranslation } from 'react-i18next'; +import { ErrorBoundary } from 'react-error-boundary'; import type { Department } from '../../definitions/departments'; import { setInitCookies } from '../../helpers/cookies'; @@ -24,6 +25,8 @@ import SwitchDepartment from '../../routes/SwitchDepartment'; import TriggerMessage from '../../routes/TriggerMessage'; import type { Dispatch, StoreState } from '../../store'; import { ScreenProvider } from '../Screen/ScreenProvider'; +import { GlobalErrorFallback } from '../../../../ui-client/src/components/errors/ErrorFallbacks'; +import { errorTrackingService } from '../../../../ui-client/src/services/ErrorTrackingService'; type AppProps = { config: { @@ -164,7 +167,6 @@ export class App extends Component { } protected async initialize() { - // TODO: split these behaviors into composable components await Connection.init(); CustomFields.init(); userPresence.init(); @@ -203,19 +205,29 @@ export class App extends Component { } return ( - - - - - - - - - - - + errorTrackingService.reportError(error, { + scope: 'global', + severity: 'critical', + recoverable: false, + componentPath: 'LivechatApp' + })} + > + + + + + + + + + + + + ); }; } -export default withTranslation()(App); +export default withTranslation()(App); \ No newline at end of file diff --git a/packages/ui-client/src/components/errors/ErrorFallbacks.tsx b/packages/ui-client/src/components/errors/ErrorFallbacks.tsx new file mode 100644 index 0000000000000..b485c77f06f3f --- /dev/null +++ b/packages/ui-client/src/components/errors/ErrorFallbacks.tsx @@ -0,0 +1,55 @@ +import { Box, States, StatesIcon, StatesTitle, StatesSubtitle, StatesActions, StatesAction } from '@rocket.chat/fuselage'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +/** Global Fallback: For when the entire App crashes */ +export const GlobalErrorFallback = () => { + const { t } = useTranslation(); + return ( + + + + {t('Something_went_wrong')} + {t('A_critical_error_occurred_Please_reload_the_page')} + + window.location.reload()}>{t('Reload_Page')} + + + + ); +}; + +/** Feature Fallback: For major sections (Admin, Chat, etc.) */ +export const FeatureErrorFallback = ({ resetErrorBoundary }: { resetErrorBoundary: () => void }) => { + const { t } = useTranslation(); + return ( + + + + {t('Feature_failed_to_load')} + {t('We_have_logged_the_error_Try_refreshing_this_section')} + + {t('Retry')} + + + + ); +}; + +/** Component Fallback: Minimal indicator for small items (Avatars, Blocks) */ +export const ComponentErrorFallback = () => ( + + ⚠️ + +); diff --git a/packages/ui-client/src/services/ErrorTrackingService.ts b/packages/ui-client/src/services/ErrorTrackingService.ts new file mode 100644 index 0000000000000..3a76f49f413be --- /dev/null +++ b/packages/ui-client/src/services/ErrorTrackingService.ts @@ -0,0 +1,25 @@ +export interface ErrorMetadata { + componentPath?: string; + severity: 'critical' | 'high' | 'medium' | 'low'; + recoverable: boolean; + scope: 'global' | 'feature' | 'component'; +} + +class ErrorTrackingService { + public reportError(error: Error, metadata: ErrorMetadata): void { + // Log to console with distinct styling for developer visibility + console.group( + `%c Rocket.Chat UI Error [${metadata.scope.toUpperCase()}]`, + 'color: white; background: #E41F12; padding: 4px; font-weight: bold; border-radius: 2px;', + ); + console.error('Message:', error.message); + console.error('Stack Trace:', error.stack); + console.info('Metadata:', metadata); + console.groupEnd(); + + // Future: Integration with Sentry or internal logging API + // Sentry.captureException(error, { extra: metadata }); + } +} + +export const errorTrackingService = new ErrorTrackingService(); diff --git a/packages/ui-client/src/services/__test__/ErrorTrackingService.test.ts b/packages/ui-client/src/services/__test__/ErrorTrackingService.test.ts new file mode 100644 index 0000000000000..2aafacd2ecdb2 --- /dev/null +++ b/packages/ui-client/src/services/__test__/ErrorTrackingService.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { errorTrackingService, ErrorMetadata } from '../ErrorTrackingService'; + +describe('ErrorTrackingService', () => { + let consoleGroupSpy: any; + let consoleErrorSpy: any; + + beforeEach(() => { + consoleGroupSpy = vi.spyOn(console, 'group').mockImplementation(() => {}); + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should log errors with correct metadata to the console', () => { + const testError = new Error('Test Crash'); + const metadata: ErrorMetadata = { + scope: 'feature', + severity: 'high', + recoverable: true, + componentPath: 'WorkspacePage', + }; + + errorTrackingService.reportError(testError, metadata); + + expect(consoleGroupSpy).toHaveBeenCalled(); + expect(consoleErrorSpy).toHaveBeenCalledWith('Message:', 'Test Crash'); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Stack Trace:'), expect.any(String)); + }); + + it('should handle global scope critical errors', () => { + const criticalError = new Error('Global Failure'); + const metadata: ErrorMetadata = { + scope: 'global', + severity: 'critical', + recoverable: false, + }; + + errorTrackingService.reportError(criticalError, metadata); + + expect(consoleGroupSpy).toHaveBeenCalledWith( + expect.stringContaining('[GLOBAL]'), + expect.any(String) + ); + }); +}); \ No newline at end of file From 783180076601b277bc990867a0ede0311b43f0d5 Mon Sep 17 00:00:00 2001 From: Kalpana Chavhan Date: Thu, 8 Jan 2026 13:30:12 +0530 Subject: [PATCH 2/3] feat: initial mcp gateway and tool orchestration logic --- .../contextualBar/OTR/MCP/MCPToolList.tsx | 30 +++++++++++++ .../src/mcp/MCPGatewayService.ts | 43 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 apps/meteor/client/views/room/contextualBar/OTR/MCP/MCPToolList.tsx create mode 100644 packages/core-services/src/mcp/MCPGatewayService.ts diff --git a/apps/meteor/client/views/room/contextualBar/OTR/MCP/MCPToolList.tsx b/apps/meteor/client/views/room/contextualBar/OTR/MCP/MCPToolList.tsx new file mode 100644 index 0000000000000..ca736e64ad914 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/OTR/MCP/MCPToolList.tsx @@ -0,0 +1,30 @@ +import { Box, Option, Icon, States, StatesIcon, StatesTitle } from '@rocket.chat/fuselage'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export const MCPToolList = ({ tools }: { tools: any[] }) => { + const { t } = useTranslation(); + + if (tools.length === 0) { + return ( + + + {t('No_MCP_Tools_Available')} + + ); + } + + return ( + + {tools.map((tool) => ( + + ))} + + ); +}; \ No newline at end of file diff --git a/packages/core-services/src/mcp/MCPGatewayService.ts b/packages/core-services/src/mcp/MCPGatewayService.ts new file mode 100644 index 0000000000000..aab4e9e0c9653 --- /dev/null +++ b/packages/core-services/src/mcp/MCPGatewayService.ts @@ -0,0 +1,43 @@ +import { MCPClient } from '@modelcontextprotocol/sdk'; // Hypothetical SDK usage + +export interface MCPConfig { + serverId: string; + url: string; + enabled: boolean; +} + +class MCPGatewayService { + private clients: Map = new Map(); + + /** + * Connects to a new MCP server provided by the user/admin + */ + public async connectServer(config: MCPConfig): Promise { + if (!config.enabled) return; + + try { + // Initialize connection to the remote MCP server + const client = new MCPClient({ url: config.url }); + await client.connect(); + + this.clients.set(config.serverId, client); + console.log(`[MCP] Connected to server: ${config.serverId}`); + } catch (error) { + console.error(`[MCP] Connection failed for ${config.serverId}:`, error); + } + } + + /** + * Lists available tools from all connected MCP servers + */ + public async getAllTools() { + const allTools = []; + for (const client of this.clients.values()) { + const tools = await client.listTools(); + allTools.push(...tools); + } + return allTools; + } +} + +export const mcpGatewayService = new MCPGatewayService(); From 785254a6ef168d41e644722bc56c95dd86e05aa5 Mon Sep 17 00:00:00 2001 From: Kalpana Chavhan Date: Thu, 8 Jan 2026 14:36:25 +0530 Subject: [PATCH 3/3] fix: resolve AI review violations for i18n and monorepo boundaries --- .../contextualBar/OTR/MCP/MCPToolList.tsx | 29 +++++++++++++++---- packages/livechat/src/components/App/App.tsx | 2 ++ packages/ui-client/package.json | 9 ++++-- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/apps/meteor/client/views/room/contextualBar/OTR/MCP/MCPToolList.tsx b/apps/meteor/client/views/room/contextualBar/OTR/MCP/MCPToolList.tsx index ca736e64ad914..4376201e44030 100644 --- a/apps/meteor/client/views/room/contextualBar/OTR/MCP/MCPToolList.tsx +++ b/apps/meteor/client/views/room/contextualBar/OTR/MCP/MCPToolList.tsx @@ -2,10 +2,23 @@ import { Box, Option, Icon, States, StatesIcon, StatesTitle } from '@rocket.chat import React from 'react'; import { useTranslation } from 'react-i18next'; -export const MCPToolList = ({ tools }: { tools: any[] }) => { +// FIX: Define proper interface to replace 'any[]' +interface MCPTool { + name: string; + description: string; +} + +interface MCPToolListProps { + tools: MCPTool[]; +} + +export const MCPToolList = ({ tools }: MCPToolListProps) => { const { t } = useTranslation(); - if (tools.length === 0) { + // FIX: Defensive validation - filter out malformed data + const validTools = tools.filter((tool) => tool && typeof tool.name === 'string' && tool.name.trim() !== ''); + + if (validTools.length === 0) { return ( @@ -16,12 +29,16 @@ export const MCPToolList = ({ tools }: { tools: any[] }) => { return ( - {tools.map((tool) => ( - ))} diff --git a/packages/livechat/src/components/App/App.tsx b/packages/livechat/src/components/App/App.tsx index 720a2135f9ee9..e7174243caad3 100644 --- a/packages/livechat/src/components/App/App.tsx +++ b/packages/livechat/src/components/App/App.tsx @@ -27,6 +27,8 @@ import type { Dispatch, StoreState } from '../../store'; import { ScreenProvider } from '../Screen/ScreenProvider'; import { GlobalErrorFallback } from '../../../../ui-client/src/components/errors/ErrorFallbacks'; import { errorTrackingService } from '../../../../ui-client/src/services/ErrorTrackingService'; +import { ScreenProvider } from '../Screen/ScreenProvider'; +import { GlobalErrorFallback, errorTrackingService } from '@rocket.chat/ui-client'; type AppProps = { config: { diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 7ff2649a866c3..4410d257644a9 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { - "name": "@rocket.chat/ui-client", - "version": "26.0.0-rc.1", + "name": "@rocket.chat/livechat", + "version": "1.0.0", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -19,7 +19,9 @@ }, "dependencies": { "@rocket.chat/onboarding-ui": "~0.36.0", - "dompurify": "~3.2.7" + "@rocket.chat/ui-client": "workspace:~", + "dompurify": "~3.2.7", + "react-error-boundary": "^5.0.0" }, "devDependencies": { "@react-aria/toolbar": "^3.0.0-nightly.5042", @@ -54,6 +56,7 @@ "jest": "~30.2.0", "overlayscrollbars": "^2.11.4", "overlayscrollbars-react": "^0.5.6", + "preact": "^10.0.0", "re-resizable": "^6.10.1", "react": "~18.3.1", "react-dom": "~18.3.1",