@theholocron/http-client 1.15.1 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,16 +28,39 @@ const data = await client.request<{ id: string }>("/widgets/42");
28
28
 
29
29
  **Config options:**
30
30
 
31
- | Option | Type | Default | Description |
32
- | -------------- | ------------------------ | ------------------ | ------------------------------------------------ |
33
- | `baseUrl` | `string` | — | Base URL, trailing slashes trimmed automatically |
34
- | `token` | `string` | — | Auth token |
35
- | `tokenScheme` | `"bearer" \| "apikey"` | `"bearer"` | How the token is sent |
36
- | `apiKeyHeader` | `string` | `"x-api-key"` | Header name when `tokenScheme` is `"apikey"` |
37
- | `extraHeaders` | `Record<string, string>` | — | Static headers merged into every request |
38
- | `defaultQuery` | `Record<string, string>` | — | Query params appended to every request URL |
39
- | `vendor` | `string` | — | Vendor label for error messages |
40
- | `fetch` | `typeof fetch` | `globalThis.fetch` | Override fetch for testing |
31
+ | Option | Type | Default | Description |
32
+ | -------------- | ------------------------ | ------------------ | ------------------------------------------------- |
33
+ | `baseUrl` | `string` | — | Base URL, trailing slashes trimmed automatically |
34
+ | `token` | `string` | — | Auth token |
35
+ | `tokenScheme` | `"bearer" \| "apikey"` | `"bearer"` | How the token is sent |
36
+ | `apiKeyHeader` | `string` | `"x-api-key"` | Header name when `tokenScheme` is `"apikey"` |
37
+ | `extraHeaders` | `Record<string, string>` | — | Static headers merged into every request |
38
+ | `defaultQuery` | `Record<string, string>` | — | Query params appended to every request URL |
39
+ | `vendor` | `string` | — | Vendor label for error messages |
40
+ | `fetch` | `typeof fetch` | `globalThis.fetch` | Override fetch for testing |
41
+ | `logger` | `Logger` | no-op | Structured request/response diagnostics (`debug`) |
42
+ | `errors` | `ErrorSink` | no-op | Reports transport failures + unexpected 5xx |
43
+
44
+ **Observability seam.** `logger` and `errors` accept the `Logger` / `ErrorSink`
45
+ interfaces from [`@theholocron/observability/core`](https://github.com/theholocron/observability)
46
+ — a seam, not a runtime: the library never calls `Sentry.init` or reads
47
+ credentials itself, and both default to a silent no-op when omitted.
48
+
49
+ ```ts
50
+ import { SentrySink } from "@theholocron/observability/errors";
51
+ import { createLogger } from "@theholocron/observability/logger";
52
+
53
+ const { logger } = createLogger({ level: "info" });
54
+ const errors = new SentrySink();
55
+ errors.init({ dsn: process.env.SENTRY_DSN!, release: "my-app@1.0.0", environment: "local", tags: {} });
56
+
57
+ const client = createRestClient({ baseUrl: "...", token: "...", logger, errors });
58
+ ```
59
+
60
+ `errors.captureException` only fires for a transport failure (network error —
61
+ `status: 0`) or an unexpected 5xx; a 4xx is left unreported since those are
62
+ typically expected and already handled by the caller (a 404 from a
63
+ "does this exist" check, etc.).
41
64
 
42
65
  ### `createResolveToken(config)`
43
66
 
package/dist/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ErrorSink, Logger } from "@theholocron/observability/core";
1
2
  //#region src/auth-resolver.d.ts
2
3
  declare class AuthError extends Error {
3
4
  name: string;
@@ -79,6 +80,20 @@ interface RestClientConfig {
79
80
  vendor?: string;
80
81
  /** Override `fetch` for tests. Defaults to `globalThis.fetch`. */
81
82
  fetch?: typeof fetch;
83
+ /**
84
+ * Structured logger for request/response diagnostics (`debug` level).
85
+ * Defaults to a no-op — the seam, not a runtime. Bring your own
86
+ * `@theholocron/observability` `Logger` (or any structurally-compatible
87
+ * one) to see it.
88
+ */
89
+ logger?: Logger;
90
+ /**
91
+ * Reports transport failures (network errors, `status: 0`) and unexpected
92
+ * 5xx responses. 4xx is left unreported — those are typically expected /
93
+ * handled by the caller (a 404 from a "does this exist" check, etc.).
94
+ * Defaults to a no-op.
95
+ */
96
+ errors?: ErrorSink;
82
97
  }
83
98
  interface RequestOptions {
84
99
  method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { NoopErrorSink, NoopLogger } from "@theholocron/observability/core";
1
2
  //#region src/auth-resolver.ts
2
3
  var AuthError = class extends Error {
3
4
  name = "AuthError";
@@ -49,6 +50,8 @@ var ProviderApiError = class extends Error {
49
50
  function createRestClient(config) {
50
51
  const fetchImpl = config.fetch ?? globalThis.fetch;
51
52
  const vendor = config.vendor ?? "";
53
+ const logger = config.logger ?? new NoopLogger();
54
+ const errors = config.errors ?? new NoopErrorSink();
52
55
  let baseUrl = config.baseUrl;
53
56
  while (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
54
57
  const staticHeaders = { accept: "application/json" };
@@ -72,16 +75,31 @@ function createRestClient(config) {
72
75
  init.body = JSON.stringify(opts.body);
73
76
  }
74
77
  const tag = vendor ? `${vendor} ${init.method} ${path}` : `${init.method} ${path}`;
78
+ logger.debug({
79
+ vendor,
80
+ method: init.method,
81
+ path
82
+ }, "request");
75
83
  let res;
76
84
  try {
77
85
  res = await fetchImpl(url.toString(), init);
78
86
  } catch (err) {
79
- throw new ProviderApiError(`${tag} failed: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`, 0, void 0);
87
+ const apiErr = new ProviderApiError(`${tag} failed: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`, 0, void 0);
88
+ errors.captureException(apiErr);
89
+ throw apiErr;
80
90
  }
81
91
  if (!res.ok) {
82
92
  const body = await res.text().catch(() => "");
83
- throw new ProviderApiError(`${tag} → ${res.status}`, res.status, body);
93
+ const apiErr = new ProviderApiError(`${tag} → ${res.status}`, res.status, body);
94
+ if (res.status >= 500) errors.captureException(apiErr);
95
+ throw apiErr;
84
96
  }
97
+ logger.debug({
98
+ vendor,
99
+ method: init.method,
100
+ path,
101
+ status: res.status
102
+ }, "response");
85
103
  if (opts.expectNoContent || res.status === 204) return void 0;
86
104
  const text = await res.text();
87
105
  if (!text) return void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/http-client",
3
- "version": "1.15.1",
3
+ "version": "1.16.0",
4
4
  "description": "Shared HTTP primitives for theholocron — REST client factory, auth resolver, and error types",
5
5
  "keywords": [
6
6
  "typescript",
@@ -35,6 +35,9 @@
35
35
  "files": [
36
36
  "dist"
37
37
  ],
38
+ "dependencies": {
39
+ "@theholocron/observability": "^0.3.0"
40
+ },
38
41
  "devDependencies": {
39
42
  "@theholocron/eslint-config": "^8.2.0",
40
43
  "@theholocron/tsconfig": "^8.2.0",