Skip to content

Commit 0233685

Browse files
committed
Backend: Add structured logging + request correlation IDs
1 parent 87e24b5 commit 0233685

20 files changed

Lines changed: 637 additions & 294 deletions

frontend/package-lock.json

Lines changed: 65 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/App.tsx

Lines changed: 47 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
import { ThemeProvider } from "./context/ThemeContext";
99
import { ToastProvider } from "./context/ToastContext";
1010
import { VaultProvider } from "./context/VaultContext";
11-
import { ToastProvider } from "./context/ToastContext";
11+
import { CorrelationIdProvider } from "./context/CorrelationIdContext";
12+
import { CorrelationIdSync } from "./components/CorrelationIdSync";
1213
import Navbar from "./components/Navbar";
1314
import "./index.css";
1415

@@ -83,83 +84,57 @@ function App() {
8384
return (
8485
<Sentry.ErrorBoundary
8586
fallback={({ error, resetError }) => (
86-
<ErrorFallback error={error} resetError={resetError} />
87+
<ErrorFallback error={error as Error} resetError={resetError} />
8788
)}
8889
showDialog
8990
>
9091
<ThemeProvider>
91-
<ToastProvider>
92-
<VaultProvider>
93-
<VaultProvider>
94-
<Router>
95-
<div className="app-container">
96-
<Navbar
97-
walletAddress={walletAddress}
98-
onConnect={handleConnect}
99-
onDisconnect={handleDisconnect}
100-
/>
101-
<main
102-
className="container"
103-
style={{ marginTop: "100px", paddingBottom: "60px" }}
104-
>
105-
<Suspense fallback={<LoadingPage />}>
106-
{/* Replaced Routes with SentryRoutes to capture performance events */}
107-
<SentryRoutes>
108-
<Route
109-
path="/"
110-
element={<Home walletAddress={walletAddress} usdcBalance={usdcBalance} />}
111-
/>
112-
<Route
113-
path="/portfolio"
114-
element={<Portfolio walletAddress={walletAddress} />}
115-
/>
116-
<Route path="/analytics" element={<Analytics />} />
117-
<Route
118-
path="/transactions"
119-
element={
120-
<TransactionHistory walletAddress={walletAddress} />
121-
}
122-
/>
123-
<Route path="*" element={<Navigate to="/" replace />} />
124-
</SentryRoutes>
125-
</Suspense>
126-
</main>
127-
</div>
128-
</Router>
92+
<CorrelationIdProvider>
93+
<CorrelationIdSync />
12994
<ToastProvider>
130-
<Router>
131-
<div className="app-container">
132-
<Navbar
133-
walletAddress={walletAddress}
134-
onConnect={handleConnect}
135-
onDisconnect={handleDisconnect}
136-
/>
137-
<main
138-
className="container"
139-
style={{ marginTop: "100px", paddingBottom: "60px" }}
140-
>
141-
<Suspense fallback={<LoadingPage />}>
142-
{/* Replaced Routes with SentryRoutes to capture performance events */}
143-
<SentryRoutes>
144-
<Route
145-
path="/"
146-
element={<Home walletAddress={walletAddress} />}
147-
/>
148-
<Route
149-
path="/portfolio"
150-
element={<Portfolio walletAddress={walletAddress} />}
151-
/>
152-
<Route path="/analytics" element={<Analytics />} />
153-
<Route path="*" element={<Navigate to="/" replace />} />
154-
</SentryRoutes>
155-
</Suspense>
156-
</main>
157-
</div>
158-
</Router>
159-
</VaultProvider>
160-
</ToastProvider>
95+
<VaultProvider>
96+
<Router>
97+
<div className="app-container">
98+
<Navbar
99+
walletAddress={walletAddress}
100+
onConnect={handleConnect}
101+
onDisconnect={handleDisconnect}
102+
/>
103+
<main
104+
className="container"
105+
style={{ marginTop: "100px", paddingBottom: "60px" }}
106+
>
107+
<Suspense fallback={<LoadingPage />}>
108+
<SentryRoutes>
109+
<Route
110+
path="/"
111+
element={
112+
<Home
113+
walletAddress={walletAddress}
114+
usdcBalance={usdcBalance}
115+
/>
116+
}
117+
/>
118+
<Route
119+
path="/portfolio"
120+
element={<Portfolio walletAddress={walletAddress} />}
121+
/>
122+
<Route path="/analytics" element={<Analytics />} />
123+
<Route
124+
path="/transactions"
125+
element={
126+
<TransactionHistory walletAddress={walletAddress} />
127+
}
128+
/>
129+
<Route path="*" element={<Navigate to="/" replace />} />
130+
</SentryRoutes>
131+
</Suspense>
132+
</main>
133+
</div>
134+
</Router>
135+
</VaultProvider>
161136
</ToastProvider>
162-
</VaultProvider>
137+
</CorrelationIdProvider>
163138
</ThemeProvider>
164139
</Sentry.ErrorBoundary>
165140
);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useEffect } from "react";
2+
import { useCorrelationId } from "../context/CorrelationIdContext";
3+
import { setCorrelationIdGetter } from "../lib/apiClient";
4+
5+
/**
6+
* Invisible component that keeps the shared API client's correlation ID
7+
* getter in sync with the current `CorrelationIdContext` value.
8+
*
9+
* Must be rendered inside `CorrelationIdProvider`.
10+
*/
11+
export function CorrelationIdSync() {
12+
const { correlationId } = useCorrelationId();
13+
14+
useEffect(() => {
15+
setCorrelationIdGetter(() => correlationId);
16+
}, [correlationId]);
17+
18+
return null;
19+
}

frontend/src/components/Pagination.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ export const Pagination: React.FC<PaginationProps> = ({
4949
return (
5050
<div className="pagination-container" aria-label="Pagination">
5151
<div className="pagination-summary" aria-live="polite">
52-
Showing <strong>{startItem}{endItem}</strong> of{" "}
53-
<strong>{totalItems}</strong> results
52+
{`Page ${page} of ${totalPages}`} &mdash; Showing {startItem}{endItem} of {totalItems} results
5453
</div>
5554

5655
<div className="pagination-controls-wrapper">

frontend/src/components/Tabs.tsx

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,31 +28,28 @@ interface TabsProps {
2828
className?: string;
2929
}
3030

31-
export function Tabs({
31+
/** Inner component that uses useSearchParams — only rendered when syncWithUrl=true */
32+
function TabsWithUrl({
3233
defaultValue,
3334
value: controlledValue,
3435
onValueChange,
35-
syncWithUrl = false,
3636
urlParam = "tab",
3737
children,
3838
className = "",
39-
}: TabsProps) {
39+
}: Omit<TabsProps, "syncWithUrl">) {
4040
const [searchParams, setSearchParams] = useSearchParams();
4141
const [internalValue, setInternalValue] = useState(defaultValue || "");
4242

43-
const urlValue = syncWithUrl ? searchParams.get(urlParam) : null;
43+
const urlValue = searchParams.get(urlParam);
4444
const activeValue =
4545
controlledValue !== undefined
4646
? controlledValue
47-
: syncWithUrl && urlValue
47+
: urlValue
4848
? urlValue
4949
: internalValue;
5050

5151
useEffect(() => {
52-
// If we're syncing with URL but the param isn't there, and we have a defaultValue,
53-
// let's set the default value in the URL implicitly, or just let activeValue handle it.
54-
// Setting it explicitly ensures deep links are predictable.
55-
if (syncWithUrl && !urlValue && defaultValue) {
52+
if (!urlValue && defaultValue) {
5653
setSearchParams(
5754
(prev) => {
5855
const newParams = new URLSearchParams(prev);
@@ -62,24 +59,53 @@ export function Tabs({
6259
{ replace: true }
6360
);
6461
}
65-
}, [syncWithUrl, urlValue, defaultValue, urlParam, setSearchParams]);
62+
}, [urlValue, defaultValue, urlParam, setSearchParams]);
6663

6764
const handleValueChange = (newValue: string) => {
6865
if (controlledValue === undefined) {
6966
setInternalValue(newValue);
7067
}
7168

72-
if (syncWithUrl) {
73-
setSearchParams(
74-
(prev) => {
75-
const newParams = new URLSearchParams(prev);
76-
newParams.set(urlParam, newValue);
77-
return newParams;
78-
},
79-
{ replace: true }
80-
);
69+
setSearchParams(
70+
(prev) => {
71+
const newParams = new URLSearchParams(prev);
72+
newParams.set(urlParam, newValue);
73+
return newParams;
74+
},
75+
{ replace: true }
76+
);
77+
78+
if (onValueChange) {
79+
onValueChange(newValue);
8180
}
81+
};
82+
83+
return (
84+
<TabsContext.Provider value={{ value: activeValue, onValueChange: handleValueChange }}>
85+
<div className={`tabs-root ${className}`} data-state={activeValue}>
86+
{children}
87+
</div>
88+
</TabsContext.Provider>
89+
);
90+
}
91+
92+
/** Inner component for tabs without URL sync */
93+
function TabsWithoutUrl({
94+
defaultValue,
95+
value: controlledValue,
96+
onValueChange,
97+
children,
98+
className = "",
99+
}: Omit<TabsProps, "syncWithUrl" | "urlParam">) {
100+
const [internalValue, setInternalValue] = useState(defaultValue || "");
101+
102+
const activeValue =
103+
controlledValue !== undefined ? controlledValue : internalValue;
82104

105+
const handleValueChange = (newValue: string) => {
106+
if (controlledValue === undefined) {
107+
setInternalValue(newValue);
108+
}
83109
if (onValueChange) {
84110
onValueChange(newValue);
85111
}
@@ -94,6 +120,16 @@ export function Tabs({
94120
);
95121
}
96122

123+
export function Tabs({
124+
syncWithUrl = false,
125+
...props
126+
}: TabsProps) {
127+
if (syncWithUrl) {
128+
return <TabsWithUrl {...props} />;
129+
}
130+
return <TabsWithoutUrl {...props} />;
131+
}
132+
97133
export function TabsList({ children, className = "", style }: { children: ReactNode; className?: string; style?: React.CSSProperties }) {
98134
return (
99135
<div

0 commit comments

Comments
 (0)