Skip to content

Commit ae96cc7

Browse files
committed
chore(diagnostics): log a proving line for every v1.2.7 change testers confirm from the shared log, and start the release diagnostics ledger
1 parent 58b0053 commit ae96cc7

10 files changed

Lines changed: 215 additions & 4 deletions

File tree

apps/desktop/src-tauri/src/sync.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1611,7 +1611,10 @@ fn wait_for_dropbox_auth_code(
16111611
// A callback carrying another attempt's state (a reloaded
16121612
// tab from an earlier connect, a stale prefetch) is not this
16131613
// flow's answer; reject it and keep waiting for the real one.
1614-
log::warn!("Ignoring Dropbox OAuth callback with a mismatched state");
1614+
log::warn!(
1615+
target: "sync",
1616+
"Ignoring Dropbox OAuth callback with a mismatched state releaseCheck=v1.2.7/dropbox-signin-detached phase=callback-state-mismatch"
1617+
);
16151618
let _ = write_oauth_http_response(
16161619
&mut stream,
16171620
"400 Bad Request",
@@ -1630,6 +1633,10 @@ fn wait_for_dropbox_auth_code(
16301633
return Err("Dropbox authorization failed: missing code".to_string());
16311634
}
16321635

1636+
log::info!(
1637+
target: "sync",
1638+
"Dropbox sign-in callback accepted with a matching state releaseCheck=v1.2.7/dropbox-signin-detached phase=callback-state-matched"
1639+
);
16331640
let _ = write_oauth_http_response(
16341641
&mut stream,
16351642
"200 OK",
@@ -2758,6 +2765,10 @@ fn run_dropbox_oauth(
27582765
// sign-in page spun on the redirect, and the Connect button stayed dead.
27592766
open::that_detached(authorize_url.as_str())
27602767
.map_err(|error| format!("Failed to open Dropbox authorization URL: {error}"))?;
2768+
log::info!(
2769+
target: "sync",
2770+
"Dropbox sign-in page opened detached; waiting for the callback releaseCheck=v1.2.7/dropbox-signin-detached phase=opened"
2771+
);
27612772

27622773
let code = wait_for_dropbox_auth_code(&listener, &state)?;
27632774
exchange_dropbox_auth_code(

apps/desktop/src/lib/sync-service.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,16 @@ const hasAttachmentSyncWork = async (data: AppData, presenceScope: string | null
845845

846846
// Nothing in the document says there is work to do. The one remaining reason to run the
847847
// phase is the periodic presence proof.
848-
return isAttachmentPresenceReconciliationDue(presenceScope);
848+
const presenceDue = isAttachmentPresenceReconciliationDue(presenceScope);
849+
void logInfo('Attachment presence re-verification checked', {
850+
scope: 'sync',
851+
extra: {
852+
releaseCheck: 'v1.2.7/daily-attachment-presence',
853+
presenceDue: String(presenceDue),
854+
hasScope: String(Boolean(presenceScope)),
855+
},
856+
});
857+
return presenceDue;
849858
};
850859

851860
const getSyncConfigDeps = () => ({

apps/mobile/components/settings/use-sync-settings-transport-actions.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ vi.mock('@mindwtr/core', async () => ({
129129
SYNC_LOCAL_INSECURE_URL_OPTIONS: { allowLocalHostnames: true, allowPrivateIpRanges: true },
130130
}));
131131

132+
vi.mock('@/lib/app-log', () => ({
133+
logInfo: vi.fn(),
134+
logWarn: vi.fn(),
135+
logError: vi.fn(),
136+
}));
137+
132138
vi.mock('@/lib/storage-file', () => ({
133139
pickAndParseSyncFolder: vi.fn(),
134140
}));

apps/mobile/components/settings/use-sync-settings-transport-actions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
type AppSettings,
1818
} from '@mindwtr/core';
1919

20+
import { logInfo } from '@/lib/app-log';
2021
import { pickAndParseSyncFolder } from '@/lib/storage-file';
2122
import { getCloudKitAccountStatus } from '@/lib/cloudkit-sync';
2223
import { authorizeDropbox, getDropboxRedirectUri } from '@/lib/dropbox-oauth';
@@ -830,6 +831,18 @@ export function useSyncSettingsTransportActions({
830831
};
831832

832833
if (effectiveBackend === 'off') return;
834+
// Only an activation passes an explicit backend; a manual "Sync now" tap
835+
// calls handleSync() with no options and never reaches this line.
836+
if (options?.backend) {
837+
void logInfo('Sync backend selected; running the verification sync to activate it', {
838+
scope: 'sync',
839+
extra: {
840+
releaseCheck: 'v1.2.7/sync-settings-activation-mobile',
841+
backend: effectiveBackend,
842+
cloudProvider: effectiveCloudProvider,
843+
},
844+
});
845+
}
833846
if (effectiveBackend === 'webdav') {
834847
const trimmedWebDavUrl = effectiveWebdav.url.trim();
835848
if (!trimmedWebDavUrl) {

apps/mobile/hooks/use-incoming-url.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useEffect, useState } from 'react';
22
import * as Linking from 'expo-linking';
33

4+
import { logInfo } from '@/lib/app-log';
5+
46
export type IncomingUrl = {
57
url: string | null;
68
// Increments on every delivery, so the same link opened twice in a row
@@ -13,16 +15,42 @@ export type IncomingUrl = {
1315
const INITIAL: IncomingUrl = { url: null, key: 0 };
1416
const DUPLICATE_DELIVERY_WINDOW_MS = 1_000;
1517

18+
// Scheme and host only: a capture link carries the task title in its path/query.
19+
const logIncomingDelivery = (url: string, delivery: number, deduped: boolean): void => {
20+
let scheme = 'none';
21+
let host = 'none';
22+
try {
23+
const parsed = Linking.parse(url);
24+
scheme = parsed.scheme ?? 'none';
25+
host = parsed.hostname ?? 'none';
26+
} catch {
27+
// A link the parser rejects is still worth counting.
28+
}
29+
void logInfo('Incoming link delivered', {
30+
scope: 'link',
31+
extra: {
32+
releaseCheck: 'v1.2.7/incoming-link-redelivery',
33+
scheme,
34+
host,
35+
delivery: String(delivery),
36+
deduped: String(deduped),
37+
},
38+
});
39+
};
40+
1641
export function useIncomingUrl(): IncomingUrl {
1742
const [incoming, setIncoming] = useState<IncomingUrl>(INITIAL);
1843

1944
useEffect(() => {
2045
let cancelled = false;
2146
let lastDelivery: { url: string | null; at: number } = { url: null, at: 0 };
47+
let deliveries = 0;
2248
Linking.getInitialURL()
2349
.then((url) => {
2450
if (cancelled || !url) return;
2551
lastDelivery = { url, at: Date.now() };
52+
deliveries += 1;
53+
logIncomingDelivery(url, deliveries, false);
2654
setIncoming((previous) => (previous.key === 0 ? { url, key: 1 } : previous));
2755
})
2856
.catch(() => undefined);
@@ -31,8 +59,13 @@ export function useIncomingUrl(): IncomingUrl {
3159
// iOS can deliver the launch URL through the event as well as
3260
// getInitialURL(); a repeat of the same link within a second is
3361
// that echo, not a second press.
34-
if (event.url === lastDelivery.url && now - lastDelivery.at < DUPLICATE_DELIVERY_WINDOW_MS) return;
62+
if (event.url === lastDelivery.url && now - lastDelivery.at < DUPLICATE_DELIVERY_WINDOW_MS) {
63+
logIncomingDelivery(event.url, deliveries, true);
64+
return;
65+
}
3566
lastDelivery = { url: event.url, at: now };
67+
deliveries += 1;
68+
logIncomingDelivery(event.url, deliveries, false);
3669
setIncoming((previous) => ({ url: event.url, key: previous.key + 1 }));
3770
});
3871
return () => {

apps/mobile/lib/external-calendar.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type ExternalCalendarSubscription,
1414
} from '@mindwtr/core';
1515
import * as FileSystem from './file-system';
16+
import { logInfo } from './app-log';
1617

1718
export const EXTERNAL_CALENDARS_KEY = 'mindwtr-external-calendars';
1819
export const SYSTEM_CALENDAR_SETTINGS_KEY = 'mindwtr-system-calendar-settings';
@@ -498,6 +499,31 @@ async function fetchSystemCalendarEvents(rangeStart: Date, rangeEnd: Date, signa
498499
});
499500
}
500501

502+
// #1133/#1134 proof: `spanning` counts the events that cross a window edge — the ones
503+
// Android's containment query used to drop before the app ever saw them.
504+
const dayMs = 24 * 60 * 60 * 1000;
505+
let multiDay = 0;
506+
let allDay = 0;
507+
let spanning = 0;
508+
for (const event of events) {
509+
const start = new Date(event.start).getTime();
510+
const end = new Date(event.end).getTime();
511+
if (end - start > dayMs) multiDay += 1;
512+
if (event.allDay) allDay += 1;
513+
if (start < rangeStart.getTime() || end > rangeEnd.getTime()) spanning += 1;
514+
}
515+
void logInfo('Device calendar events loaded for the window', {
516+
scope: 'calendar',
517+
extra: {
518+
releaseCheck: 'v1.2.7/calendar-spanning-events',
519+
platform: Platform.OS,
520+
total: String(events.length),
521+
multiDay: String(multiDay),
522+
allDay: String(allDay),
523+
spanning: String(spanning),
524+
},
525+
});
526+
501527
return { calendars, events };
502528
}
503529

apps/mobile/lib/sync-service.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import AsyncStorage from '@react-native-async-storage/async-storage';
22
import { Platform } from 'react-native';
33
import Constants from 'expo-constants';
4-
import { AppData, SYNC_ENCRYPTION_LOG_EVENTS, buildSyncEncryptionActivationExtra, buildSyncEncryptionErrorExtra, buildSyncEncryptionRemoteReadExtra, buildSyncEncryptionStateExtra, type SyncEncryptionState, type SyncEncryptionStateDecision, acquireSyncRemoteMutationFence, clearIdleSyncCycleSnapshot, createDropboxSyncRemoteMutationFencePort, createSyncOrchestrator, createWebdavSyncRemoteMutationFencePort, probeWebdavSyncCompatibility, runSerializedSyncDocumentOperation, runSharedSyncCycle, useTaskStore, webdavGetSyncDocument, webdavHeadFile, webdavPutSyncDocument, syncEncryptedArtifactName, markRemoteEncryptionDiscovered, markRemotePlaintextDiscovered, SyncEncryptionRemoteConflictError, SyncEncryptionRemotePlaintextError, SyncEncryptionRemoteVersionUnavailableError, SyncEncryptionTerminalError, SyncEncryptionTransitionIncompleteError, SyncFileLockUnavailableError, SyncRemoteWriteConflict, type SyncKeyMaterial, cloudGetJson, cloudHeadJson, cloudPutJson, flushPendingSave, performSyncCycle, withRetry, isRetryableError, isRetryableWebdavReadError, isWebdavInvalidJsonError, normalizeStrongWebdavEtag, normalizeWebdavUrl, normalizeCloudUrl, createSyncBackendIO, buildFastSyncScope, hasPendingSyncSideEffects, injectExternalCalendars as injectExternalCalendarsForSync, persistExternalCalendars as persistExternalCalendarsForSync, getInMemoryAppDataSnapshot, createAbortableFetch, normalizeCloudProvider as normalizeCoreCloudProvider, isDropboxUnauthorizedError, parseFastSyncState, serializeFastSyncState, summarizeTaskLifecycleCounts, decodeUriSafe, buildSyncPayloadTraceExtra, isSyncPayloadTraceEnabled, SYNC_TRACE_EVENT_MESSAGES, SYNC_FILE_NAME, SYNC_REMOTE_MUTATION_REQUEST_HORIZON_MS, CLOUD_PROVIDER_DROPBOX, CLOUD_PROVIDER_SELF_HOSTED, type Attachment, type CloudProvider, type FastSyncState, type SyncBackendContext, type SyncBackendIO, type SyncRunDiagnosticEvent, type SyncRunNotifier, type SyncRunPlatformHooks, type SyncRunResult, type SyncRunStorage, type SyncTransport } from '@mindwtr/core';
4+
import { AppData, SYNC_ENCRYPTION_LOG_EVENTS, buildSyncEncryptionActivationExtra, buildSyncEncryptionErrorExtra, buildSyncEncryptionRemoteReadExtra, buildSyncEncryptionStateExtra, type SyncEncryptionState, type SyncEncryptionStateDecision, acquireSyncRemoteMutationFence, clearIdleSyncCycleSnapshot, createDropboxSyncRemoteMutationFencePort, createSyncOrchestrator, createWebdavSyncRemoteMutationFencePort, webdavMutationFenceUrl, probeWebdavSyncCompatibility, runSerializedSyncDocumentOperation, runSharedSyncCycle, useTaskStore, webdavGetSyncDocument, webdavHeadFile, webdavPutSyncDocument, syncEncryptedArtifactName, markRemoteEncryptionDiscovered, markRemotePlaintextDiscovered, SyncEncryptionRemoteConflictError, SyncEncryptionRemotePlaintextError, SyncEncryptionRemoteVersionUnavailableError, SyncEncryptionTerminalError, SyncEncryptionTransitionIncompleteError, SyncFileLockUnavailableError, SyncRemoteWriteConflict, type SyncKeyMaterial, cloudGetJson, cloudHeadJson, cloudPutJson, flushPendingSave, performSyncCycle, withRetry, isRetryableError, isRetryableWebdavReadError, isWebdavInvalidJsonError, normalizeStrongWebdavEtag, normalizeWebdavUrl, normalizeCloudUrl, createSyncBackendIO, buildFastSyncScope, hasPendingSyncSideEffects, injectExternalCalendars as injectExternalCalendarsForSync, persistExternalCalendars as persistExternalCalendarsForSync, getInMemoryAppDataSnapshot, createAbortableFetch, normalizeCloudProvider as normalizeCoreCloudProvider, isDropboxUnauthorizedError, parseFastSyncState, serializeFastSyncState, summarizeTaskLifecycleCounts, decodeUriSafe, buildSyncPayloadTraceExtra, isSyncPayloadTraceEnabled, SYNC_TRACE_EVENT_MESSAGES, SYNC_FILE_NAME, SYNC_REMOTE_MUTATION_REQUEST_HORIZON_MS, CLOUD_PROVIDER_DROPBOX, CLOUD_PROVIDER_SELF_HOSTED, type Attachment, type CloudProvider, type FastSyncState, type SyncBackendContext, type SyncBackendIO, type SyncRunDiagnosticEvent, type SyncRunNotifier, type SyncRunPlatformHooks, type SyncRunResult, type SyncRunStorage, type SyncTransport } from '@mindwtr/core';
55
import { mobileStorage } from './storage-adapter';
66
import { logInfo, logSyncError, logWarn, sanitizeLogMessage } from './app-log';
77
import { readSyncFileVersioned, resolveSyncFileUri, writeSyncFile } from './storage-file';
@@ -1528,6 +1528,16 @@ class MobileSyncRun {
15281528
lastSyncHistory: mergedData.settings.lastSyncHistory,
15291529
});
15301530
}
1531+
void logInfo('Sync status published to the store', {
1532+
scope: 'sync',
1533+
extra: {
1534+
releaseCheck: 'v1.2.7/sync-status-published',
1535+
backend: this.backend,
1536+
statusPublished: info.localWriteSkipped ? 'unchanged' : 'wrote-local',
1537+
lastSyncAt: String(mergedData.settings.lastSyncAt ?? 'none'),
1538+
lastSyncStatus: String(mergedData.settings.lastSyncStatus ?? 'none'),
1539+
},
1540+
});
15311541
logSyncDiagnostic('Sync diagnostic complete', this.syncDiagnosticStartedAt, {
15321542
backend: this.backend,
15331543
step: this.lastStep,
@@ -1573,6 +1583,15 @@ class MobileSyncRun {
15731583
const webdavConfig = this.webdavConfig;
15741584
if (!webdavConfig?.url) throw new Error('WebDAV URL not configured');
15751585
this.ensureWebdavSyncNotRateLimited();
1586+
// #1132 proof: React Native's URL class ignored pathname writes and resolved the
1587+
// fence to the sync document itself. The basename below must never be data.json.
1588+
void logInfo('WebDAV sync fence artifact resolved', {
1589+
scope: 'sync',
1590+
extra: {
1591+
releaseCheck: 'v1.2.7/fence-artifact',
1592+
artifact: webdavMutationFenceUrl(webdavConfig.url).split('/').pop() ?? 'none',
1593+
},
1594+
});
15761595
try {
15771596
return await acquireSyncRemoteMutationFence(
15781597
createWebdavSyncRemoteMutationFencePort(webdavConfig.url, {

0 commit comments

Comments
 (0)