Skip to content

Commit f98b9b1

Browse files
committed
refactor(error-handling): replace inline error boundaries with RenderErrorFallback
1 parent fbf91d4 commit f98b9b1

File tree

3 files changed

+148
-75
lines changed

3 files changed

+148
-75
lines changed

src/App.svelte

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,21 @@ import { GAME_REPO_URL, UI_GUIDE_NAME } from "./constants";
1919
import { t } from "@transifex/native";
2020
import { buildMetaDescription } from "./seo";
2121
import {
22-
NIGHTLY_VERSION,
23-
STABLE_VERSION,
2422
type BuildInfo,
25-
type InitialAppState,
2623
buildUrl,
2724
changeVersion,
2825
getCurrentVersionSlug,
29-
getVersionedBasePath,
3026
getUrlConfig,
27+
getVersionedBasePath,
3128
handleInternalNavigation,
29+
type InitialAppState,
3230
initializeRouting,
3331
isSupportedType,
3432
isSupportedVersion,
3533
navigateTo,
34+
NIGHTLY_VERSION,
3635
page,
36+
STABLE_VERSION,
3737
updateQueryParam,
3838
updateQueryParamNoReload,
3939
updateSearchRoute,
@@ -48,9 +48,10 @@ import CategoryGrid from "./CategoryGrid.svelte";
4848
import Loading from "./Loading.svelte";
4949
import Spinner from "./Spinner.svelte";
5050
import { fade } from "svelte/transition";
51-
import { isTesting, isNext, RUNNING_MODE } from "./utils/env";
51+
import { isNext, isTesting, RUNNING_MODE } from "./utils/env";
5252
import MigoWarning from "./MigoWarning.svelte";
5353
import Notification, { notify } from "./Notification.svelte";
54+
import RenderErrorFallback from "./RenderErrorFallback.svelte";
5455
5556
import { gameSingularName } from "./i18n/gettext";
5657
@@ -492,6 +493,30 @@ function maybeFocusSearch(e: KeyboardEvent) {
492493
}
493494
}
494495
496+
function onItemBoundaryError(boundaryError: unknown): void {
497+
const error =
498+
boundaryError instanceof Error
499+
? boundaryError
500+
: new Error(String(boundaryError));
501+
const routeItem = $page.route.item;
502+
metrics.count("app.error.catch", 1, {
503+
type: routeItem?.type ?? "shell",
504+
id: routeItem?.id ?? "none",
505+
});
506+
const context = {
507+
route: {
508+
version: $page.route.version,
509+
type: routeItem?.type,
510+
id: routeItem?.id,
511+
search: $page.route.search,
512+
},
513+
};
514+
console.error(error, context);
515+
Sentry.captureException(error, {
516+
contexts: { context },
517+
});
518+
}
519+
495520
/**
496521
* Returns the native name (endonym) of a language (e.g., "Deutsch" for "de").
497522
* Uses Intl.DisplayNames to auto-generate names without hardcoded lists.
@@ -603,7 +628,9 @@ let canonicalUrl = $derived(
603628
type="button"
604629
class="search-control-btn search-clear-button"
605630
tabindex="-1"
606-
aria-label={t("Clear search", { _context: SEARCH_UI_CONTEXT })}
631+
aria-label={t("Clear search", {
632+
_context: SEARCH_UI_CONTEXT,
633+
})}
607634
onclick={() => {
608635
search = "";
609636
handleSearchInput();
@@ -678,11 +705,16 @@ let canonicalUrl = $derived(
678705
{#if item}
679706
{#if $data}
680707
{#key item}
681-
{#if item.id}
682-
<Thing {item} data={$data} />
683-
{:else}
684-
<Catalog type={item.type} data={$data} />
685-
{/if}
708+
<svelte:boundary onerror={onItemBoundaryError}>
709+
{#if item.id}
710+
<Thing {item} data={$data} />
711+
{:else}
712+
<Catalog type={item.type} data={$data} />
713+
{/if}
714+
{#snippet failed(e)}
715+
<RenderErrorFallback data={$data} error={e} {item} />
716+
{/snippet}
717+
</svelte:boundary>
686718
{/key}
687719
{:else}
688720
<Loading
@@ -766,7 +798,9 @@ let canonicalUrl = $derived(
766798
<div class="specs-footer">
767799
<div class="footer-item">
768800
<span class="spec-label"
769-
>{t("Maintainer", { _context: INTRO_DASHBOARD_CONTEXT })}:</span>
801+
>{t("Maintainer", {
802+
_context: INTRO_DASHBOARD_CONTEXT,
803+
})}:</span>
770804
<a href="https://github.com/ushkinaz" target="_blank">ushkinaz</a>
771805
</div>
772806
<div class="footer-item">
@@ -777,7 +811,9 @@ let canonicalUrl = $derived(
777811
</div>
778812
<div class="footer-item">
779813
<span class="spec-label"
780-
>{t("Feedback", { _context: INTRO_DASHBOARD_CONTEXT })}:</span>
814+
>{t("Feedback", {
815+
_context: INTRO_DASHBOARD_CONTEXT,
816+
})}:</span>
781817
<a href="https://discord.gg/XW7XhXuZ89" target="_blank">Discord</a>
782818
</div>
783819
</div>

src/RenderErrorFallback.svelte

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<script lang="ts">
2+
import { t } from "@transifex/native";
3+
import { untrack } from "svelte";
4+
5+
import type { CBNData } from "./data";
6+
import JsonView from "./JsonView.svelte";
7+
import { isSupportedType } from "./routing";
8+
import type { SupportedTypes } from "./types";
9+
import { isDev } from "./utils/env";
10+
11+
const ERROR_CONTEXT = "Render Error";
12+
13+
interface Props {
14+
data?: CBNData | null;
15+
error: unknown;
16+
item?: { id: string; type: string } | null;
17+
}
18+
19+
let {
20+
data: sourceData,
21+
error: sourceError,
22+
item: sourceItem,
23+
}: Props = $props();
24+
25+
const data = untrack(() => sourceData);
26+
const error = untrack(() => sourceError);
27+
const item = untrack(() => sourceItem);
28+
29+
function defaultFallbackJsonObject(
30+
type: string,
31+
id: string,
32+
): { __filename: string; id: string; type: string } | undefined {
33+
if (type === "json_flag") {
34+
return { id, type, __filename: "" };
35+
}
36+
return undefined;
37+
}
38+
39+
const jsonObject = untrack(() => {
40+
if (!item?.id || !data || !isSupportedType(item.type)) {
41+
return undefined;
42+
}
43+
44+
return (
45+
data.byIdMaybe(item.type as keyof SupportedTypes, item.id) ??
46+
defaultFallbackJsonObject(item.type, item.id)
47+
);
48+
});
49+
</script>
50+
51+
<section>
52+
<div class="render-error" role="alert">
53+
<div class="error-card">
54+
<h1>{t("Error", { _context: ERROR_CONTEXT })}</h1>
55+
<p>
56+
{t(
57+
"There was a problem displaying this page. Not all versions of Cataclysm are supported by the Guide currently. Try selecting a different build.",
58+
{ _context: ERROR_CONTEXT },
59+
)}
60+
</p>
61+
{#if isDev}
62+
<section>
63+
<h2>{t("Debug", { _context: ERROR_CONTEXT })}</h2>
64+
{#if error instanceof Error}
65+
<pre class="trace">{error.stack}</pre>
66+
{:else}
67+
<div>{String(error)}</div>
68+
{/if}
69+
</section>
70+
{/if}
71+
</div>
72+
73+
{#if jsonObject}
74+
<JsonView obj={jsonObject} buildNumber={data?.build_number} />
75+
{/if}
76+
</div>
77+
</section>
78+
79+
<style>
80+
.render-error {
81+
text-align: left;
82+
}
83+
84+
.error-card {
85+
border: 1px solid var(--cata-color-red);
86+
padding: 1rem;
87+
margin: 1rem 0;
88+
}
89+
90+
.trace {
91+
font-family: monospace;
92+
overflow-x: auto;
93+
}
94+
</style>

src/Thing.svelte

Lines changed: 5 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,13 @@ import ConstructionGroup from "./types/ConstructionGroup.svelte";
2626
import Achievement from "./types/Achievement.svelte";
2727
import ObsoletionWarning from "./ObsoletionWarning.svelte";
2828
import Bionic from "./types/Bionic.svelte";
29-
import * as Sentry from "@sentry/browser";
3029
import type { SupportedTypes } from "./types";
3130
import JsonView from "./JsonView.svelte";
3231
import OvermapSpecial from "./types/OvermapSpecial.svelte";
3332
import ItemAction from "./types/ItemAction.svelte";
3433
import Technique from "./types/Technique.svelte";
3534
import { metrics } from "./metrics";
3635
import { nowTimeStamp } from "./utils/perf";
37-
import { isDev } from "./utils/env";
3836
3937
interface Props {
4038
item: { id: string; type: string };
@@ -46,19 +44,6 @@ const item = untrack(() => sourceItem);
4644
const data = untrack(() => sourceData);
4745
setContext("data", data);
4846
49-
function onError(e: Error) {
50-
metrics.count("app.error.catch", 1, { type: item?.type, id: item?.id });
51-
console.error(e);
52-
Sentry.captureException(e, {
53-
contexts: {
54-
item: {
55-
type: item?.type,
56-
id: item?.id,
57-
},
58-
},
59-
});
60-
}
61-
6247
function defaultItem(id: string, type: string) {
6348
if (type === "json_flag") {
6449
return { id, type, __filename: "" };
@@ -137,53 +122,11 @@ const display = (obj && displays[obj.type]) ?? Unknown;
137122
_comment: "Error message when an object is not found in the data",
138123
})}
139124
{:else}
140-
<svelte:boundary
141-
onerror={(boundaryError) => {
142-
onError(
143-
boundaryError instanceof Error
144-
? boundaryError
145-
: new Error(String(boundaryError)),
146-
);
147-
}}>
148-
{#if /obsolet/.test(obj.__filename)}
149-
<ObsoletionWarning item={obj} />
150-
{/if}
151-
{@const SvelteComponent = display}
152-
<SvelteComponent item={obj} />
153-
154-
{#snippet failed(e)}
155-
<section>
156-
<div class="error">
157-
<h1>{t("Error")}</h1>
158-
<p>
159-
{t(
160-
"There was a problem displaying this page. Not all versions of Cataclysm are supported by the Guide currently. Try selecting a different build.",
161-
)}
162-
</p>
163-
{#if isDev}
164-
<section>
165-
<h2>{t("Debug")}</h2>
166-
<div>{e instanceof Error ? e.message : String(e)}</div>
167-
<pre class="trace">{e instanceof Error ? e.stack : ""}</pre>
168-
</section>
169-
{/if}
170-
</div>
171-
</section>
172-
{/snippet}
173-
</svelte:boundary>
125+
{#if /obsolet/.test(obj.__filename)}
126+
<ObsoletionWarning item={obj} />
127+
{/if}
128+
{@const SvelteComponent = display}
129+
<SvelteComponent item={obj} />
174130
{/if}
175131

176132
<JsonView {obj} buildNumber={data.build_number} />
177-
178-
<style>
179-
.error {
180-
border: 1px solid red;
181-
padding: 1rem;
182-
margin: 1rem 0;
183-
}
184-
185-
.trace {
186-
font-family: monospace;
187-
overflow-x: auto;
188-
}
189-
</style>

0 commit comments

Comments
 (0)