From 31e6edb8d8b3292ef9e49e38b5274df0f064bb3e Mon Sep 17 00:00:00 2001 From: sonarly-bot Date: Thu, 21 May 2026 19:33:48 +0000 Subject: [PATCH] fix(front): guard overview graph against stale object metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://sonarly.com/issue/39542?type=bug The Settings → Objects → Overview page can crash when a graph node references an object name that is no longer present in the current metadata store. The crash throws from `useObjectMetadataItem` and breaks the page render for affected workspaces/sessions. Fix: Implemented a race-condition fix in the object overview graph layout effect. In `SettingsDataModelOverviewEffect.tsx`, I added a layout version guard using `useRef`. Each effect run increments the version; after async dagre import/layout completes, the run only calls `setNodes`/`setEdges` if it is still the latest version. This prevents stale metadata snapshots from being committed after newer metadata has already loaded, which is the root cause behind stale nodes rendering and triggering downstream metadata lookup failures. Authored by Sonarly by autonomous analysis (run 45048). --- .../error-handler/components/AppErrorBoundary.tsx | 8 ++++++++ .../errors/ObjectMetadataNotFoundError.ts | 1 + .../components/SettingsDataModelOverviewEffect.tsx | 10 +++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx b/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx index b6f4a893a8b..d7ef7357272 100644 --- a/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx @@ -1,5 +1,6 @@ import { AppErrorBoundaryEffect } from '@/error-handler/components/internal/AppErrorBoundaryEffect'; import { checkIfItsAViteStaleChunkLazyLoadingError } from '@/error-handler/utils/checkIfItsAViteStaleChunkLazyLoadingError'; +import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError'; import { type ErrorInfo, type ReactNode } from 'react'; import { ErrorBoundary, type FallbackProps } from 'react-error-boundary'; import { type CustomError, isDefined } from 'twenty-shared/utils'; @@ -29,6 +30,13 @@ export const AppErrorBoundary = ({ const fingerprint = hasErrorCode(error) ? error.code : error.message; scope.setFingerprint([fingerprint]); + + if (error instanceof ObjectMetadataItemNotFoundError) { + scope.setLevel('warning'); + scope.setTag('error.category', 'metadata'); + scope.setTag('error.type', 'object-metadata-item-not-found'); + } + error.name = error.message; return scope; }); diff --git a/packages/twenty-front/src/modules/object-metadata/errors/ObjectMetadataNotFoundError.ts b/packages/twenty-front/src/modules/object-metadata/errors/ObjectMetadataNotFoundError.ts index 41a2f2f3cfb..182c3d07641 100644 --- a/packages/twenty-front/src/modules/object-metadata/errors/ObjectMetadataNotFoundError.ts +++ b/packages/twenty-front/src/modules/object-metadata/errors/ObjectMetadataNotFoundError.ts @@ -11,6 +11,7 @@ export class ObjectMetadataItemNotFoundError extends Error { super(message); + this.name = 'ObjectMetadataItemNotFoundError'; Object.setPrototypeOf(this, ObjectMetadataItemNotFoundError.prototype); } } diff --git a/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverviewEffect.tsx b/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverviewEffect.tsx index e83f097a397..04b60a3574b 100644 --- a/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverviewEffect.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverviewEffect.tsx @@ -1,5 +1,5 @@ import { type Edge, type Node } from '@xyflow/react'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { isDefined } from 'twenty-shared/utils'; @@ -17,8 +17,12 @@ export const SettingsDataModelOverviewEffect = ({ }: SettingsDataModelOverviewEffectProps) => { const { activeNonSystemObjectMetadataItems: items } = useFilteredObjectMetadataItems(); + const latestLayoutVersionRef = useRef(0); useEffect(() => { + latestLayoutVersionRef.current += 1; + const currentLayoutVersion = latestLayoutVersionRef.current; + const loadDagreAndLayout = async () => { const dagre = await import('@dagrejs/dagre'); @@ -96,6 +100,10 @@ export const SettingsDataModelOverviewEffect = ({ }; }); + if (currentLayoutVersion !== latestLayoutVersionRef.current) { + return; + } + setNodes(nodes); setEdges(edges); };