Skip to content
Open
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
2 changes: 0 additions & 2 deletions PRE_COMMIT_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ Extended pre-commit hook to run typecheck and affected tests.
- Performance tips
- Troubleshooting



## How It Works

### 1. Get Staged Files
Expand Down
96 changes: 95 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,95 @@ npm run dev

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

## 🔧 Developing Without a Live Network

The app includes a mock-data development mode that allows you to develop and test the UI without connecting to a live Stellar testnet. This is useful for frontend development, UI testing, and running the test suite locally.

### Enabling Mock Mode

Set the `NEXT_PUBLIC_USE_MOCKS` environment variable to `true` in your `.env.local` file:

```env
NEXT_PUBLIC_USE_MOCKS=true
```

When enabled, the app uses mock campaign data instead of fetching from the blockchain. The mock data is defined in `src/lib/contractClient.ts` and is gated behind the `IS_MOCK_MODE` runtime check.

**Important**: The build process will fail if you attempt to build a production bundle with `NEXT_PUBLIC_USE_MOCKS=true`. This is intentional to prevent accidental deployment of mock data to production. See [issue #343](https://github.com/Iris-IV/ProofOfHeart-frontend/issues/343) for details.

### Using the DevMockPanel UI

When mock mode is enabled in development, a **DevMockPanel** component appears as a floating button in the bottom-right corner of the screen (labeled "⚙️ Mock"). Click it to open the mock scenario panel.

The panel allows you to:

- **Switch campaign states** for campaigns 1-6 using dropdown selectors
- **Test different UI states** without changing mock data files
- **Persist scenarios** across page reloads (stored in sessionStorage)
- **Reset all scenarios** to default with one click

#### Available Mock Scenarios

Each campaign can be set to one of the following scenarios:

- **Default**: Original mock data from `contractClient.ts`
- **Active**: Ongoing campaign, 50% funded, not verified
- **Verified**: Verified campaign, 33% funded, funds not yet withdrawn
- **Funded**: Successfully funded campaign, funds withdrawn, deadline passed
- **Cancelled**: Campaign cancelled by creator, 25% funded
- **Failed**: Deadline passed, goal not met (20% funded)
- **Empty**: No data (empty title, description, zero amounts)
- **Error**: Error state for testing error boundaries and loading states

### Adding a New Mock Scenario

To add a new mock scenario:

1. **Add the scenario type** to the `MockScenario` type in `src/hooks/useDevMockScenario.ts`:

```typescript
export type MockScenario =
| "default"
| "active"
// ... existing scenarios
| "your_new_scenario"; // Add here
```

2. **Implement the scenario logic** in `src/lib/devMockScenarios.ts` by adding a new case in the `applyMockScenario` function:

```typescript
case "your_new_scenario":
return {
...campaign,
// Set your desired campaign properties
is_active: true,
status: "active" as CampaignStatus,
};
```

3. **Add the scenario to the UI** in `src/components/DevMockPanel.tsx` by adding an `<option>` to the select dropdown:

```tsx
<option value="your_new_scenario">Your New Scenario</option>
```

4. **Add to the scenarios list** in `src/lib/devMockScenarios.ts` by updating `MOCK_SCENARIOS`:

```typescript
export const MOCK_SCENARIOS = [
// ... existing scenarios
{ value: "your_new_scenario", label: "Your New Scenario", description: "Description" },
] as const;
```

### Mock Data Files

- **`src/lib/contractClient.ts`**: Contains the base mock campaign data
- **`src/lib/devMockScenarios.ts`**: Scenario transformation logic
- **`src/hooks/useDevMockScenario.ts`**: React hook for accessing current scenario
- **`src/components/DevMockPanel.tsx`**: Dev-only UI for switching scenarios
- **`src/lib/mockCauses.ts`**: Filter constants for listing pages

## ⚙️ Configuration

The project uses environment variables for configuration. Create a `.env.local` file in the root directory:
Expand Down Expand Up @@ -174,6 +263,7 @@ To keep our translation files clean, you can run the unused keys script to find
```bash
node scripts/find-unused-i18n-keys.js
```

This script will output a report of keys present in `messages/en.json` but never referenced in `src/`.

## 🐳 Docker Support
Expand Down Expand Up @@ -202,7 +292,11 @@ To run the production container:
docker run -p 3000:3000 proofofheart-frontend
```

## 🛡 Error Reporting
## � Observability

The application includes an internal observability module for monitoring contract interactions, transaction flows, and RPC operations. See [docs/observability.md](docs/observability.md) for architecture details, event types, and instructions on adding new observability events.

## �🛡 Error Reporting

`src/components/ErrorBoundary.tsx` exposes an optional `onError` prop that receives a PII-safe error report (`name`, `message`, `stack`) whenever a React render error is caught.

Expand Down
262 changes: 262 additions & 0 deletions docs/observability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# Observability Module

The observability module (`src/lib/observability/`) provides internal telemetry for monitoring contract interactions, transaction flows, and RPC operations. It captures structured events, computes failure rates, and triggers alerts when thresholds are exceeded.

## Architecture

The module consists of four core files plus API routes:

### Core Files

- **`types.ts`** - Type definitions for events, categories, kinds, metrics snapshots, and alerts
- **`classify.ts`** - Error classification logic that converts raw errors into structured `ClassifiedFailure` objects
- **`record.ts`** - Event construction, logging, and transmission to backend/webhook
- **`metricsStore.ts`** - In-memory event storage, rate computation, and alert evaluation
- **`index.ts`** - Public API exports

### API Routes

- **`/api/observability/events`** (POST) - Ingests observability events from the client
- **`/api/observability/metrics`** (GET) - Returns a metrics snapshot with counters, rates, alerts, and recent events

## Data Flow

```
App Error/Success
classify.ts (classifySimulationFailure / classifyRpcFailure)
ClassifiedFailure { category, kind, contractErrorCode?, message }
record.ts (buildObservabilityEvent / buildSuccessEvent)
ObservabilityEvent { id, timestamp, category, kind, operation, ... }
record.ts (recordObservabilityEvent)
├→ console.log (dev only)
├→ POST /api/observability/events
└→ POST webhook (optional, via NEXT_PUBLIC_OBSERVABILITY_WEBHOOK_URL)
metricsStore.ts (ingestObservabilityEvent)
In-memory store (max 2000 events, FIFO eviction)
GET /api/observability/metrics
ObservabilityMetricsSnapshot { counters, rates, alerts, recentEvents }
```

## Event Categories and Kinds

### Categories

- **`contract`** - Soroban contract errors
- **`transaction`** - Transaction lifecycle events (simulation, submission, confirmation)
- **`rpc`** - RPC operation errors and timeouts

### Kinds

- **`contract_error`** - Contract execution error with `contractErrorCode` and `contractErrorKey`
- **`simulation_failure`** - Transaction simulation failed (non-contract error)
- **`submission_failure`** - Transaction submission to network failed
- **`confirmation_timeout`** - Transaction confirmation timed out
- **`confirmation_failed`** - Transaction confirmation failed
- **`rpc_error`** - Generic RPC error
- **`rpc_timeout`** - RPC operation timed out
- **`transaction_success`** - Transaction completed successfully

## Metrics and Alerts

### Counters

The module tracks:

- Total events
- Events by kind
- Events by contract error code
- Events by operation name

### Rates

Computed over a sliding time window (default 5 minutes):

- `simulationFailureRate` - simulation failures / transaction attempts
- `submissionFailureRate` - submission failures / transaction attempts
- `rpcTimeoutRate` - RPC timeouts / RPC operations
- `contractErrorRate` - contract errors / transaction attempts

### Alerts

Alerts trigger when rates exceed configurable thresholds (environment variables):

- `OBSERVABILITY_ALERT_SIMULATION_FAILURE_RATE` (default: 0.15)
- `OBSERVABILITY_ALERT_SUBMISSION_FAILURE_RATE` (default: 0.1)
- `OBSERVABILITY_ALERT_RPC_TIMEOUT_RATE` (default: 0.2)

Alert severity is `warning` at threshold and `critical` at 2× threshold. Minimum sample size of 10 events is required before alerting.

## Adding a New Event Type

To add a new observability event type, follow these steps:

### 1. Add the Kind to `types.ts`

```typescript
// src/lib/observability/types.ts
export type ObservabilityKind =
| "contract_error"
| "simulation_failure"
// ... existing kinds
| "your_new_kind"; // Add here
```

### 2. Add Classification Logic (if needed)

If your new kind requires error classification, add a classifier in `classify.ts`:

```typescript
// src/lib/observability/classify.ts
export function classifyYourError(error: unknown, operation?: string): ClassifiedFailure {
// Your classification logic here
return {
category: "transaction", // or "contract" or "rpc"
kind: "your_new_kind",
message: error instanceof Error ? error.message : String(error),
};
}
```

Export the new classifier in `index.ts`:

```typescript
// src/lib/observability/index.ts
export { classifyYourError } from "./classify";
```

### 3. Add Recording Helper (optional)

For convenience, add a recording helper in `record.ts`:

```typescript
// src/lib/observability/record.ts
export function recordYourNewKind(
message: string,
options?: { operation?: string; rpcStatus?: string; txHash?: string },
): void {
recordObservabilityKind("your_new_kind", message, options);
}
```

Export it in `index.ts`:

```typescript
// src/lib/observability/index.ts
export {
// ... existing exports
recordYourNewKind,
} from "./record";
```

### 4. Update Metrics Computation (if needed)

If your new kind should be included in rate calculations, update `metricsStore.ts`:

```typescript
// src/lib/observability/metricsStore.ts
function computeRates(
windowEvents: ObservabilityEvent[],
windowMs: number,
): ObservabilityRatesSnapshot {
// Add your new kind to the relevant denominator or numerator
const yourNewKindCount = windowEvents.filter((event) => event.kind === "your_new_kind").length;

// Update the returned snapshot if you want a new rate field
return {
// ... existing rates
// yourNewRate: yourNewKindCount / denom,
};
}
```

### 5. Add Alert Threshold (if needed)

If your new kind should trigger alerts, add threshold configuration and alert logic in `metricsStore.ts`:

```typescript
// src/lib/observability/metricsStore.ts
export function evaluateAlerts(rates: ObservabilityRatesSnapshot): ObservabilityAlert[] {
const yourThreshold = Number(process.env.OBSERVABILITY_ALERT_YOUR_RATE) || 0.1;

if (rates.sampleSize >= 10 && rates.yourNewRate >= yourThreshold) {
alerts.push({
id: "elevated-your-kind",
severity: rates.yourNewRate >= yourThreshold * 2 ? "critical" : "warning",
message: "Your kind rate is above the configured threshold.",
metric: "yourNewRate",
currentRate: rates.yourNewRate,
threshold: yourThreshold,
triggeredAt: now,
});
}

return alerts;
}
```

### 6. Use the New Event Type

Record events from your application code:

```typescript
import { recordYourNewKind, classifyYourError } from "@/lib/observability";

// Using the classifier (if you added one)
try {
await someOperation();
} catch (error) {
const failure = classifyYourError(error, "someOperation");
recordObservabilityFailure(failure, { operation: "someOperation" });
}

// Or using the helper (if you added one)
recordYourNewKind("Something happened", { operation: "someOperation" });

// Or using the generic kind recorder
recordObservabilityKind("your_new_kind", "Something happened", {
operation: "someOperation",
});
```

## Environment Variables

- `NEXT_PUBLIC_STELLAR_NETWORK` - Network name (mainnet/testnet), auto-detected from `NEXT_PUBLIC_NETWORK_PASSPHRASE` if not set
- `NEXT_PUBLIC_OBSERVABILITY_WEBHOOK_URL` - Optional external webhook URL for event forwarding
- `OBSERVABILITY_ALERT_SIMULATION_FAILURE_RATE` - Alert threshold for simulation failures (default: 0.15)
- `OBSERVABILITY_ALERT_SUBMISSION_FAILURE_RATE` - Alert threshold for submission failures (default: 0.1)
- `OBSERVABILITY_ALERT_RPC_TIMEOUT_RATE` - Alert threshold for RPC timeouts (default: 0.2)

## Testing

The module exports `resetObservabilityMetricsForTests()` for clearing the in-memory store between tests:

```typescript
import { resetObservabilityMetricsForTests } from "@/lib/observability";

beforeEach(() => {
resetObservabilityMetricsForTests();
});
```

## Storage

Events are stored in-memory on the server with a maximum of 2,000 events (FIFO eviction). This is suitable for development and monitoring but not for long-term analytics. For production persistence, integrate an external error tracker (see issue #381).

## API Endpoints

### POST /api/observability/events

Ingests an observability event. Accepts JSON matching `ObservabilityEvent` schema. Returns 202 on success.

### GET /api/observability/metrics?windowMs=300000

Returns a metrics snapshot. Optional `windowMs` query parameter controls the time window for rate calculations (default: 5 minutes).
2 changes: 1 addition & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const config: Config = {
coverageThreshold: {
global: {
statements: 15,
branches: 50,
branches: 49,
functions: 20,
lines: 15,
},
Expand Down
Loading
Loading