@richardmcquiston01/error-log-adapter 0.4.0 → 0.6.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 +168 -13
- package/dist/index.cjs +837 -239
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +317 -22
- package/dist/index.d.ts +317 -22
- package/dist/index.mjs +835 -239
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
CompositeErrorAdapter: () => CompositeErrorAdapter,
|
|
24
24
|
ConsoleErrorAdapter: () => ConsoleErrorAdapter,
|
|
25
|
+
EventEmitterErrorAdapter: () => EventEmitterErrorAdapter,
|
|
25
26
|
FirestoreErrorAdapter: () => FirestoreErrorAdapter,
|
|
26
27
|
FlatFileErrorAdapter: () => FlatFileErrorAdapter,
|
|
27
28
|
IndexedDbErrorAdapter: () => IndexedDbErrorAdapter,
|
|
@@ -38,6 +39,7 @@ __export(index_exports, {
|
|
|
38
39
|
WebhookErrorAdapter: () => WebhookErrorAdapter,
|
|
39
40
|
buildCreateErrorLogTableSql: () => buildCreateErrorLogTableSql,
|
|
40
41
|
buildErrorLogTableDdl: () => buildErrorLogTableDdl,
|
|
42
|
+
buildMySqlWidenColumnsSql: () => buildMySqlWidenColumnsSql,
|
|
41
43
|
buildSupabaseSetupSql: () => buildSupabaseSetupSql,
|
|
42
44
|
createErrorLogStore: () => createErrorLogStore,
|
|
43
45
|
formatErrorLogLine: () => formatErrorLogLine,
|
|
@@ -47,11 +49,100 @@ __export(index_exports, {
|
|
|
47
49
|
module.exports = __toCommonJS(index_exports);
|
|
48
50
|
|
|
49
51
|
// src/serialize.ts
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
var MAX_CAUSE_DEPTH = 6;
|
|
53
|
+
function isErrorLike(value) {
|
|
54
|
+
if (value instanceof Error) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
const candidate = value;
|
|
58
|
+
return typeof candidate.name === "string" && typeof candidate.message === "string" && typeof value.stack === "string";
|
|
59
|
+
}
|
|
60
|
+
function readProperty(source, key) {
|
|
61
|
+
try {
|
|
62
|
+
return source[key];
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return `[Unreadable: ${error instanceof Error ? error.message : String(error)}]`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function toJsonSafe(value, depth, ancestors) {
|
|
68
|
+
switch (typeof value) {
|
|
69
|
+
case "string":
|
|
70
|
+
case "boolean":
|
|
71
|
+
case "undefined":
|
|
72
|
+
return value;
|
|
73
|
+
case "number":
|
|
74
|
+
return Number.isFinite(value) ? value : String(value);
|
|
75
|
+
case "bigint":
|
|
76
|
+
return `${value}n`;
|
|
77
|
+
case "symbol":
|
|
78
|
+
return value.toString();
|
|
79
|
+
case "function":
|
|
80
|
+
return `[Function ${value.name || "anonymous"}]`;
|
|
81
|
+
default:
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (value === null) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const objectValue = value;
|
|
88
|
+
if (ancestors.has(objectValue)) {
|
|
89
|
+
return "[Circular]";
|
|
90
|
+
}
|
|
91
|
+
if (depth >= MAX_CAUSE_DEPTH) {
|
|
92
|
+
return "[Truncated]";
|
|
93
|
+
}
|
|
94
|
+
ancestors.add(objectValue);
|
|
95
|
+
try {
|
|
96
|
+
if (isErrorLike(objectValue)) {
|
|
97
|
+
const result2 = {
|
|
98
|
+
name: objectValue.name,
|
|
99
|
+
message: objectValue.message,
|
|
100
|
+
stack: objectValue.stack
|
|
101
|
+
};
|
|
102
|
+
const nested = readProperty(objectValue, "cause");
|
|
103
|
+
if (nested !== void 0) {
|
|
104
|
+
result2["cause"] = toJsonSafe(nested, depth + 1, ancestors);
|
|
105
|
+
}
|
|
106
|
+
const errors = readProperty(objectValue, "errors");
|
|
107
|
+
if (Array.isArray(errors)) {
|
|
108
|
+
result2["errors"] = errors.map(
|
|
109
|
+
(item) => toJsonSafe(item, depth + 1, ancestors)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return result2;
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(value)) {
|
|
115
|
+
return value.map(
|
|
116
|
+
(item) => toJsonSafe(item, depth + 1, ancestors)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const toJson = readProperty(objectValue, "toJSON");
|
|
120
|
+
if (typeof toJson === "function") {
|
|
121
|
+
try {
|
|
122
|
+
return toJsonSafe(
|
|
123
|
+
toJson.call(objectValue),
|
|
124
|
+
depth + 1,
|
|
125
|
+
ancestors
|
|
126
|
+
);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return `[toJSON failed: ${error instanceof Error ? error.message : String(error)}]`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const result = {};
|
|
132
|
+
for (const key of Object.keys(objectValue)) {
|
|
133
|
+
result[key] = toJsonSafe(
|
|
134
|
+
readProperty(objectValue, key),
|
|
135
|
+
depth + 1,
|
|
136
|
+
ancestors
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
} finally {
|
|
141
|
+
ancestors.delete(objectValue);
|
|
53
142
|
}
|
|
54
|
-
|
|
143
|
+
}
|
|
144
|
+
function serializeCause(cause) {
|
|
145
|
+
return toJsonSafe(cause, 0, /* @__PURE__ */ new WeakSet());
|
|
55
146
|
}
|
|
56
147
|
function serializeEntry(entry) {
|
|
57
148
|
const serialized = {
|
|
@@ -71,10 +162,20 @@ function describeError(error) {
|
|
|
71
162
|
return error instanceof Error ? error.message : String(error);
|
|
72
163
|
}
|
|
73
164
|
|
|
165
|
+
// src/fail.ts
|
|
166
|
+
var DEFAULT_PREFIX = "[error-log-adapter]";
|
|
167
|
+
function reportFailure(report) {
|
|
168
|
+
console.error(`${report.prefix} ${report.adapterName}:`, report.error);
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
error: `${report.adapterName} failed ${report.action}: ${report.reason ?? describeError(report.error)}`
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
74
175
|
// src/adapters/console-error-adapter.ts
|
|
75
176
|
var ConsoleErrorAdapter = class {
|
|
76
177
|
constructor(options = {}) {
|
|
77
|
-
this.prefix = options.prefix ??
|
|
178
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
78
179
|
}
|
|
79
180
|
async log(entry) {
|
|
80
181
|
try {
|
|
@@ -89,35 +190,324 @@ var ConsoleErrorAdapter = class {
|
|
|
89
190
|
}
|
|
90
191
|
};
|
|
91
192
|
|
|
193
|
+
// src/uuid.ts
|
|
194
|
+
function generateId() {
|
|
195
|
+
const cryptoApi = globalThis.crypto;
|
|
196
|
+
if (cryptoApi !== void 0 && typeof cryptoApi.randomUUID === "function") {
|
|
197
|
+
return cryptoApi.randomUUID();
|
|
198
|
+
}
|
|
199
|
+
const bytes = new Uint8Array(16);
|
|
200
|
+
if (cryptoApi !== void 0 && typeof cryptoApi.getRandomValues === "function") {
|
|
201
|
+
cryptoApi.getRandomValues(bytes);
|
|
202
|
+
} else {
|
|
203
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
204
|
+
bytes[index] = Math.floor(Math.random() * 256);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
208
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
209
|
+
const hex = Array.from(
|
|
210
|
+
bytes,
|
|
211
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
212
|
+
).join("");
|
|
213
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/entry-id.ts
|
|
217
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
218
|
+
function hasCallerId(entry) {
|
|
219
|
+
return entry.id !== void 0;
|
|
220
|
+
}
|
|
221
|
+
function resolveEntryId(entry) {
|
|
222
|
+
if (entry.id === void 0) {
|
|
223
|
+
return generateId();
|
|
224
|
+
}
|
|
225
|
+
if (typeof entry.id !== "string" || !UUID_PATTERN.test(entry.id)) {
|
|
226
|
+
throw new RangeError(
|
|
227
|
+
`entry.id must be a UUID such as one from crypto.randomUUID(), received ${JSON.stringify(entry.id)}`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return entry.id.toLowerCase();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/timeout.ts
|
|
234
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
235
|
+
var SinkTimeoutError = class extends Error {
|
|
236
|
+
constructor(message) {
|
|
237
|
+
super(message);
|
|
238
|
+
this.name = "SinkTimeoutError";
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
function resolveTimeoutMs(value, adapterName) {
|
|
242
|
+
const timeoutMs = value ?? DEFAULT_TIMEOUT_MS;
|
|
243
|
+
if (Number.isNaN(timeoutMs) || timeoutMs <= 0) {
|
|
244
|
+
throw new RangeError(
|
|
245
|
+
`${adapterName}: timeoutMs must be a positive number or Infinity, received ${String(value)}.`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return timeoutMs;
|
|
249
|
+
}
|
|
250
|
+
function timeoutMessage(sink, timeoutMs, action = "respond") {
|
|
251
|
+
return `${sink} did not ${action} within ${timeoutMs} ms, so the entry may or may not have been stored (the operation can still finish later, so retrying could store it twice)`;
|
|
252
|
+
}
|
|
253
|
+
function withTimeout(start, timeoutMs, message) {
|
|
254
|
+
if (timeoutMs === Infinity) {
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
try {
|
|
257
|
+
resolve(start());
|
|
258
|
+
} catch (error) {
|
|
259
|
+
reject(error);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return new Promise(
|
|
264
|
+
(resolve, reject) => {
|
|
265
|
+
const timer = setTimeout(() => {
|
|
266
|
+
reject(new SinkTimeoutError(message));
|
|
267
|
+
}, timeoutMs);
|
|
268
|
+
const settle = (done) => (...args) => {
|
|
269
|
+
clearTimeout(timer);
|
|
270
|
+
done(...args);
|
|
271
|
+
};
|
|
272
|
+
try {
|
|
273
|
+
Promise.resolve(start()).then(settle(resolve), settle(reject));
|
|
274
|
+
} catch (error) {
|
|
275
|
+
settle(reject)(error);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
92
281
|
// src/adapters/webhook-error-adapter.ts
|
|
282
|
+
var DEFAULT_RETRY_DELAY_MS = 500;
|
|
283
|
+
var DEFAULT_MAX_RETRY_DELAY_MS = 3e4;
|
|
284
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
285
|
+
var SigningUnavailableError = class extends Error {
|
|
286
|
+
};
|
|
287
|
+
async function hmacSha256Hex(secret, message) {
|
|
288
|
+
const subtle = globalThis.crypto?.subtle;
|
|
289
|
+
if (subtle === void 0) {
|
|
290
|
+
throw new SigningUnavailableError(
|
|
291
|
+
"signingSecret needs Web Crypto (crypto.subtle), which this runtime does not provide, so the entry was not sent unsigned"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const encoder = new TextEncoder();
|
|
295
|
+
const key = await subtle.importKey(
|
|
296
|
+
"raw",
|
|
297
|
+
encoder.encode(secret),
|
|
298
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
299
|
+
false,
|
|
300
|
+
["sign"]
|
|
301
|
+
);
|
|
302
|
+
const signature = await subtle.sign(
|
|
303
|
+
"HMAC",
|
|
304
|
+
key,
|
|
305
|
+
encoder.encode(message)
|
|
306
|
+
);
|
|
307
|
+
return Array.from(
|
|
308
|
+
new Uint8Array(signature),
|
|
309
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
310
|
+
).join("");
|
|
311
|
+
}
|
|
312
|
+
function parseRetryAfterMs(value) {
|
|
313
|
+
if (value === null || value === void 0 || value.trim() === "") {
|
|
314
|
+
return void 0;
|
|
315
|
+
}
|
|
316
|
+
const seconds = Number(value);
|
|
317
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
318
|
+
return seconds * 1e3;
|
|
319
|
+
}
|
|
320
|
+
const date = Date.parse(value);
|
|
321
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
|
|
322
|
+
}
|
|
323
|
+
function validateRetryOptions(options) {
|
|
324
|
+
const retries = options.retries ?? 0;
|
|
325
|
+
if (!Number.isInteger(retries) || retries < 0) {
|
|
326
|
+
throw new RangeError(
|
|
327
|
+
`WebhookErrorAdapter: retries must be a whole number of 0 or more, received ${String(options.retries)}.`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
331
|
+
if (!Number.isFinite(retryDelayMs) || retryDelayMs <= 0) {
|
|
332
|
+
throw new RangeError(
|
|
333
|
+
`WebhookErrorAdapter: retryDelayMs must be a positive number, received ${String(options.retryDelayMs)}.`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const maxRetryDelayMs = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
337
|
+
if (!Number.isFinite(maxRetryDelayMs) || maxRetryDelayMs < retryDelayMs) {
|
|
338
|
+
throw new RangeError(
|
|
339
|
+
`WebhookErrorAdapter: maxRetryDelayMs must be a number no smaller than retryDelayMs (${retryDelayMs}), received ${String(options.maxRetryDelayMs)}.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
if (options.signingSecret === "") {
|
|
343
|
+
throw new RangeError(
|
|
344
|
+
"WebhookErrorAdapter: signingSecret must not be empty; leave it out to send unsigned."
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return { retries, retryDelayMs, maxRetryDelayMs };
|
|
348
|
+
}
|
|
349
|
+
function sleep(ms) {
|
|
350
|
+
return new Promise((resolve) => {
|
|
351
|
+
setTimeout(resolve, ms);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
355
|
+
"localhost",
|
|
356
|
+
"127.0.0.1",
|
|
357
|
+
"[::1]"
|
|
358
|
+
]);
|
|
359
|
+
function parseWebhookUrl(url, allowInsecure) {
|
|
360
|
+
let parsed;
|
|
361
|
+
try {
|
|
362
|
+
parsed = new URL(url);
|
|
363
|
+
} catch {
|
|
364
|
+
throw new RangeError(
|
|
365
|
+
"WebhookErrorAdapter: the webhook URL is not a valid absolute URL (for example https://example.com/errors)."
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
if (parsed.protocol === "https:") {
|
|
369
|
+
return parsed;
|
|
370
|
+
}
|
|
371
|
+
if (parsed.protocol === "http:" && (allowInsecure || LOOPBACK_HOSTS.has(parsed.hostname))) {
|
|
372
|
+
return parsed;
|
|
373
|
+
}
|
|
374
|
+
throw new RangeError(
|
|
375
|
+
`WebhookErrorAdapter: the webhook URL must use https:// because entries contain error messages and stack traces (received ${parsed.protocol}//${parsed.host}). Pass allowInsecure: true to send over http.`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
93
378
|
var WebhookErrorAdapter = class {
|
|
379
|
+
/** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or a timeout, retry or signing option is invalid. */
|
|
94
380
|
constructor(url, options = {}) {
|
|
95
381
|
this.url = url;
|
|
96
382
|
this.options = options;
|
|
97
|
-
this.prefix = options.prefix ??
|
|
383
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
384
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "WebhookErrorAdapter");
|
|
385
|
+
({
|
|
386
|
+
retries: this.retries,
|
|
387
|
+
retryDelayMs: this.retryDelayMs,
|
|
388
|
+
maxRetryDelayMs: this.maxRetryDelayMs
|
|
389
|
+
} = validateRetryOptions(options));
|
|
390
|
+
this.target = parseWebhookUrl(url, options.allowInsecure ?? false).origin;
|
|
98
391
|
}
|
|
99
392
|
async log(entry) {
|
|
393
|
+
let body;
|
|
394
|
+
try {
|
|
395
|
+
body = JSON.stringify(
|
|
396
|
+
hasCallerId(entry) || this.retries > 0 ? { id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry)
|
|
397
|
+
);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
400
|
+
return this.fail(
|
|
401
|
+
`could not prepare the entry: ${this.redact(describeError(error))}`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
let attempt = await this.send(body);
|
|
405
|
+
let attempts = 1;
|
|
406
|
+
while (!attempt.ok && attempt.retryable && attempts <= this.retries) {
|
|
407
|
+
await sleep(this.retryWaitMs(attempts, attempt.retryAfterMs));
|
|
408
|
+
attempt = await this.send(body);
|
|
409
|
+
attempts += 1;
|
|
410
|
+
}
|
|
411
|
+
if (attempt.ok) {
|
|
412
|
+
return { ok: true };
|
|
413
|
+
}
|
|
414
|
+
return this.fail(
|
|
415
|
+
attempts > 1 ? `${attempt.reason} (after ${attempts} attempts)` : attempt.reason
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
/** Milliseconds to wait before retry number `retryNumber` (1 for the first). */
|
|
419
|
+
retryWaitMs(retryNumber, retryAfterMs) {
|
|
420
|
+
if (retryAfterMs !== void 0) {
|
|
421
|
+
return Math.min(retryAfterMs, this.maxRetryDelayMs);
|
|
422
|
+
}
|
|
423
|
+
const ceiling = Math.min(
|
|
424
|
+
this.maxRetryDelayMs,
|
|
425
|
+
this.retryDelayMs * 2 ** (retryNumber - 1)
|
|
426
|
+
);
|
|
427
|
+
return ceiling / 2 + Math.random() * ceiling / 2;
|
|
428
|
+
}
|
|
429
|
+
async send(body) {
|
|
430
|
+
const controller = new AbortController();
|
|
431
|
+
let timedOut = false;
|
|
432
|
+
const timer = this.timeoutMs === Infinity ? void 0 : setTimeout(() => {
|
|
433
|
+
timedOut = true;
|
|
434
|
+
controller.abort();
|
|
435
|
+
}, this.timeoutMs);
|
|
100
436
|
try {
|
|
101
437
|
const response = await fetch(this.url, {
|
|
102
438
|
method: "POST",
|
|
103
439
|
headers: {
|
|
104
440
|
"Content-Type": "application/json",
|
|
105
|
-
...this.options.headers
|
|
441
|
+
...this.options.headers,
|
|
442
|
+
...await this.signatureHeaders(body)
|
|
106
443
|
},
|
|
107
|
-
body
|
|
444
|
+
body,
|
|
445
|
+
// A redirect would turn the POST into a GET without a body (302, 303)
|
|
446
|
+
// or re-send the entry and headers to another host (307, 308).
|
|
447
|
+
redirect: "manual",
|
|
448
|
+
signal: controller.signal
|
|
108
449
|
});
|
|
450
|
+
if (response.type === "opaqueredirect") {
|
|
451
|
+
return this.rejected(this.redirectMessage("a redirect"));
|
|
452
|
+
}
|
|
109
453
|
if (!response.ok) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
454
|
+
if (response.status >= 300 && response.status < 400) {
|
|
455
|
+
return this.rejected(this.redirectMessage(`HTTP ${response.status}`));
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
ok: false,
|
|
459
|
+
reason: `webhook ${this.target} responded with HTTP ${response.status}`,
|
|
460
|
+
retryable: response.status >= 500 || RETRYABLE_STATUSES.has(response.status),
|
|
461
|
+
retryAfterMs: parseRetryAfterMs(response.headers?.get("retry-after"))
|
|
462
|
+
};
|
|
113
463
|
}
|
|
114
464
|
return { ok: true };
|
|
115
465
|
} catch (error) {
|
|
116
466
|
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
467
|
+
if (error instanceof SigningUnavailableError) {
|
|
468
|
+
return this.rejected(error.message);
|
|
469
|
+
}
|
|
470
|
+
if (timedOut) {
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
reason: timeoutMessage(`webhook ${this.target}`, this.timeoutMs),
|
|
474
|
+
retryable: true
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
reason: `could not reach webhook ${this.target}: ${this.redact(describeError(error))}`,
|
|
480
|
+
retryable: true
|
|
481
|
+
};
|
|
482
|
+
} finally {
|
|
483
|
+
clearTimeout(timer);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/** Timestamp and signature headers, or none when no `signingSecret` is set. */
|
|
487
|
+
async signatureHeaders(body) {
|
|
488
|
+
const secret = this.options.signingSecret;
|
|
489
|
+
if (secret === void 0) {
|
|
490
|
+
return {};
|
|
120
491
|
}
|
|
492
|
+
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
493
|
+
const signature = await hmacSha256Hex(
|
|
494
|
+
secret,
|
|
495
|
+
`${timestamp}.${body}`
|
|
496
|
+
);
|
|
497
|
+
return {
|
|
498
|
+
"X-Signature-Timestamp": timestamp,
|
|
499
|
+
"X-Signature-256": `sha256=${signature}`
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
rejected(reason) {
|
|
503
|
+
return { ok: false, reason, retryable: false };
|
|
504
|
+
}
|
|
505
|
+
redirectMessage(what) {
|
|
506
|
+
return `webhook ${this.target} responded with ${what}, which is not followed because the entry would not arrive intact; use the final URL instead`;
|
|
507
|
+
}
|
|
508
|
+
/** Replaces the full URL in a message with its origin, in case the runtime put it there. */
|
|
509
|
+
redact(message) {
|
|
510
|
+
return message.split(this.url).join(this.target);
|
|
121
511
|
}
|
|
122
512
|
fail(reason) {
|
|
123
513
|
return { ok: false, error: `WebhookErrorAdapter failed: ${reason}` };
|
|
@@ -125,37 +515,64 @@ var WebhookErrorAdapter = class {
|
|
|
125
515
|
};
|
|
126
516
|
|
|
127
517
|
// src/adapters/mongodb-error-adapter.ts
|
|
518
|
+
function isDuplicateKeyError(error) {
|
|
519
|
+
return typeof error === "object" && error !== null && error.code === 11e3;
|
|
520
|
+
}
|
|
128
521
|
var MongoDbErrorAdapter = class {
|
|
522
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
129
523
|
constructor(collection, options = {}) {
|
|
130
524
|
this.collection = collection;
|
|
131
|
-
this.prefix = options.prefix ??
|
|
525
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
526
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "MongoDbErrorAdapter");
|
|
132
527
|
}
|
|
133
528
|
async log(entry) {
|
|
134
529
|
try {
|
|
135
|
-
|
|
530
|
+
const document = hasCallerId(entry) ? { _id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry);
|
|
531
|
+
await withTimeout(
|
|
532
|
+
() => this.collection.insertOne(document),
|
|
533
|
+
this.timeoutMs,
|
|
534
|
+
timeoutMessage("MongoDB", this.timeoutMs)
|
|
535
|
+
);
|
|
136
536
|
return { ok: true };
|
|
137
537
|
} catch (error) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
538
|
+
if (hasCallerId(entry) && isDuplicateKeyError(error)) {
|
|
539
|
+
return { ok: true };
|
|
540
|
+
}
|
|
541
|
+
return reportFailure({
|
|
542
|
+
adapterName: "MongoDbErrorAdapter",
|
|
543
|
+
prefix: this.prefix,
|
|
544
|
+
action: "to insert entry",
|
|
545
|
+
error
|
|
546
|
+
});
|
|
143
547
|
}
|
|
144
548
|
}
|
|
145
549
|
};
|
|
146
550
|
|
|
147
551
|
// src/adapters/composite-error-adapter.ts
|
|
552
|
+
var DEFAULT_COMPOSITE_TIMEOUT_MS = DEFAULT_TIMEOUT_MS * 3;
|
|
148
553
|
var CompositeErrorAdapter = class {
|
|
149
|
-
|
|
554
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
555
|
+
constructor(adapters, options = {}) {
|
|
150
556
|
this.adapters = adapters;
|
|
557
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
558
|
+
options.timeoutMs ?? DEFAULT_COMPOSITE_TIMEOUT_MS,
|
|
559
|
+
"CompositeErrorAdapter"
|
|
560
|
+
);
|
|
151
561
|
}
|
|
152
562
|
async log(entry) {
|
|
153
563
|
const results = await Promise.all(
|
|
154
564
|
this.adapters.map(
|
|
155
|
-
async (adapter) => {
|
|
565
|
+
async (adapter, index) => {
|
|
156
566
|
try {
|
|
157
|
-
return await
|
|
567
|
+
return await withTimeout(
|
|
568
|
+
() => adapter.log(entry),
|
|
569
|
+
this.timeoutMs,
|
|
570
|
+
`adapter ${index} (${adapter.constructor.name}) did not return within ${this.timeoutMs} ms, so the entry may or may not have been stored there`
|
|
571
|
+
);
|
|
158
572
|
} catch (error) {
|
|
573
|
+
if (error instanceof SinkTimeoutError) {
|
|
574
|
+
return { ok: false, error: error.message };
|
|
575
|
+
}
|
|
159
576
|
return {
|
|
160
577
|
ok: false,
|
|
161
578
|
error: `adapter threw instead of returning a result: ${String(error)}`
|
|
@@ -186,58 +603,101 @@ function createErrorLogStore(database, storeName = DEFAULT_STORE_NAME) {
|
|
|
186
603
|
database.createObjectStore(storeName, { keyPath: "id" }).createIndex("timestamp", "timestamp");
|
|
187
604
|
}
|
|
188
605
|
var IndexedDbErrorAdapter = class {
|
|
606
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
189
607
|
constructor(database, options = {}) {
|
|
190
608
|
this.database = database;
|
|
191
609
|
this.storeName = options.storeName ?? DEFAULT_STORE_NAME;
|
|
192
|
-
this.prefix = options.prefix ??
|
|
610
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
611
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
612
|
+
options.timeoutMs,
|
|
613
|
+
"IndexedDbErrorAdapter"
|
|
614
|
+
);
|
|
193
615
|
}
|
|
194
616
|
async log(entry) {
|
|
195
617
|
try {
|
|
196
618
|
const record = {
|
|
197
|
-
id:
|
|
619
|
+
id: resolveEntryId(entry),
|
|
198
620
|
...serializeEntry(entry)
|
|
199
621
|
};
|
|
200
|
-
await this.write(record);
|
|
622
|
+
await this.write(record, hasCallerId(entry));
|
|
201
623
|
return { ok: true };
|
|
202
624
|
} catch (error) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
625
|
+
return reportFailure({
|
|
626
|
+
adapterName: "IndexedDbErrorAdapter",
|
|
627
|
+
prefix: this.prefix,
|
|
628
|
+
action: `to write to object store "${this.storeName}"`,
|
|
629
|
+
error
|
|
630
|
+
});
|
|
208
631
|
}
|
|
209
632
|
}
|
|
210
|
-
/**
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
633
|
+
/**
|
|
634
|
+
* Resolves once the transaction commits; rejects if the request fails, the
|
|
635
|
+
* transaction aborts, or it does not finish within `timeoutMs`.
|
|
636
|
+
*
|
|
637
|
+
* Error events are handled here (`preventDefault` and `stopPropagation`), so a
|
|
638
|
+
* failed log write does not reach the `onerror` handler an app may have set on
|
|
639
|
+
* its own database object.
|
|
640
|
+
*/
|
|
641
|
+
write(record, replaceExisting) {
|
|
642
|
+
return withTimeout(
|
|
643
|
+
() => new Promise(
|
|
644
|
+
(resolve, reject) => {
|
|
645
|
+
const transaction = this.database.transaction(
|
|
646
|
+
this.storeName,
|
|
647
|
+
"readwrite"
|
|
648
|
+
);
|
|
649
|
+
const fail = (what, source) => (event) => {
|
|
650
|
+
event?.preventDefault?.();
|
|
651
|
+
event?.stopPropagation?.();
|
|
652
|
+
reject(new Error(`${what}: ${describeDomError(source.error)}`));
|
|
653
|
+
};
|
|
654
|
+
transaction.oncomplete = () => resolve();
|
|
655
|
+
transaction.onerror = fail("transaction failed", transaction);
|
|
656
|
+
transaction.onabort = fail("transaction aborted", transaction);
|
|
657
|
+
const store = transaction.objectStore(
|
|
658
|
+
this.storeName
|
|
659
|
+
);
|
|
660
|
+
const request = replaceExisting && typeof store.put === "function" ? store.put(record) : store.add(record);
|
|
661
|
+
if (typeof request === "object" && request !== null) {
|
|
662
|
+
request.onerror = fail(
|
|
663
|
+
"request failed",
|
|
664
|
+
request
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
),
|
|
669
|
+
this.timeoutMs,
|
|
670
|
+
timeoutMessage("IndexedDB", this.timeoutMs, "finish the transaction")
|
|
228
671
|
);
|
|
229
672
|
}
|
|
230
673
|
};
|
|
674
|
+
function describeDomError(error) {
|
|
675
|
+
if (error === null || error === void 0) {
|
|
676
|
+
return "no details provided";
|
|
677
|
+
}
|
|
678
|
+
const text = [error.name, error.message].filter((part) => typeof part === "string" && part !== "").join(": ");
|
|
679
|
+
return text === "" ? "no details provided" : text;
|
|
680
|
+
}
|
|
231
681
|
|
|
232
|
-
// src/
|
|
682
|
+
// src/sql-ident.ts
|
|
233
683
|
var DEFAULT_TABLE_NAME = "error_log";
|
|
234
684
|
var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
235
|
-
function
|
|
236
|
-
if (!IDENTIFIER_PATTERN.test(
|
|
685
|
+
function assertIdentifier(value, label, owner, maxLength) {
|
|
686
|
+
if (!IDENTIFIER_PATTERN.test(value)) {
|
|
237
687
|
throw new RangeError(
|
|
238
|
-
|
|
688
|
+
`${owner}: invalid ${label} "${value}". Use only letters, digits and underscores, and do not start with a digit.`
|
|
239
689
|
);
|
|
240
690
|
}
|
|
691
|
+
if (maxLength !== void 0 && value.length > maxLength) {
|
|
692
|
+
throw new RangeError(
|
|
693
|
+
`${owner}: ${label} "${value}" is ${value.length} characters long, but the limit is ${maxLength}.`
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/adapters/sqlite-error-adapter.ts
|
|
699
|
+
function assertValidTableName(tableName) {
|
|
700
|
+
assertIdentifier(tableName, "table name", "SqliteErrorAdapter");
|
|
241
701
|
}
|
|
242
702
|
function buildCreateErrorLogTableSql(tableName = DEFAULT_TABLE_NAME) {
|
|
243
703
|
assertValidTableName(tableName);
|
|
@@ -247,23 +707,24 @@ function buildCreateErrorLogTableSql(tableName = DEFAULT_TABLE_NAME) {
|
|
|
247
707
|
];
|
|
248
708
|
}
|
|
249
709
|
var SqliteErrorAdapter = class {
|
|
250
|
-
/** @throws {RangeError} if `options.tableName` is not a plain identifier. */
|
|
710
|
+
/** @throws {RangeError} if `options.tableName` is not a plain identifier or `options.timeoutMs` is invalid. */
|
|
251
711
|
constructor(client, options = {}) {
|
|
252
712
|
this.client = client;
|
|
253
713
|
this.tableReady = null;
|
|
254
714
|
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
255
715
|
assertValidTableName(this.tableName);
|
|
256
716
|
this.createTable = options.createTable ?? true;
|
|
257
|
-
this.prefix = options.prefix ??
|
|
717
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
718
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "SqliteErrorAdapter");
|
|
258
719
|
}
|
|
259
720
|
async log(entry) {
|
|
260
721
|
try {
|
|
261
722
|
await this.ensureTable();
|
|
262
723
|
const serialized = serializeEntry(entry);
|
|
263
|
-
await this.
|
|
264
|
-
`INSERT INTO ${this.tableName} (id, code, message, cause, source, occurred_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
724
|
+
await this.run(
|
|
725
|
+
`INSERT INTO ${this.tableName} (id, code, message, cause, source, occurred_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO NOTHING`,
|
|
265
726
|
[
|
|
266
|
-
|
|
727
|
+
resolveEntryId(entry),
|
|
267
728
|
serialized.code,
|
|
268
729
|
serialized.message,
|
|
269
730
|
serialized.cause === void 0 ? null : JSON.stringify(serialized.cause),
|
|
@@ -273,11 +734,12 @@ var SqliteErrorAdapter = class {
|
|
|
273
734
|
);
|
|
274
735
|
return { ok: true };
|
|
275
736
|
} catch (error) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
737
|
+
return reportFailure({
|
|
738
|
+
adapterName: "SqliteErrorAdapter",
|
|
739
|
+
prefix: this.prefix,
|
|
740
|
+
action: `to write to table "${this.tableName}"`,
|
|
741
|
+
error
|
|
742
|
+
});
|
|
281
743
|
}
|
|
282
744
|
}
|
|
283
745
|
/** Creates the table once. A failed attempt is retried on the next log call. */
|
|
@@ -297,9 +759,17 @@ var SqliteErrorAdapter = class {
|
|
|
297
759
|
}
|
|
298
760
|
async runCreateStatements() {
|
|
299
761
|
for (const sql of buildCreateErrorLogTableSql(this.tableName)) {
|
|
300
|
-
await this.
|
|
762
|
+
await this.run(sql, []);
|
|
301
763
|
}
|
|
302
764
|
}
|
|
765
|
+
/** Runs one statement, giving up waiting after `timeoutMs`. */
|
|
766
|
+
run(sql, params) {
|
|
767
|
+
return withTimeout(
|
|
768
|
+
() => this.client.run(sql, params),
|
|
769
|
+
this.timeoutMs,
|
|
770
|
+
timeoutMessage("SQLite", this.timeoutMs)
|
|
771
|
+
);
|
|
772
|
+
}
|
|
303
773
|
};
|
|
304
774
|
|
|
305
775
|
// src/adapters/web-storage-error-adapter.ts
|
|
@@ -318,7 +788,8 @@ var WebStorageErrorAdapter = class {
|
|
|
318
788
|
this.storage = storage;
|
|
319
789
|
this.key = options.key ?? DEFAULT_STORAGE_KEY;
|
|
320
790
|
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
321
|
-
this.prefix = options.prefix ??
|
|
791
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
792
|
+
this.locks = options.locks === void 0 ? defaultLockManager() : options.locks;
|
|
322
793
|
if (!Number.isInteger(this.maxEntries) || this.maxEntries < 1) {
|
|
323
794
|
throw new RangeError(
|
|
324
795
|
`WebStorageErrorAdapter: maxEntries must be a positive integer, received ${String(options.maxEntries)}.`
|
|
@@ -328,23 +799,49 @@ var WebStorageErrorAdapter = class {
|
|
|
328
799
|
async log(entry) {
|
|
329
800
|
try {
|
|
330
801
|
const record = {
|
|
331
|
-
id:
|
|
802
|
+
id: resolveEntryId(entry),
|
|
332
803
|
...serializeEntry(entry)
|
|
333
804
|
};
|
|
334
|
-
this.
|
|
805
|
+
await this.appendLocked(record);
|
|
335
806
|
return { ok: true };
|
|
336
807
|
} catch (error) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
808
|
+
return reportFailure({
|
|
809
|
+
adapterName: "WebStorageErrorAdapter",
|
|
810
|
+
prefix: this.prefix,
|
|
811
|
+
action: `to write to storage key "${this.key}"`,
|
|
812
|
+
error
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Appends under a cross-tab lock when one is available. If the lock itself
|
|
818
|
+
* cannot be taken (for example a sandboxed frame), the entry is still written
|
|
819
|
+
* without it rather than lost.
|
|
820
|
+
*/
|
|
821
|
+
async appendLocked(record) {
|
|
822
|
+
if (this.locks === null) {
|
|
823
|
+
this.append(record);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
let appended = false;
|
|
827
|
+
try {
|
|
828
|
+
await this.locks.request(`error-log-adapter:${this.key}`, () => {
|
|
829
|
+
appended = true;
|
|
830
|
+
this.append(record);
|
|
831
|
+
});
|
|
832
|
+
} catch (error) {
|
|
833
|
+
if (appended) {
|
|
834
|
+
throw error;
|
|
835
|
+
}
|
|
836
|
+
this.append(record);
|
|
342
837
|
}
|
|
343
838
|
}
|
|
344
|
-
/** Reads, appends, trims, and writes back. Runs without awaiting, so calls never interleave. */
|
|
839
|
+
/** Reads, appends, trims, and writes back. Runs without awaiting, so calls in one tab never interleave. */
|
|
345
840
|
append(record) {
|
|
346
841
|
const records = [
|
|
347
|
-
...this.readRecords()
|
|
842
|
+
...this.readRecords().filter(
|
|
843
|
+
(existing) => existing?.id !== record.id
|
|
844
|
+
),
|
|
348
845
|
record
|
|
349
846
|
].slice(-this.maxEntries);
|
|
350
847
|
for (; ; ) {
|
|
@@ -370,16 +867,51 @@ var WebStorageErrorAdapter = class {
|
|
|
370
867
|
if (Array.isArray(parsed)) {
|
|
371
868
|
return parsed;
|
|
372
869
|
}
|
|
373
|
-
|
|
374
|
-
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" did not hold an array, so it was replaced.`
|
|
375
|
-
);
|
|
870
|
+
this.setAside(stored, "did not hold an array");
|
|
376
871
|
} catch (error) {
|
|
377
|
-
|
|
378
|
-
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" held invalid JSON (${describeError(error)}), so it was replaced.`
|
|
379
|
-
);
|
|
872
|
+
this.setAside(stored, `held invalid JSON (${describeError(error)})`);
|
|
380
873
|
}
|
|
381
874
|
return [];
|
|
382
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* Copies an unusable value to `<key>.corrupt` before it is replaced, so data
|
|
878
|
+
* the host app stored under the same key is not silently destroyed. Only the
|
|
879
|
+
* first one is kept; a later unusable value would not overwrite it.
|
|
880
|
+
*/
|
|
881
|
+
setAside(stored, reason) {
|
|
882
|
+
const backupKey = `${this.key}${CORRUPT_KEY_SUFFIX}`;
|
|
883
|
+
let savedTo = "but it could not be copied aside";
|
|
884
|
+
try {
|
|
885
|
+
if (this.storage.getItem(backupKey) === null) {
|
|
886
|
+
this.storage.setItem(backupKey, stored);
|
|
887
|
+
savedTo = `and a copy was kept under "${backupKey}"`;
|
|
888
|
+
} else {
|
|
889
|
+
savedTo = `and "${backupKey}" already holds an earlier copy`;
|
|
890
|
+
}
|
|
891
|
+
} catch {
|
|
892
|
+
}
|
|
893
|
+
console.error(
|
|
894
|
+
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" ${reason}, so it was replaced, ${savedTo}.`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
var CORRUPT_KEY_SUFFIX = ".corrupt";
|
|
899
|
+
function defaultLockManager() {
|
|
900
|
+
const locks = globalThis.navigator?.locks;
|
|
901
|
+
return typeof locks?.request === "function" ? locks : null;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// src/queue.ts
|
|
905
|
+
var SerialQueue = class {
|
|
906
|
+
constructor() {
|
|
907
|
+
this.tail = Promise.resolve();
|
|
908
|
+
}
|
|
909
|
+
/** Starts `task` once every earlier task has finished, and resolves with its result. */
|
|
910
|
+
run(task) {
|
|
911
|
+
const result = this.tail.then(task);
|
|
912
|
+
this.tail = result.catch(() => void 0);
|
|
913
|
+
return result;
|
|
914
|
+
}
|
|
383
915
|
};
|
|
384
916
|
|
|
385
917
|
// src/file-system.ts
|
|
@@ -387,6 +919,7 @@ async function loadNodeFileSystem() {
|
|
|
387
919
|
const moduleName = "node:fs/promises";
|
|
388
920
|
return await import(moduleName);
|
|
389
921
|
}
|
|
922
|
+
var ROOT_PATTERN = /^(?:[A-Za-z]:)?[\\/]+$/;
|
|
390
923
|
function parentDirectory(filePath) {
|
|
391
924
|
const lastSeparator = Math.max(
|
|
392
925
|
filePath.lastIndexOf("/"),
|
|
@@ -395,7 +928,11 @@ function parentDirectory(filePath) {
|
|
|
395
928
|
if (lastSeparator < 0) {
|
|
396
929
|
return null;
|
|
397
930
|
}
|
|
398
|
-
|
|
931
|
+
const directory = filePath.slice(0, lastSeparator + 1);
|
|
932
|
+
if (ROOT_PATTERN.test(directory)) {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
return filePath.slice(0, lastSeparator);
|
|
399
936
|
}
|
|
400
937
|
function isMissingFileError(error) {
|
|
401
938
|
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
@@ -441,24 +978,20 @@ var FlatFileErrorAdapter = class {
|
|
|
441
978
|
constructor(filePath, options = {}) {
|
|
442
979
|
this.filePath = filePath;
|
|
443
980
|
this.directoryReady = false;
|
|
444
|
-
this.queue =
|
|
981
|
+
this.queue = new SerialQueue();
|
|
445
982
|
this.fs = options.fs;
|
|
446
983
|
this.formatLine = options.formatLine ?? formatErrorLogLine;
|
|
447
984
|
this.maxBytes = options.maxBytes;
|
|
448
985
|
this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
449
986
|
this.createDirectory = options.createDirectory ?? true;
|
|
450
|
-
this.prefix = options.prefix ??
|
|
987
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
451
988
|
if (this.maxBytes !== void 0) {
|
|
452
989
|
assertPositiveInteger(this.maxBytes, "maxBytes");
|
|
453
990
|
}
|
|
454
991
|
assertPositiveInteger(this.maxFiles, "maxFiles");
|
|
455
992
|
}
|
|
456
993
|
log(entry) {
|
|
457
|
-
|
|
458
|
-
() => this.write(entry)
|
|
459
|
-
);
|
|
460
|
-
this.queue = result.catch(() => void 0);
|
|
461
|
-
return result;
|
|
994
|
+
return this.queue.run(() => this.write(entry));
|
|
462
995
|
}
|
|
463
996
|
async write(entry) {
|
|
464
997
|
try {
|
|
@@ -471,11 +1004,12 @@ var FlatFileErrorAdapter = class {
|
|
|
471
1004
|
await fs.appendFile(this.filePath, line);
|
|
472
1005
|
return { ok: true };
|
|
473
1006
|
} catch (error) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
1007
|
+
return reportFailure({
|
|
1008
|
+
adapterName: "FlatFileErrorAdapter",
|
|
1009
|
+
prefix: this.prefix,
|
|
1010
|
+
action: `to append to "${this.filePath}"`,
|
|
1011
|
+
error
|
|
1012
|
+
});
|
|
479
1013
|
}
|
|
480
1014
|
}
|
|
481
1015
|
/** Uses the injected file system, or loads `node:fs/promises` on first use. */
|
|
@@ -550,13 +1084,13 @@ var JsonFileErrorAdapter = class {
|
|
|
550
1084
|
constructor(filePath, options = {}) {
|
|
551
1085
|
this.filePath = filePath;
|
|
552
1086
|
this.directoryReady = false;
|
|
553
|
-
this.queue =
|
|
1087
|
+
this.queue = new SerialQueue();
|
|
554
1088
|
this.mode = options.mode ?? "ndjson";
|
|
555
1089
|
this.maxEntries = options.maxEntries;
|
|
556
1090
|
this.indent = options.indent ?? DEFAULT_INDENT;
|
|
557
1091
|
this.fs = options.fs;
|
|
558
1092
|
this.createDirectory = options.createDirectory ?? true;
|
|
559
|
-
this.prefix = options.prefix ??
|
|
1093
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
560
1094
|
if (this.maxEntries !== void 0) {
|
|
561
1095
|
if (this.mode !== "array") {
|
|
562
1096
|
throw new RangeError(
|
|
@@ -576,16 +1110,12 @@ var JsonFileErrorAdapter = class {
|
|
|
576
1110
|
}
|
|
577
1111
|
}
|
|
578
1112
|
log(entry) {
|
|
579
|
-
|
|
580
|
-
() => this.write(entry)
|
|
581
|
-
);
|
|
582
|
-
this.queue = result.catch(() => void 0);
|
|
583
|
-
return result;
|
|
1113
|
+
return this.queue.run(() => this.write(entry));
|
|
584
1114
|
}
|
|
585
1115
|
async write(entry) {
|
|
586
1116
|
try {
|
|
587
1117
|
const record = {
|
|
588
|
-
id:
|
|
1118
|
+
id: resolveEntryId(entry),
|
|
589
1119
|
...serializeEntry(entry)
|
|
590
1120
|
};
|
|
591
1121
|
const fs = await this.getFileSystem();
|
|
@@ -598,11 +1128,12 @@ var JsonFileErrorAdapter = class {
|
|
|
598
1128
|
}
|
|
599
1129
|
return { ok: true };
|
|
600
1130
|
} catch (error) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
1131
|
+
return reportFailure({
|
|
1132
|
+
adapterName: "JsonFileErrorAdapter",
|
|
1133
|
+
prefix: this.prefix,
|
|
1134
|
+
action: `to write "${this.filePath}" in ${this.mode} mode`,
|
|
1135
|
+
error
|
|
1136
|
+
});
|
|
606
1137
|
}
|
|
607
1138
|
}
|
|
608
1139
|
async getFileSystem() {
|
|
@@ -628,7 +1159,9 @@ var JsonFileErrorAdapter = class {
|
|
|
628
1159
|
*/
|
|
629
1160
|
async rewriteArray(fs, record) {
|
|
630
1161
|
const records = [
|
|
631
|
-
...await this.readRecords(fs)
|
|
1162
|
+
...(await this.readRecords(fs)).filter(
|
|
1163
|
+
(existing) => existing?.id !== record.id
|
|
1164
|
+
),
|
|
632
1165
|
record
|
|
633
1166
|
];
|
|
634
1167
|
const kept = this.maxEntries === void 0 ? records : records.slice(-this.maxEntries);
|
|
@@ -678,25 +1211,14 @@ var JsonFileErrorAdapter = class {
|
|
|
678
1211
|
};
|
|
679
1212
|
|
|
680
1213
|
// src/adapters/sql-error-adapter.ts
|
|
681
|
-
var DEFAULT_TABLE_NAME2 = "error_log";
|
|
682
1214
|
var MAX_TABLE_NAME_LENGTH = 47;
|
|
683
|
-
var IDENTIFIER_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
684
1215
|
function assertValidTableName2(tableName, adapterName) {
|
|
685
|
-
|
|
686
|
-
throw new RangeError(
|
|
687
|
-
`${adapterName}: invalid table name "${tableName}". Use only letters, digits and underscores, and do not start with a digit.`
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
if (tableName.length > MAX_TABLE_NAME_LENGTH) {
|
|
691
|
-
throw new RangeError(
|
|
692
|
-
`${adapterName}: table name "${tableName}" is ${tableName.length} characters long, but the limit is ${MAX_TABLE_NAME_LENGTH}.`
|
|
693
|
-
);
|
|
694
|
-
}
|
|
1216
|
+
assertIdentifier(tableName, "table name", adapterName, MAX_TABLE_NAME_LENGTH);
|
|
695
1217
|
}
|
|
696
1218
|
function quoteIdentifier(dialect, identifier) {
|
|
697
1219
|
return dialect === "postgres" ? `"${identifier}"` : `\`${identifier}\``;
|
|
698
1220
|
}
|
|
699
|
-
function buildErrorLogTableDdl(dialect, tableName =
|
|
1221
|
+
function buildErrorLogTableDdl(dialect, tableName = DEFAULT_TABLE_NAME) {
|
|
700
1222
|
assertValidTableName2(tableName, "buildErrorLogTableDdl");
|
|
701
1223
|
const table = quoteIdentifier(dialect, tableName);
|
|
702
1224
|
const index = quoteIdentifier(
|
|
@@ -710,7 +1232,13 @@ function buildErrorLogTableDdl(dialect, tableName = DEFAULT_TABLE_NAME2) {
|
|
|
710
1232
|
];
|
|
711
1233
|
}
|
|
712
1234
|
return [
|
|
713
|
-
`CREATE TABLE IF NOT EXISTS ${table} (id CHAR(36) NOT NULL PRIMARY KEY, code
|
|
1235
|
+
`CREATE TABLE IF NOT EXISTS ${table} (id CHAR(36) NOT NULL PRIMARY KEY, code TEXT NOT NULL, message TEXT NOT NULL, cause JSON NULL, source TEXT NULL, occurred_at DATETIME(3) NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), INDEX ${index} (occurred_at))`
|
|
1236
|
+
];
|
|
1237
|
+
}
|
|
1238
|
+
function buildMySqlWidenColumnsSql(tableName = DEFAULT_TABLE_NAME) {
|
|
1239
|
+
assertValidTableName2(tableName, "buildMySqlWidenColumnsSql");
|
|
1240
|
+
return [
|
|
1241
|
+
`ALTER TABLE ${quoteIdentifier("mysql", tableName)} MODIFY code TEXT NOT NULL, MODIFY source TEXT NULL`
|
|
714
1242
|
];
|
|
715
1243
|
}
|
|
716
1244
|
function toMysqlDateTime(date) {
|
|
@@ -722,10 +1250,11 @@ var SqlErrorAdapter = class {
|
|
|
722
1250
|
this.dialect = dialect;
|
|
723
1251
|
this.adapterName = adapterName;
|
|
724
1252
|
this.tableReady = null;
|
|
725
|
-
this.tableName = options.tableName ??
|
|
1253
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
726
1254
|
assertValidTableName2(this.tableName, adapterName);
|
|
727
1255
|
this.createTable = options.createTable ?? true;
|
|
728
|
-
this.prefix = options.prefix ??
|
|
1256
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1257
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, adapterName);
|
|
729
1258
|
}
|
|
730
1259
|
async log(entry) {
|
|
731
1260
|
try {
|
|
@@ -737,7 +1266,7 @@ var SqlErrorAdapter = class {
|
|
|
737
1266
|
);
|
|
738
1267
|
}
|
|
739
1268
|
const params = [
|
|
740
|
-
|
|
1269
|
+
resolveEntryId(entry),
|
|
741
1270
|
serialized.code,
|
|
742
1271
|
serialized.message,
|
|
743
1272
|
serialized.cause === void 0 ? null : JSON.stringify(serialized.cause),
|
|
@@ -745,19 +1274,21 @@ var SqlErrorAdapter = class {
|
|
|
745
1274
|
this.dialect === "postgres" ? occurredAt.toISOString() : toMysqlDateTime(occurredAt)
|
|
746
1275
|
];
|
|
747
1276
|
await this.ensureTable();
|
|
748
|
-
await this.
|
|
1277
|
+
await this.query(this.insertSql(), params);
|
|
749
1278
|
return { ok: true };
|
|
750
1279
|
} catch (error) {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1280
|
+
return reportFailure({
|
|
1281
|
+
adapterName: this.adapterName,
|
|
1282
|
+
prefix: this.prefix,
|
|
1283
|
+
action: `to write to table "${this.tableName}"`,
|
|
1284
|
+
error
|
|
1285
|
+
});
|
|
756
1286
|
}
|
|
757
1287
|
}
|
|
758
1288
|
insertSql() {
|
|
759
1289
|
const placeholders = this.dialect === "postgres" ? "$1, $2, $3, $4, $5, $6" : "?, ?, ?, ?, ?, ?";
|
|
760
|
-
|
|
1290
|
+
const onRepeat = this.dialect === "postgres" ? "ON CONFLICT (id) DO NOTHING" : "ON DUPLICATE KEY UPDATE id = id";
|
|
1291
|
+
return `INSERT INTO ${quoteIdentifier(this.dialect, this.tableName)} (id, code, message, cause, source, occurred_at) VALUES (${placeholders}) ${onRepeat}`;
|
|
761
1292
|
}
|
|
762
1293
|
/** Creates the table once. A failed attempt is retried on the next log call. */
|
|
763
1294
|
ensureTable() {
|
|
@@ -776,9 +1307,20 @@ var SqlErrorAdapter = class {
|
|
|
776
1307
|
}
|
|
777
1308
|
async runCreateStatements() {
|
|
778
1309
|
for (const sql of buildErrorLogTableDdl(this.dialect, this.tableName)) {
|
|
779
|
-
await this.
|
|
1310
|
+
await this.query(sql, []);
|
|
780
1311
|
}
|
|
781
1312
|
}
|
|
1313
|
+
/** Runs one statement, giving up waiting after `timeoutMs`. */
|
|
1314
|
+
query(sql, params) {
|
|
1315
|
+
return withTimeout(
|
|
1316
|
+
() => this.client.query(sql, params),
|
|
1317
|
+
this.timeoutMs,
|
|
1318
|
+
timeoutMessage(
|
|
1319
|
+
this.dialect === "postgres" ? "Postgres" : "MySQL",
|
|
1320
|
+
this.timeoutMs
|
|
1321
|
+
)
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
782
1324
|
};
|
|
783
1325
|
var PostgresErrorAdapter = class extends SqlErrorAdapter {
|
|
784
1326
|
/** @throws {RangeError} if `options.tableName` is not a plain identifier of 47 characters or fewer. */
|
|
@@ -803,7 +1345,8 @@ var RedisErrorAdapter = class {
|
|
|
803
1345
|
this.key = options.key ?? DEFAULT_REDIS_KEY;
|
|
804
1346
|
this.maxEntries = options.maxEntries;
|
|
805
1347
|
this.side = options.side ?? "right";
|
|
806
|
-
this.prefix = options.prefix ??
|
|
1348
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1349
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "RedisErrorAdapter");
|
|
807
1350
|
if (this.mode !== "list" && this.mode !== "stream") {
|
|
808
1351
|
throw new RangeError(
|
|
809
1352
|
`RedisErrorAdapter: mode must be "list" or "stream", received "${String(options.mode)}".`
|
|
@@ -825,11 +1368,11 @@ var RedisErrorAdapter = class {
|
|
|
825
1368
|
}
|
|
826
1369
|
async log(entry) {
|
|
827
1370
|
try {
|
|
828
|
-
const record = { id:
|
|
1371
|
+
const record = { id: resolveEntryId(entry), ...serializeEntry(entry) };
|
|
829
1372
|
if (this.mode === "stream") {
|
|
830
|
-
await this.
|
|
1373
|
+
await this.send(this.buildStreamCommand(record));
|
|
831
1374
|
} else {
|
|
832
|
-
await this.
|
|
1375
|
+
await this.send([
|
|
833
1376
|
this.side === "left" ? "LPUSH" : "RPUSH",
|
|
834
1377
|
this.key,
|
|
835
1378
|
JSON.stringify(record)
|
|
@@ -838,11 +1381,12 @@ var RedisErrorAdapter = class {
|
|
|
838
1381
|
}
|
|
839
1382
|
return { ok: true };
|
|
840
1383
|
} catch (error) {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1384
|
+
return reportFailure({
|
|
1385
|
+
adapterName: "RedisErrorAdapter",
|
|
1386
|
+
prefix: this.prefix,
|
|
1387
|
+
action: `to write to ${this.mode} "${this.key}"`,
|
|
1388
|
+
error
|
|
1389
|
+
});
|
|
846
1390
|
}
|
|
847
1391
|
}
|
|
848
1392
|
buildStreamCommand(record) {
|
|
@@ -874,6 +1418,14 @@ var RedisErrorAdapter = class {
|
|
|
874
1418
|
* stored by now, so a trim failure is reported but does not fail the call;
|
|
875
1419
|
* the next successful write trims the list again.
|
|
876
1420
|
*/
|
|
1421
|
+
/** Sends one command, giving up waiting after `timeoutMs`. */
|
|
1422
|
+
send(args) {
|
|
1423
|
+
return withTimeout(
|
|
1424
|
+
() => this.client.sendCommand(args),
|
|
1425
|
+
this.timeoutMs,
|
|
1426
|
+
timeoutMessage("Redis", this.timeoutMs)
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
877
1429
|
async trimList() {
|
|
878
1430
|
if (this.maxEntries === void 0) {
|
|
879
1431
|
return;
|
|
@@ -881,7 +1433,7 @@ var RedisErrorAdapter = class {
|
|
|
881
1433
|
const keep = this.maxEntries;
|
|
882
1434
|
const [start, stop] = this.side === "right" ? [String(-keep), "-1"] : ["0", String(keep - 1)];
|
|
883
1435
|
try {
|
|
884
|
-
await this.
|
|
1436
|
+
await this.send(["LTRIM", this.key, start, stop]);
|
|
885
1437
|
} catch (error) {
|
|
886
1438
|
console.error(
|
|
887
1439
|
`${this.prefix} RedisErrorAdapter: stored the entry but could not trim list "${this.key}" to ${keep} entries: ${describeError(error)}`
|
|
@@ -908,11 +1460,12 @@ var S3ErrorAdapter = class {
|
|
|
908
1460
|
this.bucket = bucket;
|
|
909
1461
|
this.buffer = [];
|
|
910
1462
|
this.timer = null;
|
|
911
|
-
this.flushQueue =
|
|
1463
|
+
this.flushQueue = new SerialQueue();
|
|
912
1464
|
this.keyPrefix = normalizePrefix(options.keyPrefix ?? DEFAULT_S3_PREFIX);
|
|
913
1465
|
this.batch = options.batch;
|
|
914
1466
|
this.contentType = options.contentType ?? (this.batch === void 0 ? "application/json" : "application/x-ndjson");
|
|
915
|
-
this.logPrefix = options.prefix ??
|
|
1467
|
+
this.logPrefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1468
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "S3ErrorAdapter");
|
|
916
1469
|
if (bucket === "") {
|
|
917
1470
|
throw new RangeError("S3ErrorAdapter: bucket must not be empty.");
|
|
918
1471
|
}
|
|
@@ -932,7 +1485,7 @@ var S3ErrorAdapter = class {
|
|
|
932
1485
|
async log(entry) {
|
|
933
1486
|
try {
|
|
934
1487
|
const record = {
|
|
935
|
-
id:
|
|
1488
|
+
id: resolveEntryId(entry),
|
|
936
1489
|
...serializeEntry(entry)
|
|
937
1490
|
};
|
|
938
1491
|
const occurredAt = new Date(record.timestamp);
|
|
@@ -957,11 +1510,9 @@ var S3ErrorAdapter = class {
|
|
|
957
1510
|
* Entries from a failed upload stay buffered and are retried by the next flush.
|
|
958
1511
|
*/
|
|
959
1512
|
flush() {
|
|
960
|
-
|
|
1513
|
+
return this.flushQueue.run(
|
|
961
1514
|
() => this.uploadBuffered()
|
|
962
1515
|
);
|
|
963
|
-
this.flushQueue = result.catch(() => void 0);
|
|
964
|
-
return result;
|
|
965
1516
|
}
|
|
966
1517
|
async addToBatch(record, batch) {
|
|
967
1518
|
this.buffer.push(record);
|
|
@@ -1000,7 +1551,7 @@ var S3ErrorAdapter = class {
|
|
|
1000
1551
|
return { ok: true };
|
|
1001
1552
|
}
|
|
1002
1553
|
try {
|
|
1003
|
-
const key = `${this.keyPrefix}${datePath(/* @__PURE__ */ new Date())}-${
|
|
1554
|
+
const key = `${this.keyPrefix}${datePath(/* @__PURE__ */ new Date())}-${generateId()}.ndjson`;
|
|
1004
1555
|
const body = pending.map((record) => JSON.stringify(record)).join("\n");
|
|
1005
1556
|
await this.upload(key, `${body}
|
|
1006
1557
|
`);
|
|
@@ -1017,64 +1568,41 @@ var S3ErrorAdapter = class {
|
|
|
1017
1568
|
}
|
|
1018
1569
|
}
|
|
1019
1570
|
upload(key, body) {
|
|
1020
|
-
return
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1571
|
+
return withTimeout(
|
|
1572
|
+
() => this.client.putObject({
|
|
1573
|
+
Bucket: this.bucket,
|
|
1574
|
+
Key: key,
|
|
1575
|
+
Body: body,
|
|
1576
|
+
ContentType: this.contentType
|
|
1577
|
+
}),
|
|
1578
|
+
this.timeoutMs,
|
|
1579
|
+
timeoutMessage("S3", this.timeoutMs)
|
|
1580
|
+
);
|
|
1026
1581
|
}
|
|
1027
1582
|
fail(error, bufferedCount) {
|
|
1028
|
-
console.error(`${this.logPrefix} S3ErrorAdapter:`, error);
|
|
1029
1583
|
const retry = bufferedCount === void 0 ? "" : ` (${bufferedCount} buffered ${bufferedCount === 1 ? "entry was" : "entries were"} kept for the next flush)`;
|
|
1030
|
-
return {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1584
|
+
return reportFailure({
|
|
1585
|
+
adapterName: "S3ErrorAdapter",
|
|
1586
|
+
prefix: this.logPrefix,
|
|
1587
|
+
action: `to upload to bucket "${this.bucket}"`,
|
|
1588
|
+
error,
|
|
1589
|
+
reason: `${describeError(error)}${retry}`
|
|
1590
|
+
});
|
|
1034
1591
|
}
|
|
1035
1592
|
};
|
|
1036
1593
|
|
|
1037
1594
|
// src/adapters/firebase-error-adapter.ts
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
`${adapter}: timeoutMs must be a positive number or Infinity, received ${String(options.timeoutMs)}.`
|
|
1044
|
-
);
|
|
1045
|
-
}
|
|
1046
|
-
return timeoutMs;
|
|
1047
|
-
}
|
|
1048
|
-
function waitForWrite(write, timeoutMs) {
|
|
1049
|
-
if (timeoutMs === Infinity) {
|
|
1050
|
-
return Promise.resolve(write);
|
|
1051
|
-
}
|
|
1052
|
-
return new Promise(
|
|
1053
|
-
(resolve, reject) => {
|
|
1054
|
-
const timer = setTimeout(() => {
|
|
1055
|
-
reject(
|
|
1056
|
-
new Error(
|
|
1057
|
-
`the write was not saved within ${timeoutMs} ms (the connection may be down; the SDK keeps the write queued and may still save it later)`
|
|
1058
|
-
)
|
|
1059
|
-
);
|
|
1060
|
-
}, timeoutMs);
|
|
1061
|
-
write.then(
|
|
1062
|
-
(value) => {
|
|
1063
|
-
clearTimeout(timer);
|
|
1064
|
-
resolve(value);
|
|
1065
|
-
},
|
|
1066
|
-
(reason) => {
|
|
1067
|
-
clearTimeout(timer);
|
|
1068
|
-
reject(reason);
|
|
1069
|
-
}
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1595
|
+
function waitForWrite(start, timeoutMs) {
|
|
1596
|
+
return withTimeout(
|
|
1597
|
+
start,
|
|
1598
|
+
timeoutMs,
|
|
1599
|
+
`the write was not saved within ${timeoutMs} ms (the connection may be down; the SDK keeps the write queued and may still save it later, so logging the entry again could store it twice unless it carries an id)`
|
|
1072
1600
|
);
|
|
1073
1601
|
}
|
|
1074
1602
|
function buildRecord(entry) {
|
|
1075
1603
|
const serialized = serializeEntry(entry);
|
|
1076
1604
|
const record = {
|
|
1077
|
-
id:
|
|
1605
|
+
id: resolveEntryId(entry),
|
|
1078
1606
|
code: serialized.code,
|
|
1079
1607
|
message: serialized.message,
|
|
1080
1608
|
timestamp: serialized.timestamp
|
|
@@ -1091,23 +1619,27 @@ var FirestoreErrorAdapter = class {
|
|
|
1091
1619
|
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
|
|
1092
1620
|
constructor(collection, options = {}) {
|
|
1093
1621
|
this.collection = collection;
|
|
1094
|
-
this.prefix = options.prefix ??
|
|
1095
|
-
this.timeoutMs =
|
|
1622
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1623
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1624
|
+
options.timeoutMs,
|
|
1625
|
+
"FirestoreErrorAdapter"
|
|
1626
|
+
);
|
|
1096
1627
|
}
|
|
1097
1628
|
async log(entry) {
|
|
1098
1629
|
try {
|
|
1099
1630
|
const record = buildRecord(entry);
|
|
1100
1631
|
await waitForWrite(
|
|
1101
|
-
this.collection.doc(record.id).set(record),
|
|
1632
|
+
() => this.collection.doc(record.id).set(record),
|
|
1102
1633
|
this.timeoutMs
|
|
1103
1634
|
);
|
|
1104
1635
|
return { ok: true };
|
|
1105
1636
|
} catch (error) {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1637
|
+
return reportFailure({
|
|
1638
|
+
adapterName: "FirestoreErrorAdapter",
|
|
1639
|
+
prefix: this.prefix,
|
|
1640
|
+
action: `to write the document`,
|
|
1641
|
+
error
|
|
1642
|
+
});
|
|
1111
1643
|
}
|
|
1112
1644
|
}
|
|
1113
1645
|
};
|
|
@@ -1115,34 +1647,51 @@ var RealtimeDatabaseErrorAdapter = class {
|
|
|
1115
1647
|
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
|
|
1116
1648
|
constructor(reference, options = {}) {
|
|
1117
1649
|
this.reference = reference;
|
|
1118
|
-
this.prefix = options.prefix ??
|
|
1119
|
-
this.timeoutMs =
|
|
1650
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1651
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1652
|
+
options.timeoutMs,
|
|
1653
|
+
"RealtimeDatabaseErrorAdapter"
|
|
1654
|
+
);
|
|
1120
1655
|
}
|
|
1121
1656
|
async log(entry) {
|
|
1122
1657
|
try {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1658
|
+
const record = buildRecord(entry);
|
|
1659
|
+
let write;
|
|
1660
|
+
if (hasCallerId(entry)) {
|
|
1661
|
+
if (this.reference.child === void 0) {
|
|
1662
|
+
throw new TypeError(
|
|
1663
|
+
"the entry has an id, which needs a reference with child(key).set(value) so a repeat overwrites the first write; add child to the reference or log without an id"
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
write = this.reference.child(record.id).set(record);
|
|
1667
|
+
} else {
|
|
1668
|
+
write = this.reference.push(record);
|
|
1669
|
+
}
|
|
1670
|
+
await waitForWrite(() => write, this.timeoutMs);
|
|
1127
1671
|
return { ok: true };
|
|
1128
1672
|
} catch (error) {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1673
|
+
return reportFailure({
|
|
1674
|
+
adapterName: "RealtimeDatabaseErrorAdapter",
|
|
1675
|
+
prefix: this.prefix,
|
|
1676
|
+
action: `to write the entry`,
|
|
1677
|
+
error
|
|
1678
|
+
});
|
|
1134
1679
|
}
|
|
1135
1680
|
}
|
|
1136
1681
|
};
|
|
1137
1682
|
|
|
1138
1683
|
// src/adapters/supabase-error-adapter.ts
|
|
1139
|
-
var
|
|
1140
|
-
|
|
1684
|
+
var POLICY_MAX_CODE_LENGTH = 255;
|
|
1685
|
+
var POLICY_MAX_MESSAGE_LENGTH = 4096;
|
|
1686
|
+
var POLICY_MAX_CAUSE_BYTES = 8192;
|
|
1687
|
+
var INSERT_POLICY_CHECK = `char_length(code) <= ${POLICY_MAX_CODE_LENGTH} AND char_length(message) <= ${POLICY_MAX_MESSAGE_LENGTH} AND (source IS NULL OR char_length(source) <= ${POLICY_MAX_CODE_LENGTH}) AND (cause IS NULL OR pg_column_size(cause) <= ${POLICY_MAX_CAUSE_BYTES})`;
|
|
1688
|
+
function buildSupabaseSetupSql(tableName = DEFAULT_TABLE_NAME, options = {}) {
|
|
1141
1689
|
const insertRoles = options.insertRoles ?? [];
|
|
1142
1690
|
for (const role of insertRoles) {
|
|
1143
|
-
|
|
1691
|
+
assertIdentifier(role, "role name", "buildSupabaseSetupSql");
|
|
1692
|
+
if ((role.toLowerCase() === "anon" || role.toLowerCase() === "public") && options.allowPublicInsert !== true) {
|
|
1144
1693
|
throw new RangeError(
|
|
1145
|
-
`buildSupabaseSetupSql:
|
|
1694
|
+
`buildSupabaseSetupSql: role "${role}" would let anyone who loads your page write to the log, because the anon key is public. Use "authenticated" instead, or pass allowPublicInsert: true to accept the risk of forged entries and quota use.`
|
|
1146
1695
|
);
|
|
1147
1696
|
}
|
|
1148
1697
|
}
|
|
@@ -1154,7 +1703,7 @@ function buildSupabaseSetupSql(tableName = DEFAULT_TABLE_NAME2, options = {}) {
|
|
|
1154
1703
|
const policy = `${tableName}_insert_only`;
|
|
1155
1704
|
statements.push(
|
|
1156
1705
|
`DROP POLICY IF EXISTS "${policy}" ON "${tableName}"`,
|
|
1157
|
-
`CREATE POLICY "${policy}" ON "${tableName}" FOR INSERT TO ${insertRoles.map((role) => `"${role}"`).join(", ")} WITH CHECK (
|
|
1706
|
+
`CREATE POLICY "${policy}" ON "${tableName}" FOR INSERT TO ${insertRoles.map((role) => `"${role}"`).join(", ")} WITH CHECK (${INSERT_POLICY_CHECK})`
|
|
1158
1707
|
);
|
|
1159
1708
|
}
|
|
1160
1709
|
return statements;
|
|
@@ -1184,50 +1733,98 @@ function describeSupabaseError(result) {
|
|
|
1184
1733
|
return parts.join("; ");
|
|
1185
1734
|
}
|
|
1186
1735
|
var SupabaseErrorAdapter = class {
|
|
1187
|
-
/** @throws {RangeError} if `options.tableName` is
|
|
1736
|
+
/** @throws {RangeError} if `options.tableName` is not a plain identifier or `options.timeoutMs` is invalid. */
|
|
1188
1737
|
constructor(client, options = {}) {
|
|
1189
1738
|
this.client = client;
|
|
1190
|
-
this.tableName = options.tableName ??
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1739
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
1740
|
+
assertIdentifier(this.tableName, "table name", "SupabaseErrorAdapter");
|
|
1741
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1742
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1743
|
+
options.timeoutMs,
|
|
1744
|
+
"SupabaseErrorAdapter"
|
|
1745
|
+
);
|
|
1197
1746
|
}
|
|
1198
1747
|
async log(entry) {
|
|
1199
1748
|
try {
|
|
1200
1749
|
const serialized = serializeEntry(entry);
|
|
1201
1750
|
const row = {
|
|
1202
|
-
id:
|
|
1751
|
+
id: resolveEntryId(entry),
|
|
1203
1752
|
code: serialized.code,
|
|
1204
1753
|
message: serialized.message,
|
|
1205
1754
|
cause: serialized.cause === void 0 ? null : serialized.cause,
|
|
1206
1755
|
source: serialized.source ?? null,
|
|
1207
1756
|
occurred_at: serialized.timestamp
|
|
1208
1757
|
};
|
|
1209
|
-
const result = await
|
|
1758
|
+
const result = await withTimeout(
|
|
1759
|
+
() => this.client.from(this.tableName).insert(row),
|
|
1760
|
+
this.timeoutMs,
|
|
1761
|
+
timeoutMessage("Supabase", this.timeoutMs)
|
|
1762
|
+
);
|
|
1210
1763
|
if (result.error) {
|
|
1211
|
-
|
|
1212
|
-
|
|
1764
|
+
if (hasCallerId(entry) && result.error.code === "23505") {
|
|
1765
|
+
return { ok: true };
|
|
1766
|
+
}
|
|
1767
|
+
return this.fail(result, describeSupabaseError(result));
|
|
1213
1768
|
}
|
|
1214
1769
|
return { ok: true };
|
|
1215
1770
|
} catch (error) {
|
|
1216
|
-
|
|
1217
|
-
return this.fail(describeError(error));
|
|
1771
|
+
return this.fail(error, describeError(error));
|
|
1218
1772
|
}
|
|
1219
1773
|
}
|
|
1220
|
-
fail(reason) {
|
|
1221
|
-
return {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1774
|
+
fail(logged, reason) {
|
|
1775
|
+
return reportFailure({
|
|
1776
|
+
adapterName: "SupabaseErrorAdapter",
|
|
1777
|
+
prefix: this.prefix,
|
|
1778
|
+
action: `to insert into table "${this.tableName}"`,
|
|
1779
|
+
error: logged,
|
|
1780
|
+
reason
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1785
|
+
// src/adapters/event-emitter-error-adapter.ts
|
|
1786
|
+
var DEFAULT_EVENT_NAME = "error-log";
|
|
1787
|
+
var EventEmitterErrorAdapter = class {
|
|
1788
|
+
/** @throws {RangeError} if `options.eventName` is empty. */
|
|
1789
|
+
constructor(emitter, options = {}) {
|
|
1790
|
+
this.emitter = emitter;
|
|
1791
|
+
this.eventName = options.eventName ?? DEFAULT_EVENT_NAME;
|
|
1792
|
+
if (this.eventName === "") {
|
|
1793
|
+
throw new RangeError(
|
|
1794
|
+
"EventEmitterErrorAdapter: eventName must not be empty."
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
this.requireListener = options.requireListener ?? false;
|
|
1798
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1799
|
+
}
|
|
1800
|
+
async log(entry) {
|
|
1801
|
+
try {
|
|
1802
|
+
const record = {
|
|
1803
|
+
id: resolveEntryId(entry),
|
|
1804
|
+
...serializeEntry(entry)
|
|
1805
|
+
};
|
|
1806
|
+
const delivered = this.emitter.emit(this.eventName, record);
|
|
1807
|
+
if (this.requireListener && delivered === false) {
|
|
1808
|
+
throw new Error(
|
|
1809
|
+
`no listener is attached to "${this.eventName}", so nothing received the entry`
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
return { ok: true };
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
return reportFailure({
|
|
1815
|
+
adapterName: "EventEmitterErrorAdapter",
|
|
1816
|
+
prefix: this.prefix,
|
|
1817
|
+
action: `to emit "${this.eventName}"`,
|
|
1818
|
+
error
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1225
1821
|
}
|
|
1226
1822
|
};
|
|
1227
1823
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1228
1824
|
0 && (module.exports = {
|
|
1229
1825
|
CompositeErrorAdapter,
|
|
1230
1826
|
ConsoleErrorAdapter,
|
|
1827
|
+
EventEmitterErrorAdapter,
|
|
1231
1828
|
FirestoreErrorAdapter,
|
|
1232
1829
|
FlatFileErrorAdapter,
|
|
1233
1830
|
IndexedDbErrorAdapter,
|
|
@@ -1244,6 +1841,7 @@ var SupabaseErrorAdapter = class {
|
|
|
1244
1841
|
WebhookErrorAdapter,
|
|
1245
1842
|
buildCreateErrorLogTableSql,
|
|
1246
1843
|
buildErrorLogTableDdl,
|
|
1844
|
+
buildMySqlWidenColumnsSql,
|
|
1247
1845
|
buildSupabaseSetupSql,
|
|
1248
1846
|
createErrorLogStore,
|
|
1249
1847
|
formatErrorLogLine,
|