fix(startup): make the deferred crash report survive its races
The persist-then-flush model had four ways to lose or corrupt the record it exists to protect. A no-op hub — which is what a failed or timed-out crash-reporting init leaves behind, because that phase is best effort — accepts an event and returns an empty id without throwing. "Did not throw" was treated as delivery, so the record was marked reported and suppressed forever. Delivery now requires a non-empty Sentry id, and init completion is tracked explicitly rather than assumed. Opting out, and building without a DSN, are deliberate suppression rather than delivery failure: both mark the record resolved so it is not rediscovered every launch. Everything else stays pending, and consumption no longer deletes an unreported record — deleting it ended the only retry there was, which made "the next launch tries again" false. The write path is now a queue. Record writes were launched unawaited from the failure path, so a fast retry could flush before the file existed, consume before a late write landed, or run two writers against one file and let the older one finish last. markReported joins the same queue and compares record identity before rewriting, because reading and writing outside it let a concurrent record land in between and be overwritten by the record it had just superseded. Records carry an id so that comparison is meaningful. Consumption also waits on a registered flush, so the success path cannot delete the file mid-send. Also routes the tvOS recovery marker through the tolerant read. reconcile() runs inside AppDatabase.open, a fatal gate step, so a wrong-typed marker vetoed the launch outright on a first-class TV target. Both new guards have regression tests verified to fail without the fix.
This commit is contained in:
@@ -467,6 +467,26 @@ void main() {
|
||||
expect(prefs.getString(TvosDatabaseRecoveryStore.manifestKey), contains('committed'));
|
||||
});
|
||||
|
||||
test('a wrong-typed recovery marker behaves like a missing one', () async {
|
||||
// `reconcile` runs inside `AppDatabase.open`, a fatal startup step, so
|
||||
// a mistyped marker used to throw a raw TypeError and veto the launch
|
||||
// outright on a first-class TV target (#1732). An unreadable marker
|
||||
// tells us nothing, which is the same position as an absent one.
|
||||
await prefs.setString(TvosDatabaseRecoveryStore.recoveryRequiredKey, 'yes');
|
||||
|
||||
final result = await open();
|
||||
|
||||
expect(result.recoveryOutcome, TvosDatabaseRecoveryOutcome.fresh);
|
||||
expect(prefs.getString(TvosDatabaseRecoveryStore.manifestKey), contains('committed'));
|
||||
|
||||
// The unreadable value is dropped so the next launch starts clean. The
|
||||
// removal is fire-and-forget, so give it a turn to land.
|
||||
for (var i = 0; i < 20 && prefs.keys.contains(TvosDatabaseRecoveryStore.recoveryRequiredKey); i++) {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
}
|
||||
expect(prefs.keys, isNot(contains(TvosDatabaseRecoveryStore.recoveryRequiredKey)));
|
||||
});
|
||||
|
||||
test('missing database with prior-install evidence requires recovery', () async {
|
||||
await prefs.setString('active_app_profile_id', 'surviving-profile');
|
||||
final result = await open();
|
||||
|
||||
Reference in New Issue
Block a user