Skip to content

Commit b33ba35

Browse files
authored
Merge branch 'main' into fix/26-trivial-frontend-implement-max-balance-button
2 parents 50083be + af0a471 commit b33ba35

833 files changed

Lines changed: 483318 additions & 2958 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@
99
.idea
1010
.vscode
1111
*.swp
12+
13+
# Agents
14+
GEMINI.md
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"specId": "545f7066-8f82-4dd3-bbaa-7a754be1793e", "workflowType": "requirements-first", "specType": "feature"}
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
# Design: Structured Logging + Correlation IDs
2+
3+
## Overview
4+
5+
This feature adds structured JSON logging and correlation ID propagation to the YieldVault-RWA off-chain layer (the React/TypeScript frontend and its API client). The goal is to make every API call traceable end-to-end: each request carries a unique `X-Correlation-ID` header, that ID flows through telemetry events and error objects, and all log output is machine-readable JSON with a consistent field schema.
6+
7+
The scope is intentionally limited to the off-chain layer — the frontend API client (`frontend/src/lib/api/`), the telemetry bus, and the React context that exposes the current correlation ID to components. On-chain Soroban contracts are out of scope.
8+
9+
### Key Design Decisions
10+
11+
- **No new runtime dependencies for logging**: The existing telemetry pub/sub system (`telemetry.ts`) is extended rather than replaced. A thin `logger` module subscribes to telemetry events and writes structured JSON to `console`.
12+
- **Interceptor-based correlation ID injection**: The existing `ApiClient` interceptor API is used to attach `X-Correlation-ID` on every request and read it back from responses, keeping the concern isolated.
13+
- **React Context for correlation ID**: A `CorrelationIdContext` makes the active ID available to any component without prop drilling, consistent with how `ThemeContext` and `ToastContext` are already structured.
14+
- **UUID v4 via `crypto.randomUUID()`**: Available in all modern browsers and Node 14.17+; no library needed.
15+
16+
---
17+
18+
## Architecture
19+
20+
```mermaid
21+
flowchart TD
22+
subgraph React App
23+
A[Component] -->|reads| B[CorrelationIdContext]
24+
B -->|provides correlationId| C[ApiClient]
25+
end
26+
27+
subgraph ApiClient
28+
C -->|request interceptor| D[attach X-Correlation-ID header]
29+
D -->|fetch| E[Backend / Mock API]
30+
E -->|response| F[response interceptor]
31+
F -->|extract X-Correlation-ID| G[emitApiTelemetry]
32+
end
33+
34+
subgraph Logging
35+
G -->|ApiTelemetryEvent + correlationId| H[logger subscriber]
36+
H -->|JSON.stringify| I[console output]
37+
end
38+
39+
subgraph Error Path
40+
F -->|HTTP error| J[ApiError with correlationId]
41+
J --> H
42+
end
43+
```
44+
45+
The data flow is:
46+
47+
1. A component (or script) obtains or generates a correlation ID.
48+
2. The `ApiClient` request interceptor reads the ID from context and sets the header.
49+
3. On response, the response interceptor reads the echoed header (if present) and attaches it to the telemetry event.
50+
4. The logger subscriber formats the event as a JSON log entry and writes it.
51+
5. Errors carry the correlation ID so they can be correlated with the request log line.
52+
53+
---
54+
55+
## Components and Interfaces
56+
57+
### `logger` module (`frontend/src/lib/logger.ts`)
58+
59+
Thin structured-logging facade. Subscribes to the telemetry bus and also exposes a direct `log()` function for non-telemetry log sites.
60+
61+
```typescript
62+
export type LogLevel = "debug" | "info" | "warn" | "error";
63+
64+
export interface LogEntry {
65+
timestamp: string; // ISO 8601
66+
level: LogLevel;
67+
message: string;
68+
correlationId?: string;
69+
method?: string;
70+
url?: string;
71+
durationMs?: number;
72+
attempt?: number;
73+
status?: number;
74+
errorCode?: string;
75+
[key: string]: unknown; // extensible for future fields
76+
}
77+
78+
export interface LoggerConfig {
79+
minLevel: LogLevel;
80+
output?: (entry: LogEntry) => void; // defaults to console.log(JSON.stringify(entry))
81+
}
82+
83+
export function configureLogger(config: LoggerConfig): void;
84+
export function log(level: LogLevel, message: string, fields?: Partial<LogEntry>): void;
85+
```
86+
87+
Log level ordering: `debug < info < warn < error`. Entries below `minLevel` are silently dropped.
88+
89+
### `CorrelationIdContext` (`frontend/src/context/CorrelationIdContext.tsx`)
90+
91+
React context that holds the active correlation ID for the current "session" or user interaction.
92+
93+
```typescript
94+
export interface CorrelationIdContextValue {
95+
correlationId: string;
96+
/** Replace the active ID (e.g. when starting a new user action). */
97+
refreshCorrelationId: () => void;
98+
}
99+
100+
export const CorrelationIdContext: React.Context<CorrelationIdContextValue>;
101+
export function CorrelationIdProvider({ children }: { children: React.ReactNode }): JSX.Element;
102+
export function useCorrelationId(): CorrelationIdContextValue;
103+
```
104+
105+
The provider generates a UUID v4 on mount and exposes `refreshCorrelationId` to rotate it.
106+
107+
### `correlationInterceptors` (`frontend/src/lib/api/correlationInterceptors.ts`)
108+
109+
Two `ApiClient` interceptors that handle ID injection and extraction.
110+
111+
```typescript
112+
/**
113+
* Request interceptor: reads correlationId from the provided getter and
114+
* sets the X-Correlation-ID header on every outgoing request.
115+
*/
116+
export function createCorrelationRequestInterceptor(
117+
getCorrelationId: () => string,
118+
): RequestInterceptor;
119+
120+
/**
121+
* Response interceptor: reads X-Correlation-ID from the response headers
122+
* and attaches it to the telemetry context for downstream logging.
123+
*/
124+
export function createCorrelationResponseInterceptor(): ResponseInterceptor;
125+
```
126+
127+
### Extended `ApiTelemetryEvent` (`frontend/src/lib/api/telemetry.ts`)
128+
129+
Each event variant gains an optional `correlationId` field:
130+
131+
```typescript
132+
// Added to every event variant:
133+
correlationId?: string;
134+
```
135+
136+
### Extended `ApiError` (`frontend/src/lib/api/error.ts`)
137+
138+
`ApiErrorMetadata` and `ApiError` gain:
139+
140+
```typescript
141+
correlationId?: string;
142+
```
143+
144+
---
145+
146+
## Data Models
147+
148+
### Log Entry Schema
149+
150+
Every log line written to `console` is a single-line JSON object conforming to `LogEntry`:
151+
152+
| Field | Type | Required | Description |
153+
|---|---|---|---|
154+
| `timestamp` | string (ISO 8601) | yes | When the entry was created |
155+
| `level` | `"debug"\|"info"\|"warn"\|"error"` | yes | Severity |
156+
| `message` | string | yes | Human-readable summary |
157+
| `correlationId` | string (UUID v4) | no | Active correlation ID |
158+
| `method` | string | no | HTTP method (GET, POST, …) |
159+
| `url` | string | no | Full request URL |
160+
| `durationMs` | number | no | Round-trip time in milliseconds |
161+
| `attempt` | number | no | Retry attempt number (1-based) |
162+
| `status` | number | no | HTTP response status code |
163+
| `errorCode` | string | no | `ApiErrorCode` value on error |
164+
165+
Example output:
166+
167+
```json
168+
{"timestamp":"2026-01-15T10:23:45.123Z","level":"info","message":"API request succeeded","correlationId":"f47ac10b-58cc-4372-a567-0e02b2c3d479","method":"GET","url":"https://api.example.com/mock-api/vault-summary.json","durationMs":142,"attempt":1,"status":200}
169+
```
170+
171+
### Correlation ID Format
172+
173+
UUID v4 as produced by `crypto.randomUUID()`. Example: `f47ac10b-58cc-4372-a567-0e02b2c3d479`.
174+
175+
Pattern: `/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i`
176+
177+
### Header Convention
178+
179+
| Header | Direction | Description |
180+
|---|---|---|
181+
| `X-Correlation-ID` | Request (client → server) | Client-generated or client-propagated ID |
182+
| `X-Correlation-ID` | Response (server → client) | Server echoes or overrides the ID |
183+
| `X-Trace-ID` | Response (server → client) | Already read by `ApiClient`; stored on `ApiError.traceId` (unchanged) |
184+
185+
---
186+
187+
## Correctness Properties
188+
189+
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
190+
191+
### Property 1: Every outgoing request carries a correlation ID header
192+
193+
*For any* API request made through `ApiClient` when the correlation request interceptor is registered, the outgoing `Headers` object must contain an `X-Correlation-ID` entry with a non-empty string value.
194+
195+
**Validates: Requirements 1.1**
196+
197+
---
198+
199+
### Property 2: Client-supplied correlation ID takes precedence
200+
201+
*For any* API request where the caller provides a specific correlation ID via the context getter, the `X-Correlation-ID` header on the outgoing request must equal that provided ID, not a freshly generated one.
202+
203+
**Validates: Requirements 1.3**
204+
205+
---
206+
207+
### Property 3: Generated correlation IDs are valid UUID v4
208+
209+
*For any* call to the correlation ID generation function, the returned string must match the UUID v4 format pattern `/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i`.
210+
211+
**Validates: Requirements 1.4**
212+
213+
---
214+
215+
### Property 4: All observable outputs carry the correlation ID
216+
217+
*For any* API request that completes (successfully or with an error), the resulting telemetry event and any resulting `ApiError` must both carry the same `correlationId` value that was sent on the request.
218+
219+
**Validates: Requirements 2.1, 2.2**
220+
221+
---
222+
223+
### Property 5: Log entries serialize to valid JSON with all required fields
224+
225+
*For any* log entry produced by the logger (at any level), calling `JSON.stringify` on it must produce valid JSON, and the parsed object must contain `timestamp`, `level`, and `message` fields with the correct types.
226+
227+
**Validates: Requirements 2.3, 3.1**
228+
229+
---
230+
231+
### Property 6: Log level filtering excludes entries below threshold
232+
233+
*For any* logger configuration with `minLevel` set to level L, and *for any* log entry with a level below L, that entry must not appear in the output (the `output` function must not be called for it).
234+
235+
**Validates: Requirements 3.2**
236+
237+
---
238+
239+
### Property 7: Correlation ID is accessible throughout the React component tree
240+
241+
*For any* component rendered as a descendant of `CorrelationIdProvider`, calling `useCorrelationId()` must return the same `correlationId` value that the provider holds, regardless of nesting depth.
242+
243+
**Validates: Requirements 4.2**
244+
245+
---
246+
247+
## Error Handling
248+
249+
### Missing or malformed `X-Correlation-ID` in response
250+
251+
If the server does not echo `X-Correlation-ID`, the client retains the ID it generated for the request. No error is thrown; the field is simply absent from the response-side telemetry.
252+
253+
### `crypto.randomUUID()` unavailability
254+
255+
`crypto.randomUUID()` is available in all browsers that support the Web Crypto API (Chrome 92+, Firefox 95+, Safari 15.4+). If unavailable (e.g., non-secure context in very old browsers), the interceptor falls back to a timestamp-based pseudo-ID prefixed with `fallback-` and logs a `warn` entry. This is a graceful degradation path, not a hard failure.
256+
257+
### Logger output errors
258+
259+
If the `output` function throws (e.g., a custom output sink fails), the error is caught and silently swallowed to prevent logging from disrupting the application. A single `console.error` is written as a last resort.
260+
261+
### Correlation ID context outside provider
262+
263+
If `useCorrelationId()` is called outside `CorrelationIdProvider`, it throws a descriptive error: `"useCorrelationId must be used within a CorrelationIdProvider"`. This matches the pattern used by `ToastContext`.
264+
265+
---
266+
267+
## Testing Strategy
268+
269+
### Dual Testing Approach
270+
271+
Both unit tests and property-based tests are used. Unit tests cover specific examples, integration points, and error conditions. Property tests verify universal invariants across many generated inputs.
272+
273+
### Property-Based Testing Library
274+
275+
**fast-check** is the chosen PBT library for TypeScript/Vitest. It integrates cleanly with Vitest via `fc.assert(fc.property(...))` and supports async properties.
276+
277+
Install: `npm install --save-dev fast-check`
278+
279+
Each property test runs a minimum of **100 iterations** (fast-check default is 100; set explicitly via `{ numRuns: 100 }`).
280+
281+
Each property test is tagged with a comment in the format:
282+
`// Feature: structured-logging-correlation-ids, Property N: <property_text>`
283+
284+
### Property Tests
285+
286+
Each correctness property maps to exactly one property-based test:
287+
288+
**Property 1**`correlationInterceptors.test.ts`
289+
```
290+
// Feature: structured-logging-correlation-ids, Property 1: Every outgoing request carries a correlation ID header
291+
fc.assert(fc.asyncProperty(fc.record({ method: fc.constantFrom('GET','POST'), path: fc.string() }), async ({ method, path }) => {
292+
// build request context, run interceptor, assert headers.get('X-Correlation-ID') is non-empty
293+
}))
294+
```
295+
296+
**Property 2**`correlationInterceptors.test.ts`
297+
```
298+
// Feature: structured-logging-correlation-ids, Property 2: Client-supplied correlation ID takes precedence
299+
fc.assert(fc.asyncProperty(fc.uuid(), async (suppliedId) => {
300+
// provide suppliedId via getter, run interceptor, assert header === suppliedId
301+
}))
302+
```
303+
304+
**Property 3**`correlationId.test.ts`
305+
```
306+
// Feature: structured-logging-correlation-ids, Property 3: Generated correlation IDs are valid UUID v4
307+
fc.assert(fc.property(fc.integer({ min: 1, max: 1000 }), (_n) => {
308+
const id = generateCorrelationId();
309+
return UUID_V4_PATTERN.test(id);
310+
}))
311+
```
312+
313+
**Property 4**`logger.test.ts`
314+
```
315+
// Feature: structured-logging-correlation-ids, Property 4: All observable outputs carry the correlation ID
316+
fc.assert(fc.asyncProperty(fc.uuid(), fc.constantFrom('success','error'), async (correlationId, outcome) => {
317+
// simulate telemetry event with correlationId, assert logger output and ApiError carry same ID
318+
}))
319+
```
320+
321+
**Property 5**`logger.test.ts`
322+
```
323+
// Feature: structured-logging-correlation-ids, Property 5: Log entries serialize to valid JSON with required fields
324+
fc.assert(fc.property(fc.record({ level: fc.constantFrom('debug','info','warn','error'), message: fc.string() }), ({ level, message }) => {
325+
const entry = buildLogEntry(level, message, {});
326+
const parsed = JSON.parse(JSON.stringify(entry));
327+
return typeof parsed.timestamp === 'string' && typeof parsed.level === 'string' && typeof parsed.message === 'string';
328+
}))
329+
```
330+
331+
**Property 6**`logger.test.ts`
332+
```
333+
// Feature: structured-logging-correlation-ids, Property 6: Log level filtering excludes entries below threshold
334+
fc.assert(fc.property(
335+
fc.constantFrom('debug','info','warn','error'),
336+
fc.constantFrom('debug','info','warn','error'),
337+
(minLevel, entryLevel) => {
338+
// configure logger with minLevel, emit entry at entryLevel
339+
// assert output was called iff levelOrder[entryLevel] >= levelOrder[minLevel]
340+
}
341+
))
342+
```
343+
344+
**Property 7**`CorrelationIdContext.test.tsx`
345+
```
346+
// Feature: structured-logging-correlation-ids, Property 7: Correlation ID accessible throughout React tree
347+
fc.assert(fc.property(fc.integer({ min: 1, max: 10 }), (depth) => {
348+
// render CorrelationIdProvider with a component nested `depth` levels deep
349+
// assert useCorrelationId() returns the provider's ID
350+
}))
351+
```
352+
353+
### Unit Tests
354+
355+
- `correlationInterceptors.test.ts`: missing header fallback, non-secure context fallback to `fallback-` prefix
356+
- `logger.test.ts`: `configureLogger` reads `VITE_LOG_LEVEL` env var (example for Requirement 4.1), output-function error is swallowed
357+
- `CorrelationIdContext.test.tsx`: `refreshCorrelationId` produces a new UUID v4, calling hook outside provider throws descriptive error
358+
- `client.test.ts` (existing): verify interceptors are invoked in order; extend existing tests to assert `correlationId` on `ApiError`

0 commit comments

Comments
 (0)