https://sonarly.com/issue/38763?type=bug Opening a specific workflow record crashes the page with a frontend TypeError when rendering workflow variable labels. The failure is caused by unsafe access to iterator schema fields during variable-chip rendering. Fix: Implemented a defensive fix in iterator variable search so malformed iterator schemas no longer crash rendering. What changed: - In `searchVariableThroughIteratorOutputSchema`, I now guard `iteratorOutputSchema.currentItem` before any property access. - If `currentItem` is missing, the function safely returns `{ variableLabel: undefined, variablePathLabel: undefined }` (same non-crashing fallback used elsewhere). - Subsequent label/type reads now use a local `currentItem` variable, avoiding repeated unsafe property access. Why this fixes the incident: - The production crash was caused by direct dereference of `currentItem.value` when `currentItem` was undefined for a specific workflow schema payload. - With this guard, malformed/incomplete iterator output schema data degrades gracefully instead of throwing in render. I also added a regression test that explicitly covers “currentItem missing from iterator output schema” and asserts the safe undefined-label fallback. Authored by Sonarly by autonomous analysis (run 44106).
90 lines
2.4 KiB
TypeScript
90 lines
2.4 KiB
TypeScript
import { useCallback, useEffect } from 'react';
|
|
|
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|
import {
|
|
CombinedGraphQLErrors,
|
|
CombinedProtocolErrors,
|
|
LinkError,
|
|
LocalStateError,
|
|
ServerError,
|
|
ServerParseError,
|
|
UnconventionalError,
|
|
} from '@apollo/client/errors';
|
|
import { isDefined, type CustomError } from 'twenty-shared/utils';
|
|
|
|
const isApolloError = (error: unknown): boolean =>
|
|
CombinedGraphQLErrors.is(error) ||
|
|
CombinedProtocolErrors.is(error) ||
|
|
LinkError.is(error) ||
|
|
LocalStateError.is(error) ||
|
|
ServerError.is(error) ||
|
|
ServerParseError.is(error) ||
|
|
UnconventionalError.is(error);
|
|
|
|
const hasErrorCode = (
|
|
error: CustomError | any,
|
|
): error is CustomError & { code: string } => {
|
|
return 'code' in error && isDefined(error.code);
|
|
};
|
|
|
|
export const PromiseRejectionEffect = () => {
|
|
const { enqueueErrorSnackBar } = useSnackBar();
|
|
|
|
const handlePromiseRejection = useCallback(
|
|
async (event: PromiseRejectionEvent) => {
|
|
const error = event.reason;
|
|
if (isApolloError(error)) {
|
|
enqueueErrorSnackBar({
|
|
apolloError: error,
|
|
});
|
|
return; // already handled by apolloLink
|
|
}
|
|
|
|
const isAbortError =
|
|
error?.networkError?.name === 'AbortError' ||
|
|
error?.name === 'AbortError';
|
|
|
|
if (isAbortError) {
|
|
return;
|
|
}
|
|
|
|
enqueueErrorSnackBar(
|
|
error instanceof Error ? { message: error.message } : {},
|
|
);
|
|
|
|
try {
|
|
const { captureException } = await import('@sentry/react');
|
|
captureException(error, (scope) => {
|
|
scope.setExtras({ mechanism: 'onUnhandle' });
|
|
|
|
const fingerprint = hasErrorCode(error) ? error.code : error.message;
|
|
|
|
if (isDefined(fingerprint)) {
|
|
scope.setFingerprint([fingerprint]);
|
|
}
|
|
|
|
if (error instanceof Error) {
|
|
error.name = error.message;
|
|
}
|
|
|
|
return scope;
|
|
});
|
|
} catch (sentryError) {
|
|
// oxlint-disable-next-line no-console
|
|
console.error('Failed to capture exception with Sentry:', sentryError);
|
|
}
|
|
},
|
|
[enqueueErrorSnackBar],
|
|
);
|
|
|
|
useEffect(() => {
|
|
window.addEventListener('unhandledrejection', handlePromiseRejection);
|
|
|
|
return () => {
|
|
window.removeEventListener('unhandledrejection', handlePromiseRejection);
|
|
};
|
|
}, [handlePromiseRejection]);
|
|
|
|
return <></>;
|
|
};
|