@richardmcquiston01/error-log-adapter 0.9.0 → 0.10.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 +2 -1
- package/dist/index.cjs +32 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -4
- package/dist/index.d.ts +32 -4
- package/dist/index.mjs +32 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ Every adapter takes an optional `prefix` (default `[error-log-adapter]`) that is
|
|
|
81
81
|
| `SupabaseErrorAdapter(client)` | A Supabase table, through `supabase-js` | Anywhere | `tableName`, `timeoutMs` |
|
|
82
82
|
| `RedisErrorAdapter(client)` | A Redis list or stream | Node | `mode`, `key`, `maxEntries`, `side`, `timeoutMs` |
|
|
83
83
|
| `S3ErrorAdapter(client, bucket)` | S3, DigitalOcean Spaces, R2, MinIO objects | Node | `keyPrefix`, `batch`, `contentType`, `timeoutMs` |
|
|
84
|
-
| `FirestoreErrorAdapter(collection)` | A Firestore collection | Node, browser | `timeoutMs`
|
|
84
|
+
| `FirestoreErrorAdapter(collection)` | A Firestore collection | Node, browser | `timeoutMs`, `expireAfterMs` |
|
|
85
85
|
| `RealtimeDatabaseErrorAdapter(ref)` | A Realtime Database reference | Node, browser | `timeoutMs` |
|
|
86
86
|
| `FlatFileErrorAdapter(path)` | One text line per entry | Node, Bun | `formatLine`, `maxBytes`, `maxFiles` |
|
|
87
87
|
| `JsonFileErrorAdapter(path)` | NDJSON lines or one JSON array | Node, Bun | `mode`, `maxEntries`, `indent` |
|
|
@@ -103,6 +103,7 @@ What changes durability or size, by adapter:
|
|
|
103
103
|
- **Files** (`maxBytes` / `maxFiles`, `maxEntries`): the flat file adapter rotates by size; the JSON file adapter's `array` mode keeps the newest `maxEntries`.
|
|
104
104
|
- **Redis and Web Storage** (`maxEntries`): the oldest entries are dropped past the cap.
|
|
105
105
|
- **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.
|
|
106
|
+
- **Firestore expiry**: pass `expireAfterMs` (for example `30 * 24 * 60 * 60 * 1000`) and every document gets a Firestore timestamp field, `expire_at` (change the name with `expireField`), set to the entry's `timestamp` plus that time. This only writes the field: Firestore deletes nothing until you enable a TTL policy on it, in the console or with `gcloud firestore fields ttls update expire_at --collection-group=error_log --enable-ttl`. Documents are then removed some time after they expire (typically within a day), and the deletes are billed like any other. The Realtime Database has no TTL feature, so the option is Firestore only.
|
|
106
107
|
- **Event emitter**: each entry is emitted as an `error-log` event whose payload is the stored record, for in-process hooks such as a toast, a metrics counter or a test assertion. Listeners run synchronously, so one that throws makes `log()` return `ok: false` with its message. Node's `EventEmitter` and mitt work as they are; a browser `EventTarget` needs a one-line wrapper (shown in the example below). Don't name the event `error` on a Node `EventEmitter`: it throws when nobody listens. Node's `emit` returns `false` when there are no listeners; pass `requireListener: true` to report that as a failure instead of letting the entry vanish.
|
|
107
108
|
- **WebSocket**: each entry is sent as one JSON text message (the serialized entry plus its UUID `id`) over a socket you own; the adapter never connects, reconnects or buffers. If the socket's `readyState` is not open (connecting, closing or closed), `log()` returns `ok: false` and says which. `ok: true` means the socket accepted the message, not that the server got it, since a WebSocket has no acknowledgement. Browsers, Node's global `WebSocket`, Deno, Bun and `ws` all fit. Pair it with a durable adapter in a `CompositeErrorAdapter` if entries must survive the socket being down.
|
|
108
109
|
- **CloudWatch Logs**: each entry is sent through `PutLogEvents` as a JSON event (the serialized entry plus its UUID `id`) stamped with the entry's time, so Logs Insights can query its fields. Pass an AWS SDK v3 `CloudWatchLogs` client. AWS no longer needs sequence tokens, so none are tracked. If the stream (or group) does not exist, the adapter creates it and tries again once (`createMissing`, on by default; it needs `logs:CreateLogStream` and `logs:CreateLogGroup`, and a group created this way never expires its logs, so set a retention policy). CloudWatch can answer ok and still refuse events older than 14 days or more than 2 hours ahead; that is reported as `ok: false`. An event over 256 KiB is refused before sending. With `batch: { batchSize, maxWaitMs }`, events are buffered and sent together (up to 10,000 events and 1 MiB per request, oldest first), `log()` returns `ok` as soon as the event is buffered, and you should call `flush()` before the process exits. In Lambda and ECS, stdout already reaches CloudWatch; this is for apps running elsewhere.
|
package/dist/index.cjs
CHANGED
|
@@ -1627,6 +1627,15 @@ var S3ErrorAdapter = class {
|
|
|
1627
1627
|
};
|
|
1628
1628
|
|
|
1629
1629
|
// src/adapters/firebase-error-adapter.ts
|
|
1630
|
+
var DEFAULT_EXPIRE_FIELD = "expire_at";
|
|
1631
|
+
var RESERVED_FIELDS = /* @__PURE__ */ new Set([
|
|
1632
|
+
"id",
|
|
1633
|
+
"code",
|
|
1634
|
+
"message",
|
|
1635
|
+
"timestamp",
|
|
1636
|
+
"source",
|
|
1637
|
+
"cause"
|
|
1638
|
+
]);
|
|
1630
1639
|
function waitForWrite(start, timeoutMs) {
|
|
1631
1640
|
return withTimeout(
|
|
1632
1641
|
start,
|
|
@@ -1651,7 +1660,7 @@ function buildRecord(entry) {
|
|
|
1651
1660
|
return record;
|
|
1652
1661
|
}
|
|
1653
1662
|
var FirestoreErrorAdapter = class {
|
|
1654
|
-
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity
|
|
1663
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`, or the expiry options are invalid. */
|
|
1655
1664
|
constructor(collection, options = {}) {
|
|
1656
1665
|
this.collection = collection;
|
|
1657
1666
|
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
@@ -1659,10 +1668,31 @@ var FirestoreErrorAdapter = class {
|
|
|
1659
1668
|
options.timeoutMs,
|
|
1660
1669
|
"FirestoreErrorAdapter"
|
|
1661
1670
|
);
|
|
1671
|
+
this.expireAfterMs = options.expireAfterMs;
|
|
1672
|
+
this.expireField = options.expireField ?? DEFAULT_EXPIRE_FIELD;
|
|
1673
|
+
if (this.expireAfterMs !== void 0 && (!Number.isFinite(this.expireAfterMs) || this.expireAfterMs <= 0)) {
|
|
1674
|
+
throw new RangeError(
|
|
1675
|
+
`FirestoreErrorAdapter: expireAfterMs must be a positive number of milliseconds, received ${String(options.expireAfterMs)}.`
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
if (this.expireField === "" || /[./]/.test(this.expireField) || RESERVED_FIELDS.has(this.expireField)) {
|
|
1679
|
+
throw new RangeError(
|
|
1680
|
+
`FirestoreErrorAdapter: expireField must be a name other than ${[...RESERVED_FIELDS].join(", ")}, without "." or "/", received ${JSON.stringify(this.expireField)}.`
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1662
1683
|
}
|
|
1663
1684
|
async log(entry) {
|
|
1664
1685
|
try {
|
|
1665
|
-
const record = buildRecord(entry);
|
|
1686
|
+
const record = { ...buildRecord(entry) };
|
|
1687
|
+
if (this.expireAfterMs !== void 0) {
|
|
1688
|
+
const occurredAt = Date.parse(record.timestamp);
|
|
1689
|
+
if (Number.isNaN(occurredAt)) {
|
|
1690
|
+
throw new RangeError(
|
|
1691
|
+
`timestamp "${record.timestamp}" is not a valid ISO 8601 date, so the expiry time cannot be worked out`
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
record[this.expireField] = new Date(occurredAt + this.expireAfterMs);
|
|
1695
|
+
}
|
|
1666
1696
|
await waitForWrite(
|
|
1667
1697
|
() => this.collection.doc(record.id).set(record),
|
|
1668
1698
|
this.timeoutMs
|