From 4e83a64f81270c344f16f23e3816610df534d75a Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Wed, 15 Jul 2026 13:53:08 +0200 Subject: [PATCH] fix(front): read fresh metadata in SSE update path to avoid false unknown-field warnings (#22897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Sentry `warning`: *"SSE update event for person carried fields unknown to this tab's metadata: lastInboundAt, pdlCertifications, pdlBirthYear, …"* ([TWENTY-FRONT issue](https://twenty-v7.sentry.io/issues/7610855477)). The listed fields are all custom fields created by the **People Data Labs app** (`packages/twenty-apps/public/people-data-labs`). The flow that triggers this: 1. The app installs a batch of `person` fields (emits metadata SSE events). 2. Its enrichment logic-function updates a person record with all of those fields (emits a record-update SSE event). Field creation already emits metadata SSE events, and the front applies them **synchronously** to the Jotai metadata store (`MetadataStoreSSEEffect` → `applyChanges` → `store.set`). So the store converges. The bug is that the SSE **record-update** handler doesn't read the converged store. `useTriggerOptimisticEffectFromSseUpdateEvents` reads `objectMetadataItems` from a React/Jotai closure captured at render time. The long-lived SSE subscription in `useTriggerEventStreamCreation` holds a metadata snapshot that lags the store, so even after the field-create events have been applied, the record path still sees the old field set. It then: - flags the new fields as "unknown" and logs to Sentry, and - **drops those field values** from the optimistic update (`getUnknownRecordInputFields` filters them out), so already-loaded views miss the enriched data until a refetch. Metadata events are dispatched before record events within each SSE message (`useTriggerEventStreamCreation` lines 126-128), and the store update is synchronous, so a fresh store read at processing time sees fields that converged in the same or any earlier message. ## Fix Read `objectMetadataItems` fresh from the Jotai store at event-processing time instead of from the render closure, and re-resolve the object metadata item from that fresh list. This eliminates the false-positive warnings and stops dropping legitimately-known field values. Follows the design from #22474: the metadata-event pipeline owns schema convergence; the record pipeline just reads the converged store (now actually reading the current store rather than a stale snapshot). A genuine race where the record update truly precedes the field-create event is still tolerated and still warns. ## Testing - `nx typecheck twenty-front` passes - `nx lint:diff-with-main twenty-front` passes Review in cubic --- ...iggerOptimisticEffectFromSseUpdateEvents.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseUpdateEvents.ts b/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseUpdateEvents.ts index 6862a254c8..f8550267bb 100644 --- a/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseUpdateEvents.ts +++ b/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseUpdateEvents.ts @@ -1,6 +1,6 @@ import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; @@ -13,6 +13,7 @@ import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useU import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput'; import { getUnknownRecordInputFields } from '@/object-record/utils/getUnknownRecordInputFields'; import { captureMessage } from '@sentry/react'; +import { useStore } from 'jotai'; import { useCallback } from 'react'; import { isDefined, isNonEmptyArray } from 'twenty-shared/utils'; import { @@ -21,8 +22,8 @@ import { } from '~/generated-metadata/graphql'; export const useTriggerOptimisticEffectFromSseUpdateEvents = () => { + const store = useStore(); const apolloCoreClient = useApolloCoreClient(); - const { objectMetadataItems } = useObjectMetadataItems(); const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); const { refetchAggregateQueriesForObjectMetadataItem } = useRefetchAggregateQueriesForObjectMetadataItem(); @@ -31,11 +32,20 @@ export const useTriggerOptimisticEffectFromSseUpdateEvents = () => { const triggerOptimisticEffectFromSseUpdateEvents = useCallback( ({ objectRecordEvents, - objectMetadataItem, + objectMetadataItem: objectMetadataItemFromCaller, }: { objectRecordEvents: ObjectRecordEvent[]; objectMetadataItem: EnrichedObjectMetadataItem; }) => { + const objectMetadataItems = store.get( + objectMetadataItemsWithFieldsSelector.atom, + ); + + const objectMetadataItem = + objectMetadataItems.find( + (item) => item.id === objectMetadataItemFromCaller.id, + ) ?? objectMetadataItemFromCaller; + const updateEvents = objectRecordEvents.filter((objectRecordEvent) => { return objectRecordEvent.action === DatabaseEventAction.UPDATED; }); @@ -164,8 +174,8 @@ export const useTriggerOptimisticEffectFromSseUpdateEvents = () => { return isNonEmptyArray(updateEvents); }, [ + store, apolloCoreClient.cache, - objectMetadataItems, objectPermissionsByObjectMetadataId, refetchAggregateQueriesForObjectMetadataItem, upsertRecordsInStore,