From 2c24bb56542594adb50a3d03f17099a96c4a7795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Wed, 15 Apr 2026 10:37:05 +0200 Subject: [PATCH 1/4] feat: Log original error --- lib/OnyxUtils.ts | 16 ++++++++-------- tests/unit/onyxUtilsTest.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 46bdaac47..b38da0727 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -765,13 +765,13 @@ function remove(key: TKey, isProcessingCollectionUpdate?: return Storage.removeItem(key).then(() => undefined); } -function reportStorageQuota(): Promise { +function reportStorageQuota(error?: Error): Promise { return Storage.getDatabaseSize() .then(({bytesUsed, bytesRemaining}) => { - Logger.logInfo(`Storage Quota Check -- bytesUsed: ${bytesUsed} bytesRemaining: ${bytesRemaining}`); + Logger.logInfo(`Storage Quota Check -- bytesUsed: ${bytesUsed} bytesRemaining: ${bytesRemaining}. Original error: ${error}`); }) .catch((dbSizeError) => { - Logger.logAlert(`Unable to get database size. Error: ${dbSizeError}`); + Logger.logAlert(`Unable to get database size. Original error: ${error}. getDatabaseSize error: ${dbSizeError}`); }); } @@ -788,7 +788,7 @@ function retryOperation(error: Error, on Logger.logInfo(`Failed to save to storage. Error: ${error}. onyxMethod: ${onyxMethod.name}. retryAttempt: ${currentRetryAttempt}/${MAX_STORAGE_OPERATION_RETRY_ATTEMPTS}`); if (error && Str.startsWith(error.message, "Failed to execute 'put' on 'IDBObjectStore'")) { - Logger.logAlert('Attempted to set invalid data set in Onyx. Please ensure all data is serializable.'); + Logger.logAlert(`Attempted to set invalid data set in Onyx. Please ensure all data is serializable. Error: ${error}`); throw error; } @@ -812,13 +812,13 @@ function retryOperation(error: Error, on // If we have no acceptable keys to remove then we are possibly trying to save mission critical data. If this is the case, // then we should stop retrying as there is not much the user can do to fix this. Instead of getting them stuck in an infinite loop we // will allow this write to be skipped. - Logger.logAlert('Out of storage. But found no acceptable keys to remove.'); - return reportStorageQuota(); + Logger.logAlert(`Out of storage. But found no acceptable keys to remove. Error: ${error}`); + return reportStorageQuota(error); } // Remove the least recently accessed key and retry. - Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`); - reportStorageQuota(); + Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying. Error: ${error}`); + reportStorageQuota(error); // @ts-expect-error No overload matches this call. return remove(keyForRemoval).then(() => onyxMethod(defaultParams, nextRetryAttempt)); diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index f124639b6..d4bceec61 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -6,6 +6,7 @@ import utils from '../../lib/utils'; import type {Collection, OnyxCollection} from '../../lib/types'; import type GenericCollection from '../utils/GenericCollection'; import OnyxCache from '../../lib/OnyxCache'; +import * as Logger from '../../lib/Logger'; import StorageMock from '../../lib/storage'; import createDeferredTask from '../../lib/createDeferredTask'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; @@ -395,6 +396,26 @@ describe('OnyxUtils', () => { expect(retryOperationSpy).toHaveBeenCalledTimes(1); }); + it('should include the error in logAlert for IDBObjectStore invalid data errors', async () => { + const logAlertSpy = jest.spyOn(Logger, 'logAlert'); + StorageMock.setItem = jest.fn().mockRejectedValueOnce(invalidDataError); + + await expect(Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'})).rejects.toThrow(invalidDataError); + + expect(logAlertSpy).toHaveBeenCalledWith(`Attempted to set invalid data set in Onyx. Please ensure all data is serializable. Error: ${invalidDataError}`); + }); + + it('should include the error in logs when out of storage with no evictable keys', async () => { + const logAlertSpy = jest.spyOn(Logger, 'logAlert'); + const logInfoSpy = jest.spyOn(Logger, 'logInfo'); + StorageMock.setItem = jest.fn().mockRejectedValue(memoryError); + + await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); + + expect(logAlertSpy).toHaveBeenCalledWith(`Out of storage. But found no acceptable keys to remove. Error: ${memoryError}`); + expect(logInfoSpy).toHaveBeenCalledWith(`Storage Quota Check -- bytesUsed: 0 bytesRemaining: Infinity. Original error: ${memoryError}`); + }); + it('should not re-add an evicted key to recentlyAccessedKeys after removal', async () => { // Re-init with evictable keys so getKeyForEviction() has something to return Object.assign(OnyxUtils.getDeferredInitTask(), createDeferredTask()); @@ -422,6 +443,7 @@ describe('OnyxUtils', () => { let LocalOnyxUtils: typeof OnyxUtils; let LocalOnyxCache: typeof OnyxCache; let LocalStorageMock: typeof StorageMock; + let LocalLogger: typeof Logger; // Reset all modules to get fresh singletons (OnyxCache, OnyxUtils, etc.) // then re-init Onyx with evictableKeys configured @@ -432,6 +454,7 @@ describe('OnyxUtils', () => { LocalOnyxUtils = require('../../lib/OnyxUtils').default; LocalOnyxCache = require('../../lib/OnyxCache').default; LocalStorageMock = require('../../lib/storage').default; + LocalLogger = require('../../lib/Logger'); LocalOnyx.init({ keys: ONYXKEYS, @@ -537,6 +560,20 @@ describe('OnyxUtils', () => { expect(keyForEviction).toBeDefined(); expect(keyForEviction?.startsWith(ONYXKEYS.COLLECTION.TEST_KEY)).toBe(true); }); + + it('should include the error in logs when evicting a key', async () => { + const logInfoSpy = jest.spyOn(LocalLogger, 'logInfo'); + const key1 = `${ONYXKEYS.COLLECTION.TEST_KEY}1`; + + await LocalOnyx.set(key1, {id: 1}); + + LocalStorageMock.setItem = jest.fn(LocalStorageMock.setItem).mockRejectedValueOnce(diskFullError).mockImplementation(LocalStorageMock.setItem); + + await LocalOnyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); + + expect(logInfoSpy).toHaveBeenCalledWith(`Out of storage. Evicting least recently accessed key (${key1}) and retrying. Error: ${diskFullError}`); + expect(logInfoSpy).toHaveBeenCalledWith(`Storage Quota Check -- bytesUsed: 0 bytesRemaining: Infinity. Original error: ${diskFullError}`); + }); }); describe('afterInit', () => { From b23b74b961f7267828764d6eaa6fd784aba11ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Wed, 15 Apr 2026 11:47:35 +0200 Subject: [PATCH 2/4] fix: one more test --- tests/unit/onyxUtilsTest.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index d4bceec61..2c2a09f4c 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -416,6 +416,17 @@ describe('OnyxUtils', () => { expect(logInfoSpy).toHaveBeenCalledWith(`Storage Quota Check -- bytesUsed: 0 bytesRemaining: Infinity. Original error: ${memoryError}`); }); + it('should include the error in logAlert when out of storage and getDatabaseSize fails', async () => { + const dbSizeError = new Error('Failed to estimate storage'); + const logAlertSpy = jest.spyOn(Logger, 'logAlert'); + StorageMock.setItem = jest.fn().mockRejectedValue(memoryError); + StorageMock.getDatabaseSize = jest.fn().mockRejectedValue(dbSizeError); + + await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); + + expect(logAlertSpy).toHaveBeenCalledWith(`Unable to get database size. Original error: ${memoryError}. getDatabaseSize error: ${dbSizeError}`); + }); + it('should not re-add an evicted key to recentlyAccessedKeys after removal', async () => { // Re-init with evictable keys so getKeyForEviction() has something to return Object.assign(OnyxUtils.getDeferredInitTask(), createDeferredTask()); From c1ecb3fcabe17b16d768d78f3f00df2247148802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Thu, 16 Apr 2026 08:47:05 +0200 Subject: [PATCH 3/4] fix: change error order --- lib/OnyxUtils.ts | 2 +- tests/unit/onyxUtilsTest.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index b38da0727..c55a5485f 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -771,7 +771,7 @@ function reportStorageQuota(error?: Error): Promise { Logger.logInfo(`Storage Quota Check -- bytesUsed: ${bytesUsed} bytesRemaining: ${bytesRemaining}. Original error: ${error}`); }) .catch((dbSizeError) => { - Logger.logAlert(`Unable to get database size. Original error: ${error}. getDatabaseSize error: ${dbSizeError}`); + Logger.logAlert(`Unable to get database size. getDatabaseSize error: ${dbSizeError}. Original error: ${error}`); }); } diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index 2c2a09f4c..99bafeda3 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -424,7 +424,7 @@ describe('OnyxUtils', () => { await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); - expect(logAlertSpy).toHaveBeenCalledWith(`Unable to get database size. Original error: ${memoryError}. getDatabaseSize error: ${dbSizeError}`); + expect(logAlertSpy).toHaveBeenCalledWith(`Unable to get database size. getDatabaseSize error: ${dbSizeError}. Original error: ${memoryError}`); }); it('should not re-add an evicted key to recentlyAccessedKeys after removal', async () => { From e08c160012833b6745359e6f590e13015ce3300e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Thu, 16 Apr 2026 09:24:03 +0200 Subject: [PATCH 4/4] fix: after rebase --- tests/unit/onyxUtilsTest.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index 5817f9670..e1c847635 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -408,23 +408,23 @@ describe('OnyxUtils', () => { it('should include the error in logs when out of storage with no evictable keys', async () => { const logAlertSpy = jest.spyOn(Logger, 'logAlert'); const logInfoSpy = jest.spyOn(Logger, 'logInfo'); - StorageMock.setItem = jest.fn().mockRejectedValue(memoryError); + StorageMock.setItem = jest.fn().mockRejectedValue(diskFullError); await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); - expect(logAlertSpy).toHaveBeenCalledWith(`Out of storage. But found no acceptable keys to remove. Error: ${memoryError}`); - expect(logInfoSpy).toHaveBeenCalledWith(`Storage Quota Check -- bytesUsed: 0 bytesRemaining: Infinity. Original error: ${memoryError}`); + expect(logAlertSpy).toHaveBeenCalledWith(`Out of storage. But found no acceptable keys to remove. Error: ${diskFullError}`); + expect(logInfoSpy).toHaveBeenCalledWith(`Storage Quota Check -- bytesUsed: 0 bytesRemaining: Infinity. Original error: ${diskFullError}`); }); it('should include the error in logAlert when out of storage and getDatabaseSize fails', async () => { const dbSizeError = new Error('Failed to estimate storage'); const logAlertSpy = jest.spyOn(Logger, 'logAlert'); - StorageMock.setItem = jest.fn().mockRejectedValue(memoryError); + StorageMock.setItem = jest.fn().mockRejectedValue(diskFullError); StorageMock.getDatabaseSize = jest.fn().mockRejectedValue(dbSizeError); await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); - expect(logAlertSpy).toHaveBeenCalledWith(`Unable to get database size. getDatabaseSize error: ${dbSizeError}. Original error: ${memoryError}`); + expect(logAlertSpy).toHaveBeenCalledWith(`Unable to get database size. getDatabaseSize error: ${dbSizeError}. Original error: ${diskFullError}`); }); it('should not re-add an evicted key to recentlyAccessedKeys after removal', async () => {