chore: audit frontend error reporting to exclude expected behaviours (#79)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m2s
CI / Lint, Build & Test (push) Successful in 1m8s

## 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: #79
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #79.
This commit is contained in:
2026-03-19 16:01:22 -07:00
committed by Naomi Carrigan
parent a8a465f293
commit 161127dc21
+111 -103
View File
@@ -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<ExploreCollectResponse> => {
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<ExploreCollectResponse> => {
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 ]);