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
7 changed files with 39 additions and 16 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 }));
};
@@ -8,7 +8,6 @@ import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspac
import { PublicDomainEntity } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
describe('WorkspaceDomainsService', () => {
let workspaceDomainsService: WorkspaceDomainsService;
@@ -214,7 +213,7 @@ describe('WorkspaceDomainsService', () => {
expect(result?.id).toEqual('workspace-id');
});
it('should return Apple workspace if multiple workspaces when IS_MULTIWORKSPACE_ENABLED=false', async () => {
it('should return 1st workspace if multiple workspaces when IS_MULTIWORKSPACE_ENABLED=false', async () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
@@ -231,9 +230,6 @@ describe('WorkspaceDomainsService', () => {
{
id: 'workspace-id1',
},
{
id: SEED_APPLE_WORKSPACE_ID,
},
{
id: 'workspace-id2',
},
@@ -244,7 +240,7 @@ describe('WorkspaceDomainsService', () => {
'https://example.com',
);
expect(result?.id).toEqual(SEED_APPLE_WORKSPACE_ID);
expect(result?.id).toEqual('workspace-id1');
});
it('should return workspace by subdomain', async () => {
@@ -9,7 +9,6 @@ import { buildUrlWithPathnameAndSearchParams } from 'src/engine/core-modules/dom
import { WorkspaceDomainConfig } from 'src/engine/core-modules/domain/workspace-domains/types/workspace-domain-config.type';
import { PublicDomainEntity } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
@@ -78,10 +77,7 @@ export class WorkspaceDomainsService {
);
}
const foundWorkspace =
workspaces.length === 1
? workspaces[0]
: workspaces.find((workspace) => workspace.id === SEED_APPLE_WORKSPACE_ID);
const foundWorkspace = workspaces[0];
assertIsDefinedOrThrow(foundWorkspace, WorkspaceNotFoundDefaultError);
@@ -3,5 +3,4 @@ import { FeatureFlagKey } from 'twenty-shared/types';
export const DEFAULT_FEATURE_FLAGS = [
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
FeatureFlagKey.IS_BILLING_V2_ENABLED,
] as const satisfies FeatureFlagKey[];