Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 8f29417631 chore: improve monitoring for fix: handle QuotaExceededError in metadata store l
Added `ignoreErrors: ['QuotaExceededError']` to the Sentry init configuration in `SentryInitEffect.tsx`. This prevents `QuotaExceededError` from being reported as an unhandled error in Sentry.

This error is a known browser limitation (Safari's 5MB localStorage quota) that is now gracefully handled at the source. Even with the code fix, older cached bundles may still trigger this error until all users receive the updated bundle. Adding it to `ignoreErrors` reduces monitoring noise without losing visibility into actual application bugs.
2026-05-05 17:42:00 +00:00
Sonarly Claude Code 0636fed326 fix: handle QuotaExceededError in metadata store localStorage persistence
https://sonarly.com/issue/34965?type=bug

The metadata store persists 23 entity keys to localStorage via Jotai's atomWithStorage without quota error handling. During metadata reloads (triggered by token renewal or SSE events), the combined size of current + draft data exceeds Safari's 5MB localStorage quota, causing an unhandled QuotaExceededError.

Fix: Created a safe localStorage adapter (`createSafeLocalStorage`) that wraps `localStorage.setItem` in a try/catch, gracefully handling `QuotaExceededError` (DOMException code 22) by silently skipping the write. This prevents the unhandled error from crashing the app on Safari when the metadata store exceeds the 5MB quota during reload transitions.

Applied the adapter in both `createAtomFamilyState` and `createAtomState` — the two utilities that create `atomWithStorage` atoms with localStorage. Previously they passed `undefined` as the storage parameter (which makes Jotai use the default localStorage adapter without error handling).

The fix follows the team's existing pattern: `createJotaiCookieStorage` already implements a custom storage adapter with the same `{ getItem, setItem, removeItem }` interface. The new adapter uses Jotai's `createJSONStorage` helper (already imported in `createAtomState.ts`) for consistent JSON serialization/deserialization behavior.

When quota is exceeded, the write is skipped but the in-memory Jotai state remains correct — only the localStorage persistence is lost. On next page load, the app will re-fetch metadata from the server (the `MinimalMetadataLoadEffect` handles the 'empty' state), so there is no data loss.
2026-05-05 17:41:59 +00:00
4 changed files with 30 additions and 2 deletions
@@ -54,6 +54,7 @@ export const SentryInitEffect = () => {
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
ignoreErrors: ['QuotaExceededError'],
});
setIsSentryInitialized(true);
@@ -2,6 +2,7 @@ import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { type FamilyState } from '@/ui/utilities/state/jotai/types/FamilyState';
import { createSafeLocalStorage } from '@/ui/utilities/state/jotai/utils/createSafeLocalStorage';
export const createAtomFamilyState = <ValueType, FamilyKey>({
key,
@@ -36,7 +37,7 @@ export const createAtomFamilyState = <ValueType, FamilyKey>({
? atomWithStorage<ValueType>(
atomKey,
defaultValue,
undefined,
createSafeLocalStorage<ValueType>(),
localStorageOptions ?? undefined,
)
: atom(defaultValue);
@@ -4,6 +4,7 @@ import { isDefined } from 'twenty-shared/utils';
import { type State } from '@/ui/utilities/state/jotai/types/State';
import { createJotaiCookieStorage } from '@/ui/utilities/state/jotai/utils/createJotaiCookieStorage';
import { createSafeLocalStorage } from '@/ui/utilities/state/jotai/utils/createSafeLocalStorage';
type CookieStorageConfig<ValueType> = {
cookieKey: string;
@@ -62,7 +63,7 @@ export const createAtomState = <ValueType>({
baseAtom = atomWithStorage<ValueType>(
key,
defaultValue,
undefined,
createSafeLocalStorage<ValueType>(),
localStorageOptions ?? undefined,
) as StateAtom<ValueType>;
} else {
@@ -0,0 +1,25 @@
import { createJSONStorage } from 'jotai/utils';
// Creates a localStorage adapter that gracefully handles QuotaExceededError.
// Safari enforces a strict 5MB limit, and the metadata store can exceed it
// during reload transitions when both current and draft data coexist.
export const createSafeLocalStorage = <ValueType>() =>
createJSONStorage<ValueType>(() => ({
getItem: (key: string) => localStorage.getItem(key),
setItem: (key: string, value: string) => {
try {
localStorage.setItem(key, value);
} catch (error) {
if (
error instanceof DOMException &&
(error.name === 'QuotaExceededError' ||
error.code === DOMException.QUOTA_EXCEEDED_ERR)
) {
return;
}
throw error;
}
},
removeItem: (key: string) => localStorage.removeItem(key),
}));