fix(storage): prevent history wipe on transient dataStore errors#1052
fix(storage): prevent history wipe on transient dataStore errors#1052ifoche wants to merge 1 commit into
Conversation
StorageDataStoreClient.getObject() was catching ALL errors and returning undefined, making network failures indistinguishable from missing keys. Collection mutation methods (saveObjectInCollection, etc.) treated undefined as "empty collection" via ?? [], then overwrote the dataStore key — wiping sync history on any transient error. Now getObject returns undefined only for 404 (key not found) and re-throws on network errors, 5xx, and timeouts. Also applies the same fix to getMetadataByKey. Adds 6 unit tests. StorageConstantClient was verified to not have this bug — it queries constants via the models API which returns empty arrays (not 404s) for missing entries. Closes: https://app.clickup.com/t/869c32tg2 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BundleMonNo change in files bundle size Groups updated (1)
Final result: ✅ View report in BundleMon website ➡️ |
xurxodev
left a comment
There was a problem hiding this comment.
thanks @ifoche
This fixes a real and well-diagnosed bug: StorageDataStoreClient.getObject() was treating any error (network, 5xx, timeout) the same as a 404, causing collection mutation methods (saveObjectInCollection, removeObjectsInCollection, etc.) to overwrite the dataStore with an empty collection on a transient failure.
The fix itself in StorageDataStoreClient.ts is correct and well tested (404 vs 500 vs network error vs timeout are all covered, including the case where error.response is undefined). However, the fix doesn't fully close the loop for the use case that motivated it, the PR's own checklist (tasks.md) is left incomplete, and several callers downstream now need to handle a new failure mode that didn't exist before.
1. Should fix
-
src/data/reports/ReportsD2ApiRepository.ts:44-58, 69-89—save(),delete(), anddeleteByIds()wrap the calls tosaveObjectInCollection/removeObjectInCollection/removeObjectsInCollectionin atry/catchthat doesconsole.error(error)and swallows it without rethrowing. These are exactly the methods behindreports.save()on every sync anduseDeleteHistoryon app load — the flows named inproposal.mdas the cause of the history wipe. With this catch in place, the error now correctly thrown bygetObjectnever reaches the caller: the storage layer is fixed, but the history use case still silently "succeeds" even when the report wasn't actually saved. This needs at least an explicit decision (rethrow and let the caller surface it, or report the failure some other way) — right now the fix is not observable for the case it was written for. -
openspec/changes/fix-history-wipe-on-storage-error/tasks.md:7— task 2.1 ("apply the same fix toStorageConstantClient") is unchecked and not implemented;StorageConstantClient.tshas zero diff. Butspec.mddeclares as a MUST requirement that "Error handling SHALL be consistent across storage backends." Concretely,StorageConstantClient.clearStorage()still has the same silent catch-all (catch (error) { console.error(error); }) that this PR removes from the other backend. As written, the PR doesn't satisfy its own spec, and the original bug can still occur on instances configured with theconstantstorage backend. -
StartApplicationUseCase→listObjectsInCollection(Namespace.INSTANCES)→getObject) with notry/catchanywhere in that chain. Before this change, a
transient network error here resolved silently to an empty/default state; now it throws. Since nothing catches it,appContextis never set and the app is stuck on an infinite loading state with no error message on app startup. This is a direct, app-wide side effect of wideninggetObject's contract from "never throws" to "can throw," and it isn't mentioned in the design doc's risk section. -
Test doubles don't follow the project's established pattern. The rest of the codebase builds test doubles with
ts-mockito(mock/when/instance) plusgivenA...helper functions that keep construction out of the test body (seesrc/domain/metadata/builders/__tests__/MetadataPayloadBuilder.spec.tsx). The newsrc/data/storage/__tests__/StorageDataStoreClient.spec.tsinstead hand-rolls mocks withvi.fn()and reimplementsCancelableResponse's internal shape via localbuildCancelableResponse/buildCancelableRejectionhelpers (lines 29-40), coupling the test to an SDK abstraction it doesn't even need to exercise (only.getData()is used). Worth aligning with the existing convention for consistency and to avoid breaking this test on unrelatedCancelableResponsesignature changes.
2. Recommendations non blocking
-
StorageDataStoreClient.clone()iterates all keys callinggetObjectwith no localtry/catch. Previously a transient error on one key degraded toundefinedand the backup/export continued; now a single transient failure aborts the entireclone(). This may be the right behavior (avoid partial/corrupt backups) but it's a real behavior change that isn't covered by a test or mentioned indesign.md— worth a deliberate test or at least a note. -
Several use cases that call
StorageClientcollection methods directly have no error handling (CreatePullRequestUseCase,SetResponsiblesUseCase, rule save/list flows inSummaryStep.tsxandSyncRulesListPage.tsx). None of these crash visually (no Error Boundary triggers, no render-time null deref found), but several leave UI state stuck — e.g.SummaryStep.save()leavesisSavingtrueforever ifrules.save()throws, with no error feedback to the user. Not blocking for this PR's scope, but worth a follow-up pass now thatgetObjectcan throw where it previously couldn't. -
Test coverage gives a false sense of safety for the motivating bug. The
saveObjectInCollectionpropagation test verifies behavior at theStorageClientlevel, but sinceReportsD2ApiRepository.save()wraps that same call in its own swallowingtry/catch(see Should-fix #1), there's no test exercising the actual history use case end-to-end that would catch a regression of the original wipe bug.
Summary
StorageDataStoreClient.getObject()silently returningundefinedon all errors (network, 5xx, timeout), which was indistinguishable from "key not found" (404)saveObjectInCollectioncall — the existing history was treated as empty and overwritten with just the new entrygetObjectreturnsundefinedonly for 404 and re-throws on transient errors. Same fix applied togetMetadataByKey()StorageConstantClientwas verified to not have this bug (it uses the models API which returns empty arrays, not 404s)Related Tasks
Test plan
yarn test— all 394 tests pass (48 files)yarn build core-app— builds cleanlyundefined(e.g., first-time app setup works correctly)🤖 Generated with Claude Code