Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code d9aa0f9b5b chore: improve monitoring for fix(front): guard undefined recordNode in cache ma
Added code-level monitoring in `getRecordsFromRecordConnection` for malformed connection payloads: edges with missing `node` are now filtered out (to avoid downstream failures) and reported to Sentry as a warning-level message with grouped fingerprint and counts (`invalidEdgeCount`, `edgesCount`). This reduces noisy high-severity exception capture while preserving actionable signal about data-shape anomalies.
2026-05-12 09:48:51 +00:00
Sonarly Claude Code a9b7ec8cb0 fix(front): guard undefined recordNode in cache mapper
https://sonarly.com/issue/36844?type=bug

A frontend runtime error crashes a notes-page record picker flow when a record edge has an undefined node. The exception is handled by Sentry but leaves the user in a broken interaction state.

Fix: Implemented a defensive guard in `getRecordFromRecordNode` so undefined `recordNode` no longer throws during destructuring (`const { id, __typename } = recordNode`). The function now returns an empty object when `recordNode` is missing, preventing the runtime crash seen in Notes relation picker flow. Added a unit regression test and snapshot to lock this behavior.
2026-05-12 09:48:51 +00:00
4 changed files with 36 additions and 4 deletions
@@ -105,3 +105,5 @@ exports[`getRecordFromRecordNode should handle null and undefined values 1`] = `
"phone": undefined,
}
`;
exports[`getRecordFromRecordNode should return an empty object when record node is undefined 1`] = `{}`;
@@ -137,4 +137,12 @@ describe('getRecordFromRecordNode', () => {
expect(result).toMatchSnapshot();
});
it('should return an empty object when record node is undefined', () => {
const result = getRecordFromRecordNode({
recordNode: undefined,
});
expect(result).toMatchSnapshot();
});
});
@@ -7,8 +7,12 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const getRecordFromRecordNode = <T extends ObjectRecord>({
recordNode,
}: {
recordNode: RecordGqlNode;
recordNode: RecordGqlNode | undefined;
}): T => {
if (!isDefined(recordNode)) {
return {} as T;
}
const { id, __typename } = recordNode;
return {
@@ -1,13 +1,31 @@
import { captureMessage, withScope } from '@sentry/react';
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
import { type RecordGqlConnectionEdgesRequired } from '@/object-record/graphql/types/RecordGqlConnectionEdgesRequired';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
export const getRecordsFromRecordConnection = <T extends ObjectRecord>({
recordConnection,
}: {
recordConnection: RecordGqlConnectionEdgesRequired;
}): T[] => {
return recordConnection?.edges?.map((edge) =>
getRecordFromRecordNode<T>({ recordNode: edge.node }),
);
const edges = recordConnection?.edges ?? [];
const invalidEdgeCount = edges.filter((edge) => !isDefined(edge?.node)).length;
if (invalidEdgeCount > 0) {
withScope((scope) => {
scope.setFingerprint(['record-connection-missing-node']);
scope.setLevel('warning');
scope.setExtra('invalidEdgeCount', invalidEdgeCount);
scope.setExtra('edgesCount', edges.length);
captureMessage('Record connection contains edge without node');
});
}
return edges
.filter((edge) => isDefined(edge?.node))
.map((edge) => getRecordFromRecordNode<T>({ recordNode: edge.node }));
};