mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
feat(mana/web/sync): expose debug info, surface silent push failures
The SYNC_DEBUG.md runbook tries to inspect window.unifiedSync from DevTools to figure out why pending changes aren't flushing on mana.how. The script can't work because (a) the unified sync instance is never exposed on window and (b) the two most likely failure modes — push for an unknown appId, getToken() returning null — both `return` silently with no error, no telemetry, no state change. The pending count climbs and there's nothing in the console to point at the cause. This commit makes those failures visible: push() unknown appId When a pending change lands for an appId that isn't in the registered channels map (almost always a registry/migration drift like renaming an appId without migrating the existing pending rows) we now log a warning that names the offending appId, lists the known ones for comparison, and emits a push:error telemetry event with errorCategory='unknown-appid'. The pending rows for that appId would otherwise accumulate forever — same symptom as the SYNC_DEBUG report. push() no token getValidToken() can return null if the local exp check failed and the refresh-on-online retry didn't yield a new token. This was the silent path that was hardest to diagnose: the existing health-check telemetry only fires after a successful fetch, so there was no signal at all. We now log a warning, set channel.lastError = 'no-token', flip status to 'error' and emit push:error with errorCategory='no-token'. sync-telemetry.ts Widens the errorCategory union to include 'no-token' and 'unknown-appid' so the new emits type-check. getDebugInfo() New method on the createUnifiedSync return value. Returns a flat, JSON-serializable snapshot of every channel's state (status, online, clientId, serverUrl, channels[appId] with lastError + timer flags, plus knownAppIds at top level) so the SYNC_DEBUG runbook (Schritt C) can compare what the server is being asked to sync vs. what's actually sitting in _pendingChanges. (app)/+layout.svelte Exposes the live unified-sync instance on window.__unifiedSync in the browser. Not a security concern: every method on the returned object is also reachable via Dexie + a fresh fetch from the same DevTools console, and a malicious user can't escalate anything by poking at it. This is the global the SYNC_DEBUG Schritt C script needs to exist. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
967f938e84
commit
5110065ebe
3 changed files with 69 additions and 3 deletions
|
|
@ -35,7 +35,15 @@ export interface SyncTelemetryDetail {
|
||||||
/** number of changes touched (pushed / pulled / applied / dropped). */
|
/** number of changes touched (pushed / pulled / applied / dropped). */
|
||||||
count?: number;
|
count?: number;
|
||||||
/** Error category for `*:error`. Free-form short string, never raw stacks. */
|
/** Error category for `*:error`. Free-form short string, never raw stacks. */
|
||||||
errorCategory?: 'network' | 'auth' | 'http-5xx' | 'http-4xx' | 'parse' | 'unknown';
|
errorCategory?:
|
||||||
|
| 'network'
|
||||||
|
| 'auth'
|
||||||
|
| 'http-5xx'
|
||||||
|
| 'http-4xx'
|
||||||
|
| 'parse'
|
||||||
|
| 'no-token'
|
||||||
|
| 'unknown-appid'
|
||||||
|
| 'unknown';
|
||||||
/** HTTP status code if applicable. */
|
/** HTTP status code if applicable. */
|
||||||
status?: number;
|
status?: number;
|
||||||
/** Table name for apply-level events. */
|
/** Table name for apply-level events. */
|
||||||
|
|
|
||||||
|
|
@ -631,10 +631,36 @@ export function createUnifiedSync(serverUrl: string, getToken: () => Promise<str
|
||||||
|
|
||||||
async function push(appId: string): Promise<void> {
|
async function push(appId: string): Promise<void> {
|
||||||
const channel = channels.get(appId);
|
const channel = channels.get(appId);
|
||||||
if (!channel) return;
|
if (!channel) {
|
||||||
|
// Pending changes exist for an appId we never registered. Almost
|
||||||
|
// always means the module's appId in module-registry.ts drifted
|
||||||
|
// from the appId stamped on the pending row (e.g. someone renamed
|
||||||
|
// `bodylog` → `body` and old rows are still tagged `bodylog`).
|
||||||
|
// Surface it loudly: silent drops here is exactly the SYNC_DEBUG
|
||||||
|
// failure mode where pending counts only ever go up.
|
||||||
|
console.warn(
|
||||||
|
`[mana-sync] push: no channel registered for appId="${appId}". ` +
|
||||||
|
`Known appIds: ${[...channels.keys()].join(', ')}. ` +
|
||||||
|
`Pending changes for "${appId}" will accumulate forever until you fix the registry.`
|
||||||
|
);
|
||||||
|
emitSyncTelemetry({ kind: 'push:error', appId, errorCategory: 'unknown-appid' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
if (!token) return;
|
if (!token) {
|
||||||
|
// getValidToken returned null. Most likely the local exp check
|
||||||
|
// failed and the refresh-on-online retry didn't yield a new token.
|
||||||
|
// Without this surfacing, the user sees pending counts climb with
|
||||||
|
// no error anywhere — same SYNC_DEBUG symptom as above.
|
||||||
|
console.warn(
|
||||||
|
`[mana-sync] push[${appId}]: getToken() returned null — pending changes will not flush until auth recovers.`
|
||||||
|
);
|
||||||
|
channel.lastError = 'no-token';
|
||||||
|
setStatus('error');
|
||||||
|
emitSyncTelemetry({ kind: 'push:error', appId, errorCategory: 'no-token' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get pending changes for this appId
|
// Get pending changes for this appId
|
||||||
const pending: PendingChange[] = await db
|
const pending: PendingChange[] = await db
|
||||||
|
|
@ -1021,6 +1047,31 @@ export function createUnifiedSync(serverUrl: string, getToken: () => Promise<str
|
||||||
getChannel: (appId: string) => channels.get(appId),
|
getChannel: (appId: string) => channels.get(appId),
|
||||||
pushNow: push,
|
pushNow: push,
|
||||||
pullNow: pull,
|
pullNow: pull,
|
||||||
|
/**
|
||||||
|
* Snapshot of every registered channel's state — meant for the
|
||||||
|
* SYNC_DEBUG.md runbook (Schritt C). Returns a plain serializable
|
||||||
|
* object so it survives `JSON.stringify` in DevTools without
|
||||||
|
* Promise/Map weirdness.
|
||||||
|
*/
|
||||||
|
getDebugInfo: () => ({
|
||||||
|
status,
|
||||||
|
online,
|
||||||
|
clientId,
|
||||||
|
serverUrl,
|
||||||
|
channels: Object.fromEntries(
|
||||||
|
[...channels.entries()].map(([id, c]) => [
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
appId: c.appId,
|
||||||
|
tables: c.tables,
|
||||||
|
lastError: c.lastError,
|
||||||
|
hasPushTimer: c.pushTimer !== null,
|
||||||
|
hasPullTimer: c.pullTimer !== null,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
),
|
||||||
|
knownAppIds: [...channels.keys()],
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -372,6 +372,13 @@
|
||||||
trackReturnVisit();
|
trackReturnVisit();
|
||||||
const getToken = () => authStore.getValidToken();
|
const getToken = () => authStore.getValidToken();
|
||||||
unifiedSync = createUnifiedSync(SYNC_SERVER_URL, getToken);
|
unifiedSync = createUnifiedSync(SYNC_SERVER_URL, getToken);
|
||||||
|
// Expose on window for SYNC_DEBUG.md (Schritt C). Not a security
|
||||||
|
// concern: every method on the returned object is also reachable
|
||||||
|
// via Dexie + a fresh fetch from the same DevTools console, and
|
||||||
|
// the production user can't escalate anything by poking at it.
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
(window as unknown as { __unifiedSync: typeof unifiedSync }).__unifiedSync = unifiedSync;
|
||||||
|
}
|
||||||
const refreshPendingCount = async () => {
|
const refreshPendingCount = async () => {
|
||||||
try {
|
try {
|
||||||
const count = await db.table('_pendingChanges').count();
|
const count = await db.table('_pendingChanges').count();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue