From 6703b5f8d15169af360d88bc48eb507cddcdf329 Mon Sep 17 00:00:00 2001 From: wheval Date: Fri, 28 Aug 2026 22:35:39 +0100 Subject: [PATCH] contrib: add error codes, error reporting hook, latency instrumentation, and error rate monitor (#249, #251, #252, #254) --- .../README.md | 3 ++ .../centralized-error-reporting-hook.test.ts | 18 ++++++++ .../centralized-error-reporting-hook.ts | 19 ++++++++ .../rpc-elevated-error-rate-warning/README.md | 3 ++ .../rpc-elevated-error-rate-warning.test.ts | 19 ++++++++ .../rpc-elevated-error-rate-warning.ts | 46 +++++++++++++++++++ .../rpc-latency-instrumentation/README.md | 3 ++ .../rpc-latency-instrumentation.test.ts | 24 ++++++++++ .../rpc-latency-instrumentation.ts | 38 +++++++++++++++ .../standardized-error-codes/README.md | 3 ++ .../standardized-error-codes.test.ts | 25 ++++++++++ .../standardized-error-codes.ts | 37 +++++++++++++++ 12 files changed, 238 insertions(+) create mode 100644 contrib/examples/centralized-error-reporting-hook/README.md create mode 100644 contrib/examples/centralized-error-reporting-hook/centralized-error-reporting-hook.test.ts create mode 100644 contrib/examples/centralized-error-reporting-hook/centralized-error-reporting-hook.ts create mode 100644 contrib/examples/rpc-elevated-error-rate-warning/README.md create mode 100644 contrib/examples/rpc-elevated-error-rate-warning/rpc-elevated-error-rate-warning.test.ts create mode 100644 contrib/examples/rpc-elevated-error-rate-warning/rpc-elevated-error-rate-warning.ts create mode 100644 contrib/examples/rpc-latency-instrumentation/README.md create mode 100644 contrib/examples/rpc-latency-instrumentation/rpc-latency-instrumentation.test.ts create mode 100644 contrib/examples/rpc-latency-instrumentation/rpc-latency-instrumentation.ts create mode 100644 contrib/examples/standardized-error-codes/README.md create mode 100644 contrib/examples/standardized-error-codes/standardized-error-codes.test.ts create mode 100644 contrib/examples/standardized-error-codes/standardized-error-codes.ts diff --git a/contrib/examples/centralized-error-reporting-hook/README.md b/contrib/examples/centralized-error-reporting-hook/README.md new file mode 100644 index 0000000..9f8d025 --- /dev/null +++ b/contrib/examples/centralized-error-reporting-hook/README.md @@ -0,0 +1,3 @@ +# Centralized Error Reporting Hook (#251) + +Allows consumer applications to register an `onError` hook to capture and report uncaught SDK and RPC errors. diff --git a/contrib/examples/centralized-error-reporting-hook/centralized-error-reporting-hook.test.ts b/contrib/examples/centralized-error-reporting-hook/centralized-error-reporting-hook.test.ts new file mode 100644 index 0000000..61d8665 --- /dev/null +++ b/contrib/examples/centralized-error-reporting-hook/centralized-error-reporting-hook.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect, vi } from "vitest"; +import { ClientWithErrorReporting } from "./centralized-error-reporting-hook"; + +describe("Issue #251 — Centralized Error Reporting Hook", () => { + it("calls onError when an operation fails", async () => { + const onError = vi.fn(); + const client = new ClientWithErrorReporting(onError); + + await expect( + client.execute(async () => { + throw new Error("RPC failure"); + }, { method: "sendTransaction" }) + ).rejects.toThrow("RPC failure"); + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith(expect.any(Error), { method: "sendTransaction" }); + }); +}); diff --git a/contrib/examples/centralized-error-reporting-hook/centralized-error-reporting-hook.ts b/contrib/examples/centralized-error-reporting-hook/centralized-error-reporting-hook.ts new file mode 100644 index 0000000..77fadc5 --- /dev/null +++ b/contrib/examples/centralized-error-reporting-hook/centralized-error-reporting-hook.ts @@ -0,0 +1,19 @@ +/** + * Issue #251: Centralized Error Reporting Hook Interface. + */ + +export type ErrorReporterHook = (error: Error, context?: Record) => void; + +export class ClientWithErrorReporting { + constructor(private readonly onError?: ErrorReporterHook) {} + + async execute(fn: () => Promise, context?: Record): Promise { + try { + return await fn(); + } catch (err) { + const error = err instanceof Error ? err : new Error(String(err)); + this.onError?.(error, context); + throw error; + } + } +} diff --git a/contrib/examples/rpc-elevated-error-rate-warning/README.md b/contrib/examples/rpc-elevated-error-rate-warning/README.md new file mode 100644 index 0000000..4ad805a --- /dev/null +++ b/contrib/examples/rpc-elevated-error-rate-warning/README.md @@ -0,0 +1,3 @@ +# RPC Elevated Error Rate Warning Callback (#254) + +Monitors rolling RPC error rates and triggers an alerting callback when the failure threshold is exceeded. diff --git a/contrib/examples/rpc-elevated-error-rate-warning/rpc-elevated-error-rate-warning.test.ts b/contrib/examples/rpc-elevated-error-rate-warning/rpc-elevated-error-rate-warning.test.ts new file mode 100644 index 0000000..a25c09d --- /dev/null +++ b/contrib/examples/rpc-elevated-error-rate-warning/rpc-elevated-error-rate-warning.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect, vi } from "vitest"; +import { RpcErrorRateMonitor, type ErrorRateStats } from "./rpc-elevated-error-rate-warning"; + +describe("Issue #254 — Elevated Error Rate Warning", () => { + it("fires callback when error rate exceeds threshold", () => { + const callback = vi.fn(); + const monitor = new RpcErrorRateMonitor(0.3, 10, callback); + + for (let i = 0; i < 4; i++) { + monitor.record(true); + } + expect(callback).not.toHaveBeenCalled(); + + monitor.record(false); + monitor.record(false); // 2/6 = 33.3% > 30% + + expect(callback).toHaveBeenCalledTimes(1); + }); +}); diff --git a/contrib/examples/rpc-elevated-error-rate-warning/rpc-elevated-error-rate-warning.ts b/contrib/examples/rpc-elevated-error-rate-warning/rpc-elevated-error-rate-warning.ts new file mode 100644 index 0000000..a6a800c --- /dev/null +++ b/contrib/examples/rpc-elevated-error-rate-warning/rpc-elevated-error-rate-warning.ts @@ -0,0 +1,46 @@ +/** + * Issue #254: Configurable Elevated RPC Error Rate Callback. + */ + +export interface ErrorRateStats { + errorRate: number; + totalRequests: number; + failedRequests: number; + threshold: number; +} + +export type ElevatedErrorRateCallback = (stats: ErrorRateStats) => void; + +export class RpcErrorRateMonitor { + private readonly history: boolean[] = []; + + constructor( + private readonly threshold = 0.25, + private readonly windowSize = 20, + private readonly onElevatedErrorRate?: ElevatedErrorRateCallback + ) {} + + record(success: boolean): ErrorRateStats { + this.history.push(success); + if (this.history.length > this.windowSize) { + this.history.shift(); + } + + const totalRequests = this.history.length; + const failedRequests = this.history.filter((s) => !s).length; + const errorRate = totalRequests > 0 ? failedRequests / totalRequests : 0; + + const stats: ErrorRateStats = { + errorRate, + totalRequests, + failedRequests, + threshold: this.threshold, + }; + + if (totalRequests >= 5 && errorRate >= this.threshold) { + this.onElevatedErrorRate?.(stats); + } + + return stats; + } +} diff --git a/contrib/examples/rpc-latency-instrumentation/README.md b/contrib/examples/rpc-latency-instrumentation/README.md new file mode 100644 index 0000000..4f2202c --- /dev/null +++ b/contrib/examples/rpc-latency-instrumentation/README.md @@ -0,0 +1,3 @@ +# RPC Latency Instrumentation Hook (#252) + +Provides duration measurement and request completion hooks for RPC calls. diff --git a/contrib/examples/rpc-latency-instrumentation/rpc-latency-instrumentation.test.ts b/contrib/examples/rpc-latency-instrumentation/rpc-latency-instrumentation.test.ts new file mode 100644 index 0000000..3a94ce4 --- /dev/null +++ b/contrib/examples/rpc-latency-instrumentation/rpc-latency-instrumentation.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from "vitest"; +import { instrumentRpcCall, type RequestCompleteInfo } from "./rpc-latency-instrumentation"; + +describe("Issue #252 — RPC Latency Instrumentation", () => { + it("records duration for successful and failed calls", async () => { + const records: RequestCompleteInfo[] = []; + const hook = (info: RequestCompleteInfo) => records.push(info); + + await instrumentRpcCall("simulate", async () => "ok", hook); + expect(records).toHaveLength(1); + expect(records[0].method).toBe("simulate"); + expect(records[0].success).toBe(true); + + await expect( + instrumentRpcCall("sendTx", async () => { + throw new Error("fail"); + }, hook) + ).rejects.toThrow("fail"); + + expect(records).toHaveLength(2); + expect(records[1].method).toBe("sendTx"); + expect(records[1].success).toBe(false); + }); +}); diff --git a/contrib/examples/rpc-latency-instrumentation/rpc-latency-instrumentation.ts b/contrib/examples/rpc-latency-instrumentation/rpc-latency-instrumentation.ts new file mode 100644 index 0000000..120f1c8 --- /dev/null +++ b/contrib/examples/rpc-latency-instrumentation/rpc-latency-instrumentation.ts @@ -0,0 +1,38 @@ +/** + * Issue #252: RPC Latency Instrumentation Hook. + */ + +export interface RequestCompleteInfo { + method: string; + durationMs: number; + success: boolean; + error?: Error; +} + +export type RequestCompleteHook = (info: RequestCompleteInfo) => void; + +export async function instrumentRpcCall( + method: string, + fn: () => Promise, + hook?: RequestCompleteHook +): Promise { + const start = Date.now(); + try { + const result = await fn(); + hook?.({ + method, + durationMs: Date.now() - start, + success: true, + }); + return result; + } catch (err) { + const error = err instanceof Error ? err : new Error(String(err)); + hook?.({ + method, + durationMs: Date.now() - start, + success: false, + error, + }); + throw error; + } +} diff --git a/contrib/examples/standardized-error-codes/README.md b/contrib/examples/standardized-error-codes/README.md new file mode 100644 index 0000000..678622e --- /dev/null +++ b/contrib/examples/standardized-error-codes/README.md @@ -0,0 +1,3 @@ +# Standardized SDK Error Codes (#249) + +Defines a standardized error code naming convention across all SDK modules following the format `VELLAR__`. diff --git a/contrib/examples/standardized-error-codes/standardized-error-codes.test.ts b/contrib/examples/standardized-error-codes/standardized-error-codes.test.ts new file mode 100644 index 0000000..9f6b79e --- /dev/null +++ b/contrib/examples/standardized-error-codes/standardized-error-codes.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from "vitest"; +import { VellarError, VellarErrorCode } from "./standardized-error-codes"; + +describe("Issue #249 — Standardized Error Codes", () => { + it("enforces standardized naming format VELLAR__", () => { + const regex = /^VELLAR_[A-Z0-9]+_[A-Z0-9_]+$/; + for (const code of Object.values(VellarErrorCode)) { + expect(code).toMatch(regex); + } + }); + + it("constructs VellarError with code, message, and details", () => { + const err = new VellarError( + VellarErrorCode.RPC_RATE_LIMIT_EXCEEDED, + "Too many requests sent to Soroban RPC", + { retryAfterMs: 5000 } + ); + + expect(err).toBeInstanceOf(Error); + expect(err).toBeInstanceOf(VellarError); + expect(err.code).toBe("VELLAR_RPC_RATE_LIMIT_EXCEEDED"); + expect(err.message).toContain("[VELLAR_RPC_RATE_LIMIT_EXCEEDED]"); + expect(err.details?.retryAfterMs).toBe(5000); + }); +}); diff --git a/contrib/examples/standardized-error-codes/standardized-error-codes.ts b/contrib/examples/standardized-error-codes/standardized-error-codes.ts new file mode 100644 index 0000000..0f1793f --- /dev/null +++ b/contrib/examples/standardized-error-codes/standardized-error-codes.ts @@ -0,0 +1,37 @@ +/** + * Issue #249: Standardized SDK Error Codes. + * Naming convention: VELLAR__ + */ + +export const VellarErrorCode = { + AUTH_SESSION_EXPIRED: "VELLAR_AUTH_SESSION_EXPIRED", + AUTH_BROWSER_REQUIRED: "VELLAR_AUTH_BROWSER_REQUIRED", + AUTH_INVALID_CREDENTIALS: "VELLAR_AUTH_INVALID_CREDENTIALS", + AUTH_CHALLENGE_REPLAYED: "VELLAR_AUTH_CHALLENGE_REPLAYED", + RPC_RATE_LIMIT_EXCEEDED: "VELLAR_RPC_RATE_LIMIT_EXCEEDED", + RPC_ENDPOINT_FAILED: "VELLAR_RPC_ENDPOINT_FAILED", + RPC_TIMEOUT: "VELLAR_RPC_TIMEOUT", + RPC_INVALID_NETWORK: "VELLAR_RPC_INVALID_NETWORK", + CONFIG_INVALID: "VELLAR_CONFIG_INVALID", + VALIDATION_INVALID_ADDRESS: "VELLAR_VALIDATION_INVALID_ADDRESS", + VALIDATION_UNTRUSTED_VECTOR: "VELLAR_VALIDATION_UNTRUSTED_VECTOR", + X402_NOT_CONFIGURED: "VELLAR_X402_NOT_CONFIGURED", + X402_PAYMENT_REJECTED: "VELLAR_X402_PAYMENT_REJECTED", + X402_INVALID_CHALLENGE: "VELLAR_X402_INVALID_CHALLENGE", + X402_SIGNER_UNAUTHORIZED: "VELLAR_X402_SIGNER_UNAUTHORIZED", +} as const; + +export type VellarErrorCodeType = (typeof VellarErrorCode)[keyof typeof VellarErrorCode]; + +export class VellarError extends Error { + public readonly code: VellarErrorCodeType; + public readonly details?: Record; + + constructor(code: VellarErrorCodeType, message: string, details?: Record) { + super(`[${code}] ${message}`); + this.name = "VellarError"; + this.code = code; + this.details = details; + Object.setPrototypeOf(this, new.target.prototype); + } +}