diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 08ecc1ea3..8f2b56298 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -763,13 +763,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. getDatabaseSize error: ${dbSizeError}. Original error: ${error}`); }); } @@ -786,7 +786,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; } @@ -810,13 +810,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 3713b7198..e1c847635 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,37 @@ 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(diskFullError); + + await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); + + 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(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: ${diskFullError}`); + }); + 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 +454,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 +465,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 +571,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', () => {