From 161127dc21a716a27a110519015710c043456def Mon Sep 17 00:00:00 2001 From: Hikari Date: Thu, 19 Mar 2026 16:01:22 -0700 Subject: [PATCH] chore: audit frontend error reporting to exclude expected behaviours (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Audits all `logError` call sites in `gameContext.tsx` and suppresses telemetry for expected business logic rejections, eliminating alert fatigue without hiding real errors. ### Changes per call site | Context | Before | After | |---|---|---| | `auto_save` | Logged all non-signature errors | Network failures silently swallowed — next tick retries | | `auto_prestige` | Logged eligibility failures | Silently ignored — eligibility re-checked every tick | | `auto_boss` | Logged all errors | Filters out `"Boss is not currently available"` (race condition); other errors still logged | | `challenge_boss` | Logged all errors | Filters out `"Boss is not currently available"` (race condition); other errors still logged | | `start_exploration` | Logged then rethrew | Removed useless try/catch — error propagates to UI naturally | | `collect_exploration` | Logged then rethrew | Removed useless try/catch — error propagates to UI naturally | Genuine errors (`buy_prestige_upgrade`, `transcend`, `apotheosis`, `buy_echo_upgrade`, `craft_recipe`) are unchanged — they still fire telemetry. Closes #73 ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: https://git.nhcarrigan.com/nhcarrigan/elysium/pulls/79 Co-authored-by: Hikari Co-committed-by: Hikari --- apps/web/src/context/gameContext.tsx | 214 ++++++++++++++------------- 1 file changed, 111 insertions(+), 103 deletions(-) diff --git a/apps/web/src/context/gameContext.tsx b/apps/web/src/context/gameContext.tsx index e0dfebb..e3275b7 100644 --- a/apps/web/src/context/gameContext.tsx +++ b/apps/web/src/context/gameContext.tsx @@ -1224,9 +1224,12 @@ export const GameProvider = ({ ) { signatureReference.current = null; localStorage.removeItem("elysium_save_signature"); - } else { - logError("auto_save", error_); } + + /* + * Network failures during background auto-save are expected on + * flaky connections — the next tick will retry, so no telemetry needed + */ }); } } @@ -1254,10 +1257,9 @@ export const GameProvider = ({ } await reloadReference.current(); }). - catch((error_: unknown) => { - logError("auto_prestige", error_); + catch(() => { - /* Silently ignore — will retry next tick */ + /* Silently ignore — eligibility is re-checked every tick */ }). finally(() => { isAutoPrestigingReference.current = false; @@ -1307,11 +1309,18 @@ export const GameProvider = ({ }); }). catch((error_: unknown) => { - logError("auto_boss", error_); const message = error_ instanceof Error ? error_.message : String(error_); + + /* + * "Boss is not currently available" is an expected race condition + * in the tick loop — suppress telemetry for this case only + */ + if (message !== "Boss is not currently available") { + logError("auto_boss", error_); + } setAutoBossError(message); setState((previous) => { if (previous === null) { @@ -1709,114 +1718,104 @@ export const GameProvider = ({ }, []); const startExploration = useCallback(async(areaId: string) => { - try { - const response = await startExplorationApi({ areaId }); + const response = await startExplorationApi({ areaId }); + setState((previous) => { + if (previous?.exploration === undefined) { + return previous; + } + return { + ...previous, + exploration: { + ...previous.exploration, + areas: previous.exploration.areas.map((a) => { + return a.id === areaId + ? { + ...a, + endsAt: response.endsAt, + status: "in_progress" as const, + } + : a; + }), + }, + }; + }); + }, []); + + const collectExploration = useCallback( + async(areaId: string): Promise => { + const result = await collectExplorationApi({ areaId }); setState((previous) => { if (previous?.exploration === undefined) { return previous; } + let materials = [ ...previous.exploration.materials ]; + + // Apply material drops from the random loot roll + for (const drop of result.materialsFound) { + const existing = materials.find((mat) => { + return mat.materialId === drop.materialId; + }); + if (existing === undefined) { + materials = [ + ...materials, + { materialId: drop.materialId, quantity: drop.quantity }, + ]; + } else { + materials = materials.map((mat) => { + return mat.materialId === drop.materialId + ? { ...mat, quantity: mat.quantity + drop.quantity } + : mat; + }); + } + } + + // Apply material from event (if any) + const materialGained = result.event?.materialGained; + if (materialGained !== null && materialGained !== undefined) { + const { materialId, quantity } = materialGained; + const existing = materials.find((mat) => { + return mat.materialId === materialId; + }); + if (existing === undefined) { + materials = [ ...materials, { materialId, quantity } ]; + } else { + materials = materials.map((mat) => { + return mat.materialId === materialId + ? { ...mat, quantity: mat.quantity + quantity } + : mat; + }); + } + } + return { ...previous, exploration: { ...previous.exploration, areas: previous.exploration.areas.map((a) => { return a.id === areaId - ? { - ...a, - endsAt: response.endsAt, - status: "in_progress" as const, - } + ? { ...a, completedOnce: true, status: "available" as const } : a; }), + materials: materials, + }, + player: { + ...previous.player, + totalGoldEarned: + previous.player.totalGoldEarned + + Math.max(0, result.event?.goldChange ?? 0), + }, + resources: { + ...previous.resources, + essence: + previous.resources.essence + (result.event?.essenceChange ?? 0), + gold: Math.max( + 0, + previous.resources.gold + (result.event?.goldChange ?? 0), + ), }, }; }); - } catch (error_: unknown) { - logError("start_exploration", error_); - throw error_; - } - }, []); - - const collectExploration = useCallback( - async(areaId: string): Promise => { - try { - const result = await collectExplorationApi({ areaId }); - setState((previous) => { - if (previous?.exploration === undefined) { - return previous; - } - let materials = [ ...previous.exploration.materials ]; - - // Apply material drops from the random loot roll - for (const drop of result.materialsFound) { - const existing = materials.find((mat) => { - return mat.materialId === drop.materialId; - }); - if (existing === undefined) { - materials = [ - ...materials, - { materialId: drop.materialId, quantity: drop.quantity }, - ]; - } else { - materials = materials.map((mat) => { - return mat.materialId === drop.materialId - ? { ...mat, quantity: mat.quantity + drop.quantity } - : mat; - }); - } - } - - // Apply material from event (if any) - const materialGained = result.event?.materialGained; - if (materialGained !== null && materialGained !== undefined) { - const { materialId, quantity } = materialGained; - const existing = materials.find((mat) => { - return mat.materialId === materialId; - }); - if (existing === undefined) { - materials = [ ...materials, { materialId, quantity } ]; - } else { - materials = materials.map((mat) => { - return mat.materialId === materialId - ? { ...mat, quantity: mat.quantity + quantity } - : mat; - }); - } - } - - return { - ...previous, - exploration: { - ...previous.exploration, - areas: previous.exploration.areas.map((a) => { - return a.id === areaId - ? { ...a, completedOnce: true, status: "available" as const } - : a; - }), - materials: materials, - }, - player: { - ...previous.player, - totalGoldEarned: - previous.player.totalGoldEarned - + Math.max(0, result.event?.goldChange ?? 0), - }, - resources: { - ...previous.resources, - essence: - previous.resources.essence + (result.event?.essenceChange ?? 0), - gold: Math.max( - 0, - previous.resources.gold + (result.event?.goldChange ?? 0), - ), - }, - }; - }); - return result; - } catch (error_: unknown) { - logError("collect_exploration", error_); - throw error_; - } + return result; }, [], ); @@ -1960,11 +1959,20 @@ export const GameProvider = ({ }); setBattleResult({ bossName: boss.name, result: result }); } catch (error_: unknown) { - logError("challenge_boss", error_); - setBossError( - error_ instanceof Error + const bossErrorMessage + = error_ instanceof Error ? error_.message - : "Failed to challenge boss", + : "Failed to challenge boss"; + + /* + * "Boss is not currently available" is an expected server rejection + * (race condition between UI state and server state) — suppress telemetry + */ + if (bossErrorMessage !== "Boss is not currently available") { + logError("challenge_boss", error_); + } + setBossError( + bossErrorMessage, ); } }, [ forceSync ]);