generated from nhcarrigan/template
chore: audit frontend error reporting to exclude expected behaviours (#79)
## 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:
@@ -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 ]);
|
||||
|
||||
Reference in New Issue
Block a user