fix(sync): improve error visibility in SuperSync operations

- Add logging to empty catch block in validation (is-related-model-data-valid.ts)
- Notify users when ANY ops are rejected, not just >= 10 (rejected-ops-handler.service.ts)
- Change encryption auto-disable snackbar from SUCCESS to WARNING
- Add logging when sync-config-default-override.json fails to load
- Add DIALOG_RESULT_ERROR translation key for sync dialog errors
- Add test for single op rejection notification
This commit is contained in:
Johannes Millan 2026-01-27 14:44:48 +01:00
parent 48f9967cf5
commit b911546e53
7 changed files with 35 additions and 10 deletions

View file

@ -132,7 +132,11 @@ export class SyncConfigService {
encryptKey: '',
};
})
.catch(() => {
.catch((e) => {
SyncLog.warn(
'Failed to load sync-config-default-override.json, using base config:',
e,
);
return {
...baseConfig,
encryptKey: '',

View file

@ -831,9 +831,9 @@ export class OperationLogSyncService {
'OperationLogSyncService: Local encryption config updated to match server state.',
);
// Notify user
// Notify user - use WARNING since this is a security-relevant change
this.snackService.open({
type: 'SUCCESS',
type: 'WARNING',
msg: T.F.SYNC.S.ENCRYPTION_DISABLED_ON_OTHER_DEVICE,
});
}

View file

@ -8,7 +8,6 @@ import { OperationLogStoreService } from '../persistence/operation-log-store.ser
import { SnackService } from '../../core/snack/snack.service';
import { StaleOperationResolverService } from './stale-operation-resolver.service';
import { Operation, OpType, ActionType } from '../core/operation.types';
import { MAX_REJECTED_OPS_BEFORE_WARNING } from '../core/operation-log.const';
import { T } from '../../t.const';
describe('RejectedOpsHandlerService', () => {
@ -110,9 +109,28 @@ describe('RejectedOpsHandlerService', () => {
expect(opLogStoreSpy.markRejected).toHaveBeenCalledWith(['op-1']);
});
it('should show snack warning when many ops are rejected', async () => {
it('should show snack warning when even 1 op is rejected', async () => {
const op = createOp({ id: 'op-1' });
opLogStoreSpy.getOpById.and.returnValue(Promise.resolve(mockEntry(op)));
opLogStoreSpy.markRejected.and.resolveTo();
await service.handleRejectedOps([
{ opId: 'op-1', error: 'validation error', errorCode: 'VALIDATION_ERROR' },
]);
expect(snackServiceSpy.open).toHaveBeenCalledWith(
jasmine.objectContaining({
type: 'ERROR',
msg: T.F.SYNC.S.UPLOAD_OPS_REJECTED,
translateParams: { count: 1 },
}),
);
});
it('should show snack warning with correct count when multiple ops are rejected', async () => {
const opCount = 10;
const rejectedOps: Array<{ opId: string; error: string }> = [];
for (let i = 0; i < MAX_REJECTED_OPS_BEFORE_WARNING; i++) {
for (let i = 0; i < opCount; i++) {
const op = createOp({ id: `op-${i}` });
opLogStoreSpy.getOpById
.withArgs(`op-${i}`)

View file

@ -4,7 +4,6 @@ import { Operation, VectorClock } from '../core/operation.types';
import { OpLog } from '../../core/log';
import { SnackService } from '../../core/snack/snack.service';
import { T } from '../../t.const';
import { MAX_REJECTED_OPS_BEFORE_WARNING } from '../core/operation-log.const';
import { StaleOperationResolverService } from './stale-operation-resolver.service';
import { DownloadCallback, RejectedOpInfo } from '../core/types/sync-results.types';
import { handleStorageQuotaError } from './sync-error-utils';
@ -166,8 +165,8 @@ export class RejectedOpsHandlerService {
`RejectedOpsHandlerService: Marked ${permanentlyRejectedOps.length} server-rejected ops as rejected`,
);
// Notify user if significant number of ops were rejected without conflict resolution
if (permanentlyRejectedOps.length >= MAX_REJECTED_OPS_BEFORE_WARNING) {
// Notify user when any ops are permanently rejected
if (permanentlyRejectedOps.length > 0) {
this.snackService.open({
type: 'ERROR',
msg: T.F.SYNC.S.UPLOAD_OPS_REJECTED,

View file

@ -104,7 +104,9 @@ const _validityError = (errTxt: string, additionalInfo?: any): void => {
if (environment.production) {
try {
PFLog.log('Validity Error Info string: ', JSON.stringify(additionalInfo));
} catch (e) {}
} catch (e) {
PFLog.warn('Failed to stringify validity error info:', e);
}
}
}
if (errorCount <= 3) {

View file

@ -1145,6 +1145,7 @@ const T = {
CLOCK_DRIFT_WARNING: 'F.SYNC.S.CLOCK_DRIFT_WARNING',
COMPACTION_FAILED: 'F.SYNC.S.COMPACTION_FAILED',
CONFLICT_RESOLUTION_FAILED: 'F.SYNC.S.CONFLICT_RESOLUTION_FAILED',
DIALOG_RESULT_ERROR: 'F.SYNC.S.DIALOG_RESULT_ERROR',
CONFLICT_DIALOG_TIMEOUT: 'F.SYNC.S.CONFLICT_DIALOG_TIMEOUT',
OPERATION_PERMANENTLY_FAILED: 'F.SYNC.S.OPERATION_PERMANENTLY_FAILED',
DATA_REPAIRED: 'F.SYNC.S.DATA_REPAIRED',

View file

@ -1107,6 +1107,7 @@
"CLOCK_DRIFT_WARNING": "Your device clock appears to be off by {{minutes}} minutes. This may cause sync issues.",
"COMPACTION_FAILED": "Database cleanup failed. App may slow down.",
"CONFLICT_RESOLUTION_FAILED": "Sync conflict resolution failed. Please reload.",
"DIALOG_RESULT_ERROR": "An error occurred while processing the sync dialog. Please try again.",
"CONFLICT_DIALOG_TIMEOUT": "Sync conflict dialog timed out and was cancelled. Sync will retry on next trigger.",
"OPERATION_PERMANENTLY_FAILED": "Some sync operations failed and could not be applied. Data may be incomplete.",
"DATA_REPAIRED": "Data automatically repaired ({{count}} issues fixed)",