Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/structured-observability-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@http-client-toolkit/core': minor
---

Add structured observability lifecycle events for requests, cache, dedupe, rate limiting, server cooldowns, and retries via `HttpClientOptions.observability.onEvent`.
163 changes: 115 additions & 48 deletions docs-site/src/content/docs/api/http-client.mdx

Large diffs are not rendered by default.

32 changes: 16 additions & 16 deletions docs-site/src/content/docs/api/utilities.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ title: Utilities
description: API reference for hashRequest, HttpClientError, and other utilities
---


## hashRequest

Generates deterministic SHA-256 hashes for cache and deduplication keys.
Expand Down Expand Up @@ -35,9 +34,9 @@ import { HttpClientError } from '@http-client-toolkit/core';

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `message` | `string` | Error description |
| Property | Type | Description |
| ------------ | --------------------- | ------------------------------------ |
| `message` | `string` | Error description |
| `statusCode` | `number \| undefined` | HTTP status code (e.g. `404`, `500`) |

### Usage
Expand All @@ -47,7 +46,7 @@ try {
await client.get(url);
} catch (error) {
if (error instanceof HttpClientError) {
console.log(error.message); // "Not Found"
console.log(error.message); // "Not Found"
console.log(error.statusCode); // 404
}
}
Expand All @@ -57,14 +56,15 @@ try {

The `@http-client-toolkit/core` package exports:

| Export | Type | Description |
|--------|------|-------------|
| `HttpClient` | Class | Main client class |
| `HttpClientError` | Class | Error class with `statusCode` |
| `hashRequest` | Function | Deterministic SHA-256 request hashing |
| `HttpClientCacheOptions` | Interface | Cache configuration for `HttpClientOptions.cache` |
| `HttpClientRateLimitOptions` | Interface | Rate limit configuration for `HttpClientOptions.rateLimit` |
| `CacheStore` | Interface | Cache store contract |
| `DedupeStore` | Interface | Deduplication store contract |
| `RateLimitStore` | Interface | Rate limit store contract |
| `AdaptiveRateLimitStore` | Interface | Adaptive rate limit store contract |
| Export | Type | Description |
| ---------------------------- | --------- | ------------------------------------------------------------------- |
| `HttpClient` | Class | Main client class |
| `HttpClientError` | Class | Error class with `statusCode` |
| `HttpClientEvent` | Type | Stable structured lifecycle event union for `observability.onEvent` |
| `hashRequest` | Function | Deterministic SHA-256 request hashing |
| `HttpClientCacheOptions` | Interface | Cache configuration for `HttpClientOptions.cache` |
| `HttpClientRateLimitOptions` | Interface | Rate limit configuration for `HttpClientOptions.rateLimit` |
| `CacheStore` | Interface | Cache store contract |
| `DedupeStore` | Interface | Deduplication store contract |
| `RateLimitStore` | Interface | Rate limit store contract |
| `AdaptiveRateLimitStore` | Interface | Adaptive rate limit store contract |
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/dashboard/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,4 @@ The dashboard exposes a REST API that the React SPA consumes. All store endpoint

## Future Improvements

- **Retry metrics** — Show requests that required retries and their success rate. The core `HttpClient` retry loop currently doesn't persist metrics, so this would require adding internal retry tracking (total attempts, successes after retry, final failures) and exposing it via a new dashboard adapter.
- **Retry metrics** — Show requests that required retries and their success rate by collecting `retry:scheduled`, `retry:exhausted`, `request:success`, and `request:error` events from `HttpClient` observability and exposing aggregated metrics through a dashboard adapter.
77 changes: 61 additions & 16 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,23 @@ Create a thin wrapper module per third-party API so callers don't configure anyt

**Constructor options:**

| Property | Type | Default | Description |
| --------------------- | ------------------------------------------ | ---------- | -------------------------------------------------- |
| `name` | `string` | required | Name for the client instance |
| `cache` | `CacheStore` | - | Response caching |
| `dedupe` | `DedupeStore` | - | Request deduplication |
| `rateLimit` | `RateLimitStore \| AdaptiveRateLimitStore` | - | Rate limiting |
| `cacheTTL` | `number` | `3600` | Cache TTL when response has no headers |
| `throwOnRateLimit` | `boolean` | `true` | Throw when rate limited vs. wait |
| `maxWaitTime` | `number` | `60000` | Max wait time (ms) before throwing |
| `responseTransformer` | `(data: unknown) => unknown` | - | Transform raw response data |
| `responseHandler` | `(data: unknown) => unknown` | - | Validate/process transformed data |
| `errorHandler` | `(error: unknown) => Error` | - | Convert errors to domain-specific types |
| `cacheOverrides` | `CacheOverrideOptions` | - | Override cache header behaviors |
| `retry` | `RetryOptions \| false` | - | Retry config; `false` disables globally |
| `rateLimitHeaders` | `RateLimitHeaderConfig` | defaults | Configure standard/custom header names |
| `resourceKeyResolver` | `(url: string) => string` | URL origin | Customize how rate-limit resource keys are derived |
| Property | Type | Default | Description |
| --------------------- | ----------------------------------------------------------------- | ---------- | -------------------------------------------------- |
| `name` | `string` | required | Name for the client instance |
| `cache` | `CacheStore` | - | Response caching |
| `dedupe` | `DedupeStore` | - | Request deduplication |
| `rateLimit` | `RateLimitStore \| AdaptiveRateLimitStore` | - | Rate limiting |
| `cacheTTL` | `number` | `3600` | Cache TTL when response has no headers |
| `throwOnRateLimit` | `boolean` | `true` | Throw when rate limited vs. wait |
| `maxWaitTime` | `number` | `60000` | Max wait time (ms) before throwing |
| `responseTransformer` | `(data: unknown) => unknown` | - | Transform raw response data |
| `responseHandler` | `(data: unknown) => unknown` | - | Validate/process transformed data |
| `errorHandler` | `(error: unknown) => Error` | - | Convert errors to domain-specific types |
| `cacheOverrides` | `CacheOverrideOptions` | - | Override cache header behaviors |
| `retry` | `RetryOptions \| false` | - | Retry config; `false` disables globally |
| `rateLimitHeaders` | `RateLimitHeaderConfig` | defaults | Configure standard/custom header names |
| `resourceKeyResolver` | `(url: string) => string` | URL origin | Customize how rate-limit resource keys are derived |
| `observability` | `{ onEvent?: (event: HttpClientEvent) => void \| Promise<void> }` | - | Subscribe to structured lifecycle events |

### Request Flow

Expand All @@ -108,6 +109,49 @@ Create a thin wrapper module per third-party API so callers don't configure anyt
5. **Transform & Validate** - Apply `responseTransformer` then `responseHandler`
6. **Store** - Cache the result, record the rate limit hit, and resolve any deduplicated waiters

### Observability

Use `observability.onEvent` to collect structured lifecycle events without wrapping internal stores or fetch calls:

```typescript
import { HttpClient, type HttpClientEvent } from '@http-client-toolkit/core';

const client = new HttpClient({
name: 'catalog-api',
observability: {
onEvent(event: HttpClientEvent) {
logger.info({ event }, event.type);

if (event.type === 'request:success') {
metrics.histogram('http_client_duration_ms', event.durationMs, {
clientName: event.clientName,
status: String(event.status ?? 'unknown'),
});
}
},
},
});
```

Events cover request start/success/error, cache hits/misses/stale/revalidation,
dedupe ownership and joins, rate-limit waits/throws, server cooldown updates,
and retry scheduling/exhaustion. Payloads include stable public fields such as
`clientName`, `requestId`, `url`, `method`, `resourceKey`, `timestamp`,
`attempt`, `durationMs`, `status`, `error`, `cacheKey`, and `waitMs` where they
apply.

Attempt numbers are emitted on retry events (`retry:scheduled` and
`retry:exhausted`). Final `request:success` and `request:error` events describe
the logical request outcome and do not include a fetch attempt count.

Bridge `onEvent` to your logger, metrics client, or OpenTelemetry instrumentation
by translating these events into log records, counters, histograms, span events,
or attributes. OpenTelemetry is intentionally not a dependency of core.

Observer return values are ignored, observer errors are swallowed, and returned
promises are not awaited; observers cannot change the request result or thrown
error. Keep synchronous observer work lightweight because it runs inline.

### Error Handling

All HTTP errors are wrapped in `HttpClientError`:
Expand Down Expand Up @@ -184,6 +228,7 @@ compatibility.

- `HttpClient` - Main client class
- `HttpClientError` - Error class with `statusCode`
- `HttpClientEvent` - Stable structured lifecycle event union
- `hashRequest` - Deterministic SHA-256 request hashing
- Store interfaces: `CacheStore`, `DedupeStore`, `RateLimitStore`, `AdaptiveRateLimitStore`

Expand Down
Loading