Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 4s
CI / E2E Tests (push) Has been skipped
CI / AI Services (Python) — Smoke (push) Failing after 6s
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 35s
Deploy / Build API Image (push) Failing after 15s
Deploy / Build Web Image (push) Failing after 13s
Deploy / Build AI Services Image (push) Failing after 11s
E2E Tests / Playwright E2E (push) Failing after 11s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 3s
Security Scanning / Trivy Scan — API Image (push) Failing after 1m33s
Security Scanning / Trivy Scan — Web Image (push) Failing after 54s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 45s
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Filesystem Scan (push) Failing after 46s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 1s
Deploy / Rollback Staging (push) Has been skipped
Implements GOO-63 audit requirement — React error boundaries with Vietnamese-language fallback UI, Sentry capture, and "Thử lại" retry. - ErrorBoundary: generic class component wrapping Sentry.captureException - PageErrorBoundary: full-page fallback for route layouts - ComponentErrorBoundary: inline widget fallback (compact + standard modes) - Applied to ListingMap, CheckoutModal, SearchResults as first targets Co-Authored-By: Paperclip <noreply@paperclip.ing>
98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
'use client';
|
|
|
|
import * as Sentry from '@sentry/nextjs';
|
|
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
|
|
export interface ErrorBoundaryFallbackProps {
|
|
error: Error;
|
|
reset: () => void;
|
|
}
|
|
|
|
export interface ErrorBoundaryProps {
|
|
children: ReactNode;
|
|
/** Custom fallback component. Receives the caught error and a reset callback. */
|
|
fallback?: (props: ErrorBoundaryFallbackProps) => ReactNode;
|
|
/** Called when an error is caught. Useful for additional logging / side effects. */
|
|
onError?: (error: Error, info: ErrorInfo) => void;
|
|
}
|
|
|
|
interface ErrorBoundaryState {
|
|
error: Error | null;
|
|
}
|
|
|
|
/**
|
|
* Generic class-based React Error Boundary.
|
|
*
|
|
* Captures exceptions via Sentry and renders a Vietnamese-language fallback UI
|
|
* with a "Thử lại" (retry) button when no custom fallback is provided.
|
|
*/
|
|
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
override state: ErrorBoundaryState = { error: null };
|
|
|
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
return { error };
|
|
}
|
|
|
|
override componentDidCatch(error: Error, info: ErrorInfo) {
|
|
Sentry.captureException(error, { extra: { componentStack: info.componentStack } });
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
console.error('[ErrorBoundary] caught:', error, info);
|
|
}
|
|
|
|
this.props.onError?.(error, info);
|
|
}
|
|
|
|
reset = () => {
|
|
this.setState({ error: null });
|
|
};
|
|
|
|
override render() {
|
|
const { error } = this.state;
|
|
const { children, fallback } = this.props;
|
|
|
|
if (error) {
|
|
if (fallback) {
|
|
return fallback({ error, reset: this.reset });
|
|
}
|
|
return <DefaultErrorFallback error={error} reset={this.reset} />;
|
|
}
|
|
|
|
return children;
|
|
}
|
|
}
|
|
|
|
function DefaultErrorFallback({ error, reset }: ErrorBoundaryFallbackProps) {
|
|
return (
|
|
<div
|
|
className="flex min-h-[200px] flex-col items-center justify-center rounded-lg border border-destructive/20 bg-destructive/5 px-6 py-8 text-center"
|
|
role="alert"
|
|
>
|
|
<svg
|
|
className="mx-auto mb-3 h-8 w-8 text-destructive"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
/>
|
|
</svg>
|
|
<p className="text-sm font-medium text-foreground">Đã xảy ra lỗi</p>
|
|
{process.env.NODE_ENV !== 'production' && error.message && (
|
|
<p className="mt-1 max-w-xs truncate text-xs text-muted-foreground">{error.message}</p>
|
|
)}
|
|
<button
|
|
onClick={reset}
|
|
className="mt-4 inline-flex h-9 items-center justify-center rounded-md bg-primary px-5 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
>
|
|
Thử lại
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|