Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/governance-app-frontend/src/app/RootErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Component, type ErrorInfo, type ReactNode } from 'react';

import { AnalyticsEvent } from '@features/analytics/events';
import { analytics } from '@features/analytics/service';

import { Button } from '@components/button';
import { firstComponentFromStack } from '@utils/error';

import i18n from '@/i18n/config';

type Props = {
children: ReactNode;
};

type State = {
hasError: boolean;
};

export class RootErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };

static getDerivedStateFromError(): State {
return { hasError: true };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
const component = firstComponentFromStack(errorInfo.componentStack ?? '');

analytics.event(AnalyticsEvent.FrontendError, {
error_type: error.name,
component,
});
}

render(): ReactNode {
if (this.state.hasError) {
return (
<div className="flex min-h-dvh w-full flex-col bg-background text-foreground">
<div className="px-4 py-10 sm:p-12">
<img
src="/governance-logo.svg"
alt={i18n.t(($) => $.common.alt.icpLogo)}
className="h-6 w-fit dark:invert"
/>
</div>
<div className="flex flex-1 items-center justify-center px-4">
<div className="flex max-w-md flex-col items-center text-center">
<div className="space-y-3">
<h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">
{i18n.t(($) => $.errors.errorBoundary.title)}
</h2>
<p className="text-muted-foreground">
{i18n.t(($) => $.errors.errorBoundary.description)}
</p>
</div>
<div className="mt-8">
<Button size="lg" onClick={() => (window.location.href = '/dashboard')}>
{i18n.t(($) => $.errors.errorBoundary.tryAgain)}
</Button>
</div>
</div>
</div>
</div>
);
}

return this.props.children;
}
}
23 changes: 23 additions & 0 deletions src/governance-app-frontend/src/common/utils/error.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest';

import { firstComponentFromStack } from '@utils/error';

describe('firstComponentFromStack', () => {
it('should return the first component name from a stack', () => {
const stack = '\n at Button\n at Modal\n at App';
expect(firstComponentFromStack(stack)).toBe('Button');
});

it('should handle extra whitespace around component names', () => {
const stack = '\n at MyComponent\n at Parent';
expect(firstComponentFromStack(stack)).toBe('MyComponent');
});

it('should return Unknown for an empty string', () => {
expect(firstComponentFromStack('')).toBe('Unknown');
});

it('should return Unknown when no component is found', () => {
expect(firstComponentFromStack('no match here')).toBe('Unknown');
});
});
3 changes: 3 additions & 0 deletions src/governance-app-frontend/src/common/utils/error.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export const errorMessage = (source: string, message: string): Error => {
return new Error(`❌ ERROR (${source}): ${message}.`);
};

export const firstComponentFromStack = (componentStack: string): string =>
/\s+at\s+(\w+)/.exec(componentStack)?.[1] ?? 'Unknown';
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ export enum AnalyticsEvent {
FollowingPickerApplyError = 'following_picker_apply_error',
FollowingRemoveFollowee = 'following_remove_followee',
FollowingClearAll = 'following_clear_all',

// Error tracking
FrontendError = 'frontend_error',
}
// @TODO:
// - successful login
// - click on the navigation items
// - click on dashboard buttons (deposit / withdraw / staking - apy warning icons)
// - view proposal link
Expand Down
5 changes: 5 additions & 0 deletions src/governance-app-frontend/src/i18n/en/errors.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"errorBoundary": {
"title": "Something went wrong",
"description": "Please refresh the page. If the problem persists, try clearing your browser cache.",
"tryAgain": "Try again"
},
"nnsGovernanceErrors": {
"CouldNotClaimNeuronError": "Could not find the neuron to claim.",
"InsufficientAmountError": "The amount is not enough.",
Expand Down
7 changes: 6 additions & 1 deletion src/governance-app-frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import ReactDOM from 'react-dom/client';

import { App } from './app/App';
import { RootErrorBoundary } from './app/RootErrorBoundary';

const rootElement = document.getElementById('root') as HTMLElement;
ReactDOM.createRoot(rootElement).render(<App />);
ReactDOM.createRoot(rootElement).render(
<RootErrorBoundary>
<App />
</RootErrorBoundary>,
);
Loading