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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Cost attribution by application tag or memo prefix** ([#868](https://github.com/Nanle-code/stellar-dev-dashboard/issues/868)).
Attributes transaction fees and asset transfer volumes to developer-defined project tags for project budgeting and threshold alerts.
- `src/lib/costThresholdManager.ts` — `CostThresholdManager` implementation for tag definition, memo prefix and regex pattern matching, budget limits, volume tracking, threshold alerts (`ok`, `warning`, `exceeded`), JSON/CSV report exports, and SSR/unsupported environment fallbacks.
- `src/lib/analytics.ts` — integrated `costAttribution` calculation into `buildAnalyticsSnapshot()` and re-exported `CostThresholdManager`.
- `docs/features/COST_ATTRIBUTION_ANALYTICS.md` — feature documentation, usage examples, compatibility, security, and migration notes.
- `src/lib/__tests__/costThresholdManager.test.ts` — 13 unit tests covering primary flow, boundary cases (0 fee, exact threshold %, 100% budget cap), failure paths (invalid regex, NaN/negative fees, null inputs), and SSR/storage error handling.
- **Cohort retention views for account activity** ([#863](https://github.com/Nanle-code/stellar-dev-dashboard/issues/863)).
Provides cohort charts that group accounts by first-seen period (Day, Week, Month) and subsequent activity.
- `src/lib/cohortRetention.ts` — domain calculations for cohort retention matrices, period headers, summary stats, CSV/JSON exports, and graceful error handling.
- `src/components/dashboard/CohortRetentionView.tsx` — accessible UI view featuring cohort heatmap matrix table, line chart, controls, view mode toggles, and export triggers.
- `docs/features/COHORT_RETENTION_ANALYTICS.md` — comprehensive user-facing and developer documentation including compatibility, security, and migration guidance.
- **Pre-sign risk summary** ([#982](https://github.com/Nanle-code/stellar-dev-dashboard/issues/982)).
Every transaction is now parsed and described in plain language before it
reaches a wallet, so an operation that irreversibly changes an account cannot
Expand Down
259 changes: 259 additions & 0 deletions CUSTOM_METRIC_BUILDER_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
# Custom Metric Builder Guide (#864)

Compose reusable metrics from Horizon fields and saved arithmetic expressions.

## Overview

The custom metric builder lets you define formulas that combine any numeric
Horizon API field with basic arithmetic (`+`, `-`, `*`, `/`) and
parentheses. Formulas are validated at save time, stored locally, and can be
evaluated on demand against any analytics snapshot.

## Quick Start

```typescript
import {
saveMetricFormula,
evaluateSavedFormula,
METRIC_FIELD_CATALOGUE,
} from './src/lib/metricBuilder';

// 1. Browse available fields
console.log(METRIC_FIELD_CATALOGUE.map(f => `${f.path} — ${f.label}`));

// 2. Save a reusable formula
saveMetricFormula({
id: 'fee-efficiency',
name: 'Fee Efficiency Ratio',
formula: 'network.baseFee / network.p90Fee',
format: 'percentage',
tags: ['fees', 'efficiency'],
});

// 3. Evaluate against a live snapshot
const snapshot = buildAnalyticsSnapshot({ /* ... Horizon data ... */ });
const result = evaluateSavedFormula('fee-efficiency', snapshot);
// → { value: 0.5, formatted: "50.0%", resolvedFields: ["network.baseFee", "network.p90Fee"] }
```

## Available Horizon Fields

The field catalogue (`METRIC_FIELD_CATALOGUE`) exposes these paths:

### Account fields

| Path | Label | Description |
|------|-------|-------------|
| `account.xlmBalance` | XLM Balance | Native XLM balance |
| `account.trustlineCount` | Trustline Count | Non-native trustlines |
| `account.totalAssets` | Total Assets | All balance entries including native |
| `account.nonNativeBalanceCount` | Non-Native Funded | Non-native trustlines with positive balance |

### Transaction fields

| Path | Label | Description |
|------|-------|-------------|
| `transactions.totalTransactions` | Total Transactions | Total transaction count |
| `transactions.successfulTransactions` | Successful Transactions | Count of successful transactions |
| `transactions.failedTransactions` | Failed Transactions | Count of failed transactions |
| `transactions.successRate` | Success Rate | Ratio (0–1) of successful to total |
| `transactions.weeklyActivity` | Weekly Activity | Transaction count in the last 7 days |
| `transactions.averageOperationsPerTx` | Avg Ops per Tx | Average operations per transaction |

### Network fields

| Path | Label | Description |
|------|-------|-------------|
| `network.latestLedgerSequence` | Latest Ledger | Most recent ledger sequence number |
| `network.baseFee` | Base Fee | Last ledger base fee (stroops) |
| `network.p90Fee` | P90 Fee | 90th percentile accepted fee (stroops) |
| `network.txSuccessCount` | Ledger Tx Success Count | Successful txs in latest ledger |
| `network.txFailedCount` | Ledger Tx Failed Count | Failed txs in latest ledger |
| `network.operationCount` | Ledger Operation Count | Total operations in latest ledger |
| `network.averageCloseSeconds` | Avg Close Time | Average ledger close time (seconds) |

## Formula Syntax

Formulas support:

- **Field references**: dot-separated paths (e.g. `account.xlmBalance`)
- **Numeric literals**: integers and decimals (e.g. `100`, `3.14`)
- **Operators**: `+`, `-`, `*`, `/` with standard precedence
- **Parentheses**: `(` `)` for grouping
- **Unary negation**: `-field.path` or `-(expr)`

### Examples

```
account.xlmBalance
transactions.successfulTransactions / transactions.totalTransactions
(network.baseFee + network.p90Fee) / 2
account.xlmBalance - (account.trustlineCount * 0.5)
-transactions.failedTransactions + transactions.successfulTransactions
```

### Limits

| Limit | Value |
|-------|-------|
| Max formula length | 1,024 characters |
| Max field depth | 5 segments |
| Max tokens | 256 |
| Max saved formulas | 100 |

## Display Formats

Each saved formula can specify a `format`:

| Format | Example output | Use case |
|--------|---------------|----------|
| `number` | `1,234.56` | General numeric values |
| `percentage` | `90.0%` | Ratios (value × 100) |
| `stroops` | `100 stroops` | Raw Stellar fee units |
| `xlm` | `1.0000000 XLM` | XLM amounts (stroops ÷ 10⁷) |

## CRUD API

```typescript
import {
saveMetricFormula,
getMetricFormula,
updateMetricFormula,
deleteMetricFormula,
listMetricFormulas,
clearAllMetricFormulas,
} from './src/lib/metricBuilder';

// Save
saveMetricFormula({ id: 'my-metric', name: 'My Metric', formula: '...' });

// Get by id
const formula = getMetricFormula('my-metric');

// List (optionally filtered by tags)
const all = listMetricFormulas();
const feeMetrics = listMetricFormulas(['fees']);

// Update
updateMetricFormula('my-metric', { name: 'Updated Name', formula: '...' });

// Delete
deleteMetricFormula('my-metric');

// Clear all (testing / reset)
clearAllMetricFormulas();
```

## Batch Evaluation

Evaluate multiple saved formulas against the same data snapshot. Failures
are captured individually so one bad formula does not block the others:

```typescript
import { evaluateBatch } from './src/lib/metricBuilder';

const results = evaluateBatch(
['fee-ratio', 'success-pct', 'missing-formula'],
snapshot,
);

for (const [id, result] of results) {
if ('error' in result) {
console.warn(`${id} failed: ${result.error}`);
} else {
console.log(`${id} = ${result.formatted}`);
}
}
```

## Import / Export

Back up or share saved formulas across environments:

```typescript
import { exportMetricFormulas, importMetricFormulas } from './src/lib/metricBuilder';

// Export
const json = exportMetricFormulas();
// → portable JSON array string

// Import (merges with existing; overwrites on id collision)
const { imported, skipped } = importMetricFormulas(json);
```

## Error Handling

All public functions throw `MetricBuilderError` with a typed `code` property:

| Code | When |
|------|------|
| `invalid_formula` | Expression cannot be parsed or is empty/too long |
| `invalid_input` | Non-string formula, null data, bad id/name, non-numeric field |
| `field_not_found` | A field path does not exist in the data snapshot |
| `division_by_zero` | Division by zero encountered during evaluation |
| `duplicate_id` | Attempting to save with an id that already exists |
| `not_found` | Attempting to update/evaluate a formula that does not exist |
| `limit_exceeded` | Exceeding the 100-formula storage limit |
| `storage_unavailable` | Reserved for environments where localStorage is missing |

```typescript
import { MetricBuilderError } from './src/lib/metricBuilder';

try {
evaluateFormula('account.missing / 0', snapshot);
} catch (err) {
if (err instanceof MetricBuilderError) {
switch (err.code) {
case 'field_not_found': /* guide user to pick a valid field */ break;
case 'division_by_zero': /* show a user-friendly message */ break;
default: console.error(err.message);
}
}
}
```

## Security Notes

- **No `eval()`**: Formulas are parsed with a hand-written recursive-descent
parser. Arbitrary code execution is not possible.
- **Field path validation**: Only alphanumeric characters, underscores, and
dots are allowed in identifiers. SQL injection, script injection, and
prototype pollution via field paths are blocked by the tokenizer.
- **Storage isolation**: Saved formulas are persisted under a dedicated
localStorage key (`stellar-dev-dashboard-saved-metric-formulas`) and
scoped to the origin.
- **Complexity limits**: Token count, formula length, and field depth are
capped to prevent denial-of-service via pathological expressions.

## Compatibility & Migration

- **Browser requirements**: Full functionality with any browser that
supports `localStorage`. Falls back to in-memory storage in SSR, Web
Workers, or privacy mode — formulas work but are not persisted across
page reloads.
- **Data source compatibility**: Works with `buildAnalyticsSnapshot()` from
`analytics.ts` and `ReportDataSet` from `customReports.ts`.
- **No breaking changes**: This is a new module. No existing API surfaces
or data schemas are modified.
- **TypeScript**: Fully typed with exported types for `SavedMetricFormula`,
`MetricEvaluationResult`, `MetricBuilderError`, and `MetricField`.

## Integration with Custom Reports

The metric builder extends the `customReports.ts` infrastructure. You can
use evaluated formulas as additional metric entries in report templates:

```typescript
import { evaluateFormula } from './src/lib/metricBuilder';
import { transformReportData } from './src/lib/customReports';

const reportData = transformReportData('account-activity', analyticsSnapshot);

// Add a custom metric to the report
const custom = evaluateFormula(
'transactions.successRate * 100',
analyticsSnapshot,
'number',
);
reportData.metrics.push({ label: 'Custom Success %', value: custom.formatted });
```
29 changes: 29 additions & 0 deletions PR_DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Summary

Implements cost attribution by application tag or memo prefix for project budgeting (#868). Attributes transaction fees (in Stroops and XLM) and payment transfer volumes to developer-defined project tags (e.g. `billing`, `auth`, `nft-drop`, `defi-swap`) via literal memo prefixes (e.g., `[APP:billing]`, `BILL:`) or custom regex patterns.

- **Domain Library (`src/lib/costThresholdManager.ts`)**: `CostThresholdManager` implementation for tag definition, memo prefix and regex pattern matching, budget limits, volume tracking, threshold alerts (`ok`, `warning` >= 80%, `exceeded` >= 100%, `unbudgeted`), auto-discovery of embedded tags (`[APP:tag]`), JSON/CSV report exports, and graceful SSR/storage error handling.
- **Analytics Integration (`src/lib/analytics.ts`)**: Embedded cost attribution calculations into `buildAnalyticsSnapshot()` and re-exported `CostThresholdManager`.
- **Documentation**: User-facing & developer guide in [`docs/features/COST_ATTRIBUTION_ANALYTICS.md`](docs/features/COST_ATTRIBUTION_ANALYTICS.md) and changelog entry in `CHANGELOG.md`.

Closes #868

## How was this tested?

Ran unit tests:
`pnpm run test:unit src/lib/__tests__/costThresholdManager.test.ts` (13 passed)

- **Primary flow**: Verified adding/updating rules, attributing fees and payment volumes to configured tags by memo prefix, auto-discovering unbudgeted tag prefixes, calculating fee sums in Stroops and XLM, average fee calculations, and JSON/CSV report exporting.
- **Boundary cases**: Verified empty transaction list handling, exact warning threshold calculation (80.0%), exact 100% budget cap exceedance, zero-fee transactions, and case-insensitive memo matching.
- **Failure cases**: Verified `null` / `undefined` / non-array transaction inputs, malformed transaction objects with invalid fee strings (`"invalid_number"`, negative fees), malformed regex rules (unclosed brackets), throwing `localStorage` security/quota errors in restricted SSR/iframe sandboxes, and missing required rule properties.

## Merge requirements

A PR is merged only when **every** box below is true. See
[Merge requirements](https://github.com/Nanle-code/stellar-dev-dashboard/blob/master/docs/contributing.md#merge-requirements) for the full policy.

- [x] All required CI checks pass on the latest commit (not just an earlier push).
- [x] No required checks are failing, pending, or skipped — re-run or fix them; do not ask for a merge while any are outstanding.
- [x] The branch has no merge conflicts with the target branch (rebase or merge `master` if GitHub shows "This branch has conflicts").
- [x] Tests were added or updated for the change (primary flow, a boundary case, and a failure case).
- [x] Docs were updated where behaviour, configuration, or security posture changed.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ This keeps user-specific and operational endpoints behind explicit authenticatio
- **Scheduled report delivery via webhooks (#869)** — authenticated HMAC/bearer delivery of analytics summaries with retries: [docs/features/report-webhook-delivery.md](docs/features/report-webhook-delivery.md).
- **Transaction Builder i18n (#878)** — complete locale coverage of builder strings across all nine languages: [docs/features/builder-i18n.md](docs/features/builder-i18n.md).
- **Mutation testing gate for fee math (#895)** — Stryker score gate on stroop conversion and fee estimation: [docs/features/mutation-testing-gate.md](docs/features/mutation-testing-gate.md). Run locally with `pnpm run test:mutation:feemath`.
- **Custom metric builder (#864)** — compose reusable metrics from Horizon fields and saved arithmetic formulas: [CUSTOM_METRIC_BUILDER_GUIDE.md](CUSTOM_METRIC_BUILDER_GUIDE.md).

## Canary Deployment Health Probes

Expand Down
Loading