@richardmcquiston01/error-log-adapter 0.4.0 → 0.5.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 +132 -13
- package/dist/index.cjs +650 -234
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +224 -22
- package/dist/index.d.ts +224 -22
- package/dist/index.mjs +649 -234
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
## Overview
|
|
11
11
|
|
|
12
|
-
A small, dependency-free TypeScript library for routing runtime errors to the sink of your choice. Code against a single ErrorLogAdapter interface, then swap between console, webhook,
|
|
12
|
+
A small, dependency-free TypeScript library for routing runtime errors to the sink of your choice. Code against a single `ErrorLogAdapter` interface, then swap between console, webhook, files, databases, queues, object storage, browser storage, or your own custom adapter without changing application code. Adapters never throw: a failing sink is caught and reported through the returned result, so error logging can't take down your app. Extracted from [chat-and-react](https://github.com/RichardMcQuiston01/chat-and-react).
|
|
13
|
+
|
|
14
|
+
The package imports no database or cloud SDKs. Each adapter takes a tiny structural interface (such as `{ query(sql, params) }`), which the usual client (`pg`, `mysql2`, `node-redis`, the AWS SDK, `firebase-admin`, `supabase-js`, a MongoDB `Collection`) already satisfies, so you bring your own client and version.
|
|
13
15
|
|
|
14
16
|
[Back to Top](#table-of-contents)
|
|
15
17
|
|
|
@@ -17,7 +19,7 @@ A small, dependency-free TypeScript library for routing runtime errors to the si
|
|
|
17
19
|
|
|
18
20
|
### Prerequisites
|
|
19
21
|
|
|
20
|
-
- Node.js 18 or later
|
|
22
|
+
- Node.js 18 or later, Bun, or a browser. Adapters that need `fetch`, `crypto` or the file system use the runtime's built-ins.
|
|
21
23
|
- TypeScript 5 or later (optional)
|
|
22
24
|
|
|
23
25
|
### Installation
|
|
@@ -54,15 +56,6 @@ if (!result.ok) {
|
|
|
54
56
|
}
|
|
55
57
|
```
|
|
56
58
|
|
|
57
|
-
Available adapters:
|
|
58
|
-
|
|
59
|
-
| Adapter | Sink |
|
|
60
|
-
| ----------------------- | ---------------------------------------------------------------------------------- |
|
|
61
|
-
| `ConsoleErrorAdapter` | `console.error` |
|
|
62
|
-
| `WebhookErrorAdapter` | HTTP `POST` of the entry as JSON |
|
|
63
|
-
| `MongoDbErrorAdapter` | Any object with `insertOne`, such as a MongoDB `Collection` (no driver dependency) |
|
|
64
|
-
| `CompositeErrorAdapter` | Several adapters at once; succeeds only if all succeed |
|
|
65
|
-
|
|
66
59
|
`ErrorLogEntry` is generic over the error code, so each project can restrict codes to its own union:
|
|
67
60
|
|
|
68
61
|
```ts
|
|
@@ -71,20 +64,114 @@ const errorLog: ErrorLogAdapter<AppErrorCode> =
|
|
|
71
64
|
new ConsoleErrorAdapter<AppErrorCode>();
|
|
72
65
|
```
|
|
73
66
|
|
|
67
|
+
Constructor options are checked when the adapter is created and throw a descriptive `RangeError` for a bad value (an invalid table name, a non-positive limit). `log()` itself never throws.
|
|
68
|
+
|
|
69
|
+
### Adapters
|
|
70
|
+
|
|
71
|
+
Every adapter takes an optional `prefix` (default `[error-log-adapter]`) that is printed before its failure messages.
|
|
72
|
+
|
|
73
|
+
| Adapter | Sink | Runs in | Main options |
|
|
74
|
+
| ----------------------------------- | --------------------------------------------------------- | ------------- | ------------------------------------------------ |
|
|
75
|
+
| `ConsoleErrorAdapter` | `console.error` | Anywhere | |
|
|
76
|
+
| `WebhookErrorAdapter(url)` | HTTP `POST` of the entry as JSON | Anywhere | `headers`, `timeoutMs`, `allowInsecure` |
|
|
77
|
+
| `MongoDbErrorAdapter(collection)` | Anything with `insertOne`, such as a MongoDB `Collection` | Node | `timeoutMs` |
|
|
78
|
+
| `PostgresErrorAdapter(client)` | A Postgres table, through `pg` | Node | `tableName`, `createTable`, `timeoutMs` |
|
|
79
|
+
| `MySqlErrorAdapter(client)` | A MySQL / MariaDB table, through `mysql2/promise` | Node | `tableName`, `createTable`, `timeoutMs` |
|
|
80
|
+
| `SqliteErrorAdapter(client)` | A SQLite table, through any driver | Node, Bun | `tableName`, `createTable`, `timeoutMs` |
|
|
81
|
+
| `SupabaseErrorAdapter(client)` | A Supabase table, through `supabase-js` | Anywhere | `tableName`, `timeoutMs` |
|
|
82
|
+
| `RedisErrorAdapter(client)` | A Redis list or stream | Node | `mode`, `key`, `maxEntries`, `side`, `timeoutMs` |
|
|
83
|
+
| `S3ErrorAdapter(client, bucket)` | S3, DigitalOcean Spaces, R2, MinIO objects | Node | `keyPrefix`, `batch`, `contentType`, `timeoutMs` |
|
|
84
|
+
| `FirestoreErrorAdapter(collection)` | A Firestore collection | Node, browser | `timeoutMs` |
|
|
85
|
+
| `RealtimeDatabaseErrorAdapter(ref)` | A Realtime Database reference | Node, browser | `timeoutMs` |
|
|
86
|
+
| `FlatFileErrorAdapter(path)` | One text line per entry | Node, Bun | `formatLine`, `maxBytes`, `maxFiles` |
|
|
87
|
+
| `JsonFileErrorAdapter(path)` | NDJSON lines or one JSON array | Node, Bun | `mode`, `maxEntries`, `indent` |
|
|
88
|
+
| `IndexedDbErrorAdapter(database)` | An IndexedDB object store | Browser | `storeName`, `timeoutMs` |
|
|
89
|
+
| `WebStorageErrorAdapter(storage)` | `sessionStorage` or `localStorage`, as one capped array | Browser | `key`, `maxEntries`, `locks` |
|
|
90
|
+
| `CompositeErrorAdapter([...])` | Several adapters at once; succeeds only if all succeed | Anywhere | `timeoutMs` |
|
|
91
|
+
|
|
92
|
+
Every stored record has the same shape: a UUID `id`, `code`, `message`, `timestamp` (ISO 8601), and optional `source` and `cause`. An `Error` cause is stored as `{ name, message, stack }`. `serializeEntry` and `serializeCause` are exported if you build your own sink.
|
|
93
|
+
|
|
94
|
+
What changes durability or size, by adapter:
|
|
95
|
+
|
|
96
|
+
- **SQL adapters** (`createTable`): the table is created on first use by default. Set `createTable: false` and run the statements from `buildErrorLogTableDdl('postgres' | 'mysql')` (or `buildCreateErrorLogTableSql()` for SQLite) yourself if the application's database user should not change the schema. Table names are checked to be plain identifiers. A MySQL table made by 0.4.0 or earlier has `VARCHAR(255)` columns for `code` and `source`; run the statement from `buildMySqlWidenColumnsSql()` once to widen them.
|
|
97
|
+
- **Supabase**: create the table once with `buildSupabaseSetupSql()`, which also turns on row level security. Log from a server with the service role key, which bypasses RLS and must never reach a browser. A browser-held key needs an insert policy: pass `insertRoles: ['authenticated']` to the same function. The policy limits the size of each field but cannot tie a row to a user, so a holder of that key can still add entries of their own. Listing `anon` or `public` throws unless you also pass `allowPublicInsert: true`, because the anon key is public.
|
|
98
|
+
- **S3** (`batch`): with `batch: { batchSize, maxWaitMs }`, entries are buffered and uploaded together as one NDJSON object. `log()` then returns `ok` as soon as the entry is buffered, so call `flush()` before the process exits.
|
|
99
|
+
- **Files** (`maxBytes` / `maxFiles`, `maxEntries`): the flat file adapter rotates by size; the JSON file adapter's `array` mode keeps the newest `maxEntries`.
|
|
100
|
+
- **Redis and Web Storage** (`maxEntries`): the oldest entries are dropped past the cap.
|
|
101
|
+
- **Browser storage**: `sessionStorage` belongs to one tab. `localStorage` is shared by every tab and has no atomic update, so two tabs logging at the same instant can overwrite each other's entry. `WebStorageErrorAdapter` takes a `navigator.locks` lock around each write, which reduces the loss but cannot remove it, since tabs in separate processes can still read a stale copy. Use `IndexedDbErrorAdapter` when every entry matters. Create its object store from your `onupgradeneeded` handler with `createErrorLogStore`. A stored value the adapter cannot read is copied to `<key>.corrupt` before it is replaced.
|
|
102
|
+
- **Webhook**: the URL must be `https://` (or `http://localhost`, `127.0.0.1`, `[::1]`) unless you pass `allowInsecure: true`, because entries carry messages and stack traces. Redirects are not followed, since a redirect would turn the `POST` into a body-less `GET` or re-send the entry and your headers to another host; a redirect is reported as `ok: false`. Failure messages contain only the origin, never the path or query, because webhook URLs often hold a secret.
|
|
103
|
+
|
|
104
|
+
### When a sink is slow or down
|
|
105
|
+
|
|
106
|
+
`log()` never rejects, and it does not wait forever either. Every adapter that talks over a network or to a driver takes `timeoutMs` (default `10000`; `Infinity` waits without limit). When it passes, `log()` returns `{ ok: false }` with a message such as `Postgres did not respond within 10000 ms, so the entry may or may not have been stored`. Drivers cannot cancel a call, so the write may still finish afterwards (the webhook is the exception: its request is aborted). `CompositeErrorAdapter` has its own per-adapter `timeoutMs` (default `30000`), so one hung custom adapter cannot hold back the others.
|
|
107
|
+
|
|
108
|
+
Because of that, a failed call is not proof that nothing was stored. To retry safely, give the entry an `id`:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const entry = {
|
|
112
|
+
id: crypto.randomUUID(), // a UUID; anything else makes log() return ok: false
|
|
113
|
+
timestamp: new Date().toISOString(),
|
|
114
|
+
code: 'PAYMENT_FAILED',
|
|
115
|
+
message: 'Card was declined by the processor',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
let result = await errorLog.log(entry);
|
|
119
|
+
if (!result.ok) {
|
|
120
|
+
result = await errorLog.log(entry); // same id: a repeat is stored once
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Without an `id`, every call to `log()` gets a new random one, so logging twice stores two records. With one:
|
|
125
|
+
|
|
126
|
+
| Adapter | A repeat of the same `id` |
|
|
127
|
+
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
128
|
+
| Postgres, MySQL, SQLite | Stored once (`ON CONFLICT` / `ON DUPLICATE KEY`) |
|
|
129
|
+
| MongoDB, Supabase | Stored once; the duplicate-key error counts as success |
|
|
130
|
+
| Firestore | Overwrites the same document |
|
|
131
|
+
| Realtime Database | Overwrites the same key. Needs `child(key).set(value)` on the reference you pass in; without it `log()` says so |
|
|
132
|
+
| IndexedDB, Web Storage, JSON file (`array` mode) | Replaces the earlier record |
|
|
133
|
+
| S3 | Overwrites the same object only if the retry also has the same `timestamp` (the key contains both) |
|
|
134
|
+
| Webhook | The `id` is added to the JSON body so the receiver can drop repeats |
|
|
135
|
+
| Redis, flat file, JSON file (`ndjson` mode), console | Cannot detect a repeat |
|
|
136
|
+
|
|
137
|
+
`CompositeErrorAdapter` passes the entry through unchanged, so every sink sees the same `id`.
|
|
138
|
+
|
|
139
|
+
### What gets stored for a `cause`
|
|
140
|
+
|
|
141
|
+
`serializeCause` turns any cause into something `JSON.stringify` always accepts, so an unusual cause can never stop an entry from being stored. An `Error` becomes `{ name, message, stack }` plus its own `cause` and, for an `AggregateError`, `errors`, recursively. Circular references become `"[Circular]"`, nesting deeper than 6 levels becomes `"[Truncated]"`, and `BigInt`, `Symbol`, functions and non-finite numbers become strings.
|
|
142
|
+
|
|
74
143
|
### Examples
|
|
75
144
|
|
|
76
|
-
|
|
145
|
+
Write to files, a database and the console at once:
|
|
77
146
|
|
|
78
147
|
```ts
|
|
148
|
+
import { Pool } from 'pg';
|
|
79
149
|
import {
|
|
80
150
|
CompositeErrorAdapter,
|
|
81
151
|
ConsoleErrorAdapter,
|
|
152
|
+
FlatFileErrorAdapter,
|
|
153
|
+
PostgresErrorAdapter,
|
|
154
|
+
} from '@richardmcquiston01/error-log-adapter';
|
|
155
|
+
|
|
156
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
157
|
+
|
|
158
|
+
const errorLog = new CompositeErrorAdapter([
|
|
159
|
+
new ConsoleErrorAdapter(),
|
|
160
|
+
new FlatFileErrorAdapter('logs/errors.log', { maxBytes: 5_000_000 }),
|
|
161
|
+
new PostgresErrorAdapter(pool, { createTable: false }),
|
|
162
|
+
]);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Send errors to a webhook and MongoDB:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import {
|
|
169
|
+
CompositeErrorAdapter,
|
|
82
170
|
MongoDbErrorAdapter,
|
|
83
171
|
WebhookErrorAdapter,
|
|
84
172
|
} from '@richardmcquiston01/error-log-adapter';
|
|
85
173
|
|
|
86
174
|
const errorLog = new CompositeErrorAdapter([
|
|
87
|
-
new ConsoleErrorAdapter(),
|
|
88
175
|
new WebhookErrorAdapter('https://example.com/errors', {
|
|
89
176
|
headers: { 'X-Api-Key': process.env.ERROR_WEBHOOK_KEY ?? '' },
|
|
90
177
|
}),
|
|
@@ -92,6 +179,36 @@ const errorLog = new CompositeErrorAdapter([
|
|
|
92
179
|
]);
|
|
93
180
|
```
|
|
94
181
|
|
|
182
|
+
Keep the last 200 errors in the browser:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { WebStorageErrorAdapter } from '@richardmcquiston01/error-log-adapter';
|
|
186
|
+
|
|
187
|
+
const errorLog = new WebStorageErrorAdapter(window.localStorage, {
|
|
188
|
+
maxEntries: 200,
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Upload to a DigitalOcean Space in batches:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
196
|
+
import { S3ErrorAdapter } from '@richardmcquiston01/error-log-adapter';
|
|
197
|
+
|
|
198
|
+
const s3 = new S3Client({
|
|
199
|
+
region: 'nyc3',
|
|
200
|
+
endpoint: 'https://nyc3.digitaloceanspaces.com',
|
|
201
|
+
credentials: { accessKeyId, secretAccessKey },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const errorLog = new S3ErrorAdapter(s3, 'my-space', {
|
|
205
|
+
keyPrefix: 'logs/app/',
|
|
206
|
+
batch: { batchSize: 50, maxWaitMs: 30_000 },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
process.on('beforeExit', () => void errorLog.flush());
|
|
210
|
+
```
|
|
211
|
+
|
|
95
212
|
Write your own adapter by implementing the interface. Catch failures and return them rather than throwing:
|
|
96
213
|
|
|
97
214
|
```ts
|
|
@@ -120,6 +237,8 @@ class SlackErrorAdapter implements ErrorLogAdapter {
|
|
|
120
237
|
}
|
|
121
238
|
```
|
|
122
239
|
|
|
240
|
+
See [ROADMAP.md](./ROADMAP.md) for planned adapters and [CHANGELOG.md](./CHANGELOG.md) for what changed in each release.
|
|
241
|
+
|
|
123
242
|
### Development
|
|
124
243
|
|
|
125
244
|
```bash
|