@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.mjs
CHANGED
|
@@ -1,9 +1,98 @@
|
|
|
1
1
|
// src/serialize.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
var MAX_CAUSE_DEPTH = 6;
|
|
3
|
+
function isErrorLike(value) {
|
|
4
|
+
if (value instanceof Error) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
const candidate = value;
|
|
8
|
+
return typeof candidate.name === "string" && typeof candidate.message === "string" && typeof value.stack === "string";
|
|
9
|
+
}
|
|
10
|
+
function readProperty(source, key) {
|
|
11
|
+
try {
|
|
12
|
+
return source[key];
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return `[Unreadable: ${error instanceof Error ? error.message : String(error)}]`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function toJsonSafe(value, depth, ancestors) {
|
|
18
|
+
switch (typeof value) {
|
|
19
|
+
case "string":
|
|
20
|
+
case "boolean":
|
|
21
|
+
case "undefined":
|
|
22
|
+
return value;
|
|
23
|
+
case "number":
|
|
24
|
+
return Number.isFinite(value) ? value : String(value);
|
|
25
|
+
case "bigint":
|
|
26
|
+
return `${value}n`;
|
|
27
|
+
case "symbol":
|
|
28
|
+
return value.toString();
|
|
29
|
+
case "function":
|
|
30
|
+
return `[Function ${value.name || "anonymous"}]`;
|
|
31
|
+
default:
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
if (value === null) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const objectValue = value;
|
|
38
|
+
if (ancestors.has(objectValue)) {
|
|
39
|
+
return "[Circular]";
|
|
40
|
+
}
|
|
41
|
+
if (depth >= MAX_CAUSE_DEPTH) {
|
|
42
|
+
return "[Truncated]";
|
|
43
|
+
}
|
|
44
|
+
ancestors.add(objectValue);
|
|
45
|
+
try {
|
|
46
|
+
if (isErrorLike(objectValue)) {
|
|
47
|
+
const result2 = {
|
|
48
|
+
name: objectValue.name,
|
|
49
|
+
message: objectValue.message,
|
|
50
|
+
stack: objectValue.stack
|
|
51
|
+
};
|
|
52
|
+
const nested = readProperty(objectValue, "cause");
|
|
53
|
+
if (nested !== void 0) {
|
|
54
|
+
result2["cause"] = toJsonSafe(nested, depth + 1, ancestors);
|
|
55
|
+
}
|
|
56
|
+
const errors = readProperty(objectValue, "errors");
|
|
57
|
+
if (Array.isArray(errors)) {
|
|
58
|
+
result2["errors"] = errors.map(
|
|
59
|
+
(item) => toJsonSafe(item, depth + 1, ancestors)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return result2;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
return value.map(
|
|
66
|
+
(item) => toJsonSafe(item, depth + 1, ancestors)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const toJson = readProperty(objectValue, "toJSON");
|
|
70
|
+
if (typeof toJson === "function") {
|
|
71
|
+
try {
|
|
72
|
+
return toJsonSafe(
|
|
73
|
+
toJson.call(objectValue),
|
|
74
|
+
depth + 1,
|
|
75
|
+
ancestors
|
|
76
|
+
);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return `[toJSON failed: ${error instanceof Error ? error.message : String(error)}]`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const result = {};
|
|
82
|
+
for (const key of Object.keys(objectValue)) {
|
|
83
|
+
result[key] = toJsonSafe(
|
|
84
|
+
readProperty(objectValue, key),
|
|
85
|
+
depth + 1,
|
|
86
|
+
ancestors
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
} finally {
|
|
91
|
+
ancestors.delete(objectValue);
|
|
5
92
|
}
|
|
6
|
-
|
|
93
|
+
}
|
|
94
|
+
function serializeCause(cause) {
|
|
95
|
+
return toJsonSafe(cause, 0, /* @__PURE__ */ new WeakSet());
|
|
7
96
|
}
|
|
8
97
|
function serializeEntry(entry) {
|
|
9
98
|
const serialized = {
|
|
@@ -23,10 +112,20 @@ function describeError(error) {
|
|
|
23
112
|
return error instanceof Error ? error.message : String(error);
|
|
24
113
|
}
|
|
25
114
|
|
|
115
|
+
// src/fail.ts
|
|
116
|
+
var DEFAULT_PREFIX = "[error-log-adapter]";
|
|
117
|
+
function reportFailure(report) {
|
|
118
|
+
console.error(`${report.prefix} ${report.adapterName}:`, report.error);
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
error: `${report.adapterName} failed ${report.action}: ${report.reason ?? describeError(report.error)}`
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
26
125
|
// src/adapters/console-error-adapter.ts
|
|
27
126
|
var ConsoleErrorAdapter = class {
|
|
28
127
|
constructor(options = {}) {
|
|
29
|
-
this.prefix = options.prefix ??
|
|
128
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
30
129
|
}
|
|
31
130
|
async log(entry) {
|
|
32
131
|
try {
|
|
@@ -41,35 +140,324 @@ var ConsoleErrorAdapter = class {
|
|
|
41
140
|
}
|
|
42
141
|
};
|
|
43
142
|
|
|
143
|
+
// src/uuid.ts
|
|
144
|
+
function generateId() {
|
|
145
|
+
const cryptoApi = globalThis.crypto;
|
|
146
|
+
if (cryptoApi !== void 0 && typeof cryptoApi.randomUUID === "function") {
|
|
147
|
+
return cryptoApi.randomUUID();
|
|
148
|
+
}
|
|
149
|
+
const bytes = new Uint8Array(16);
|
|
150
|
+
if (cryptoApi !== void 0 && typeof cryptoApi.getRandomValues === "function") {
|
|
151
|
+
cryptoApi.getRandomValues(bytes);
|
|
152
|
+
} else {
|
|
153
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
154
|
+
bytes[index] = Math.floor(Math.random() * 256);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
158
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
159
|
+
const hex = Array.from(
|
|
160
|
+
bytes,
|
|
161
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
162
|
+
).join("");
|
|
163
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/entry-id.ts
|
|
167
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
168
|
+
function hasCallerId(entry) {
|
|
169
|
+
return entry.id !== void 0;
|
|
170
|
+
}
|
|
171
|
+
function resolveEntryId(entry) {
|
|
172
|
+
if (entry.id === void 0) {
|
|
173
|
+
return generateId();
|
|
174
|
+
}
|
|
175
|
+
if (typeof entry.id !== "string" || !UUID_PATTERN.test(entry.id)) {
|
|
176
|
+
throw new RangeError(
|
|
177
|
+
`entry.id must be a UUID such as one from crypto.randomUUID(), received ${JSON.stringify(entry.id)}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return entry.id.toLowerCase();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/timeout.ts
|
|
184
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
185
|
+
var SinkTimeoutError = class extends Error {
|
|
186
|
+
constructor(message) {
|
|
187
|
+
super(message);
|
|
188
|
+
this.name = "SinkTimeoutError";
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
function resolveTimeoutMs(value, adapterName) {
|
|
192
|
+
const timeoutMs = value ?? DEFAULT_TIMEOUT_MS;
|
|
193
|
+
if (Number.isNaN(timeoutMs) || timeoutMs <= 0) {
|
|
194
|
+
throw new RangeError(
|
|
195
|
+
`${adapterName}: timeoutMs must be a positive number or Infinity, received ${String(value)}.`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return timeoutMs;
|
|
199
|
+
}
|
|
200
|
+
function timeoutMessage(sink, timeoutMs, action = "respond") {
|
|
201
|
+
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)`;
|
|
202
|
+
}
|
|
203
|
+
function withTimeout(start, timeoutMs, message) {
|
|
204
|
+
if (timeoutMs === Infinity) {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
try {
|
|
207
|
+
resolve(start());
|
|
208
|
+
} catch (error) {
|
|
209
|
+
reject(error);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return new Promise(
|
|
214
|
+
(resolve, reject) => {
|
|
215
|
+
const timer = setTimeout(() => {
|
|
216
|
+
reject(new SinkTimeoutError(message));
|
|
217
|
+
}, timeoutMs);
|
|
218
|
+
const settle = (done) => (...args) => {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
done(...args);
|
|
221
|
+
};
|
|
222
|
+
try {
|
|
223
|
+
Promise.resolve(start()).then(settle(resolve), settle(reject));
|
|
224
|
+
} catch (error) {
|
|
225
|
+
settle(reject)(error);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
44
231
|
// src/adapters/webhook-error-adapter.ts
|
|
232
|
+
var DEFAULT_RETRY_DELAY_MS = 500;
|
|
233
|
+
var DEFAULT_MAX_RETRY_DELAY_MS = 3e4;
|
|
234
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
235
|
+
var SigningUnavailableError = class extends Error {
|
|
236
|
+
};
|
|
237
|
+
async function hmacSha256Hex(secret, message) {
|
|
238
|
+
const subtle = globalThis.crypto?.subtle;
|
|
239
|
+
if (subtle === void 0) {
|
|
240
|
+
throw new SigningUnavailableError(
|
|
241
|
+
"signingSecret needs Web Crypto (crypto.subtle), which this runtime does not provide, so the entry was not sent unsigned"
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const encoder = new TextEncoder();
|
|
245
|
+
const key = await subtle.importKey(
|
|
246
|
+
"raw",
|
|
247
|
+
encoder.encode(secret),
|
|
248
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
249
|
+
false,
|
|
250
|
+
["sign"]
|
|
251
|
+
);
|
|
252
|
+
const signature = await subtle.sign(
|
|
253
|
+
"HMAC",
|
|
254
|
+
key,
|
|
255
|
+
encoder.encode(message)
|
|
256
|
+
);
|
|
257
|
+
return Array.from(
|
|
258
|
+
new Uint8Array(signature),
|
|
259
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
260
|
+
).join("");
|
|
261
|
+
}
|
|
262
|
+
function parseRetryAfterMs(value) {
|
|
263
|
+
if (value === null || value === void 0 || value.trim() === "") {
|
|
264
|
+
return void 0;
|
|
265
|
+
}
|
|
266
|
+
const seconds = Number(value);
|
|
267
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
268
|
+
return seconds * 1e3;
|
|
269
|
+
}
|
|
270
|
+
const date = Date.parse(value);
|
|
271
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
|
|
272
|
+
}
|
|
273
|
+
function validateRetryOptions(options) {
|
|
274
|
+
const retries = options.retries ?? 0;
|
|
275
|
+
if (!Number.isInteger(retries) || retries < 0) {
|
|
276
|
+
throw new RangeError(
|
|
277
|
+
`WebhookErrorAdapter: retries must be a whole number of 0 or more, received ${String(options.retries)}.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
281
|
+
if (!Number.isFinite(retryDelayMs) || retryDelayMs <= 0) {
|
|
282
|
+
throw new RangeError(
|
|
283
|
+
`WebhookErrorAdapter: retryDelayMs must be a positive number, received ${String(options.retryDelayMs)}.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const maxRetryDelayMs = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
287
|
+
if (!Number.isFinite(maxRetryDelayMs) || maxRetryDelayMs < retryDelayMs) {
|
|
288
|
+
throw new RangeError(
|
|
289
|
+
`WebhookErrorAdapter: maxRetryDelayMs must be a number no smaller than retryDelayMs (${retryDelayMs}), received ${String(options.maxRetryDelayMs)}.`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (options.signingSecret === "") {
|
|
293
|
+
throw new RangeError(
|
|
294
|
+
"WebhookErrorAdapter: signingSecret must not be empty; leave it out to send unsigned."
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return { retries, retryDelayMs, maxRetryDelayMs };
|
|
298
|
+
}
|
|
299
|
+
function sleep(ms) {
|
|
300
|
+
return new Promise((resolve) => {
|
|
301
|
+
setTimeout(resolve, ms);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
305
|
+
"localhost",
|
|
306
|
+
"127.0.0.1",
|
|
307
|
+
"[::1]"
|
|
308
|
+
]);
|
|
309
|
+
function parseWebhookUrl(url, allowInsecure) {
|
|
310
|
+
let parsed;
|
|
311
|
+
try {
|
|
312
|
+
parsed = new URL(url);
|
|
313
|
+
} catch {
|
|
314
|
+
throw new RangeError(
|
|
315
|
+
"WebhookErrorAdapter: the webhook URL is not a valid absolute URL (for example https://example.com/errors)."
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
if (parsed.protocol === "https:") {
|
|
319
|
+
return parsed;
|
|
320
|
+
}
|
|
321
|
+
if (parsed.protocol === "http:" && (allowInsecure || LOOPBACK_HOSTS.has(parsed.hostname))) {
|
|
322
|
+
return parsed;
|
|
323
|
+
}
|
|
324
|
+
throw new RangeError(
|
|
325
|
+
`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.`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
45
328
|
var WebhookErrorAdapter = class {
|
|
329
|
+
/** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or a timeout, retry or signing option is invalid. */
|
|
46
330
|
constructor(url, options = {}) {
|
|
47
331
|
this.url = url;
|
|
48
332
|
this.options = options;
|
|
49
|
-
this.prefix = options.prefix ??
|
|
333
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
334
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "WebhookErrorAdapter");
|
|
335
|
+
({
|
|
336
|
+
retries: this.retries,
|
|
337
|
+
retryDelayMs: this.retryDelayMs,
|
|
338
|
+
maxRetryDelayMs: this.maxRetryDelayMs
|
|
339
|
+
} = validateRetryOptions(options));
|
|
340
|
+
this.target = parseWebhookUrl(url, options.allowInsecure ?? false).origin;
|
|
50
341
|
}
|
|
51
342
|
async log(entry) {
|
|
343
|
+
let body;
|
|
344
|
+
try {
|
|
345
|
+
body = JSON.stringify(
|
|
346
|
+
hasCallerId(entry) || this.retries > 0 ? { id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry)
|
|
347
|
+
);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
350
|
+
return this.fail(
|
|
351
|
+
`could not prepare the entry: ${this.redact(describeError(error))}`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
let attempt = await this.send(body);
|
|
355
|
+
let attempts = 1;
|
|
356
|
+
while (!attempt.ok && attempt.retryable && attempts <= this.retries) {
|
|
357
|
+
await sleep(this.retryWaitMs(attempts, attempt.retryAfterMs));
|
|
358
|
+
attempt = await this.send(body);
|
|
359
|
+
attempts += 1;
|
|
360
|
+
}
|
|
361
|
+
if (attempt.ok) {
|
|
362
|
+
return { ok: true };
|
|
363
|
+
}
|
|
364
|
+
return this.fail(
|
|
365
|
+
attempts > 1 ? `${attempt.reason} (after ${attempts} attempts)` : attempt.reason
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
/** Milliseconds to wait before retry number `retryNumber` (1 for the first). */
|
|
369
|
+
retryWaitMs(retryNumber, retryAfterMs) {
|
|
370
|
+
if (retryAfterMs !== void 0) {
|
|
371
|
+
return Math.min(retryAfterMs, this.maxRetryDelayMs);
|
|
372
|
+
}
|
|
373
|
+
const ceiling = Math.min(
|
|
374
|
+
this.maxRetryDelayMs,
|
|
375
|
+
this.retryDelayMs * 2 ** (retryNumber - 1)
|
|
376
|
+
);
|
|
377
|
+
return ceiling / 2 + Math.random() * ceiling / 2;
|
|
378
|
+
}
|
|
379
|
+
async send(body) {
|
|
380
|
+
const controller = new AbortController();
|
|
381
|
+
let timedOut = false;
|
|
382
|
+
const timer = this.timeoutMs === Infinity ? void 0 : setTimeout(() => {
|
|
383
|
+
timedOut = true;
|
|
384
|
+
controller.abort();
|
|
385
|
+
}, this.timeoutMs);
|
|
52
386
|
try {
|
|
53
387
|
const response = await fetch(this.url, {
|
|
54
388
|
method: "POST",
|
|
55
389
|
headers: {
|
|
56
390
|
"Content-Type": "application/json",
|
|
57
|
-
...this.options.headers
|
|
391
|
+
...this.options.headers,
|
|
392
|
+
...await this.signatureHeaders(body)
|
|
58
393
|
},
|
|
59
|
-
body
|
|
394
|
+
body,
|
|
395
|
+
// A redirect would turn the POST into a GET without a body (302, 303)
|
|
396
|
+
// or re-send the entry and headers to another host (307, 308).
|
|
397
|
+
redirect: "manual",
|
|
398
|
+
signal: controller.signal
|
|
60
399
|
});
|
|
400
|
+
if (response.type === "opaqueredirect") {
|
|
401
|
+
return this.rejected(this.redirectMessage("a redirect"));
|
|
402
|
+
}
|
|
61
403
|
if (!response.ok) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
404
|
+
if (response.status >= 300 && response.status < 400) {
|
|
405
|
+
return this.rejected(this.redirectMessage(`HTTP ${response.status}`));
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
ok: false,
|
|
409
|
+
reason: `webhook ${this.target} responded with HTTP ${response.status}`,
|
|
410
|
+
retryable: response.status >= 500 || RETRYABLE_STATUSES.has(response.status),
|
|
411
|
+
retryAfterMs: parseRetryAfterMs(response.headers?.get("retry-after"))
|
|
412
|
+
};
|
|
65
413
|
}
|
|
66
414
|
return { ok: true };
|
|
67
415
|
} catch (error) {
|
|
68
416
|
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
417
|
+
if (error instanceof SigningUnavailableError) {
|
|
418
|
+
return this.rejected(error.message);
|
|
419
|
+
}
|
|
420
|
+
if (timedOut) {
|
|
421
|
+
return {
|
|
422
|
+
ok: false,
|
|
423
|
+
reason: timeoutMessage(`webhook ${this.target}`, this.timeoutMs),
|
|
424
|
+
retryable: true
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
ok: false,
|
|
429
|
+
reason: `could not reach webhook ${this.target}: ${this.redact(describeError(error))}`,
|
|
430
|
+
retryable: true
|
|
431
|
+
};
|
|
432
|
+
} finally {
|
|
433
|
+
clearTimeout(timer);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/** Timestamp and signature headers, or none when no `signingSecret` is set. */
|
|
437
|
+
async signatureHeaders(body) {
|
|
438
|
+
const secret = this.options.signingSecret;
|
|
439
|
+
if (secret === void 0) {
|
|
440
|
+
return {};
|
|
72
441
|
}
|
|
442
|
+
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
443
|
+
const signature = await hmacSha256Hex(
|
|
444
|
+
secret,
|
|
445
|
+
`${timestamp}.${body}`
|
|
446
|
+
);
|
|
447
|
+
return {
|
|
448
|
+
"X-Signature-Timestamp": timestamp,
|
|
449
|
+
"X-Signature-256": `sha256=${signature}`
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
rejected(reason) {
|
|
453
|
+
return { ok: false, reason, retryable: false };
|
|
454
|
+
}
|
|
455
|
+
redirectMessage(what) {
|
|
456
|
+
return `webhook ${this.target} responded with ${what}, which is not followed because the entry would not arrive intact; use the final URL instead`;
|
|
457
|
+
}
|
|
458
|
+
/** Replaces the full URL in a message with its origin, in case the runtime put it there. */
|
|
459
|
+
redact(message) {
|
|
460
|
+
return message.split(this.url).join(this.target);
|
|
73
461
|
}
|
|
74
462
|
fail(reason) {
|
|
75
463
|
return { ok: false, error: `WebhookErrorAdapter failed: ${reason}` };
|
|
@@ -77,37 +465,64 @@ var WebhookErrorAdapter = class {
|
|
|
77
465
|
};
|
|
78
466
|
|
|
79
467
|
// src/adapters/mongodb-error-adapter.ts
|
|
468
|
+
function isDuplicateKeyError(error) {
|
|
469
|
+
return typeof error === "object" && error !== null && error.code === 11e3;
|
|
470
|
+
}
|
|
80
471
|
var MongoDbErrorAdapter = class {
|
|
472
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
81
473
|
constructor(collection, options = {}) {
|
|
82
474
|
this.collection = collection;
|
|
83
|
-
this.prefix = options.prefix ??
|
|
475
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
476
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "MongoDbErrorAdapter");
|
|
84
477
|
}
|
|
85
478
|
async log(entry) {
|
|
86
479
|
try {
|
|
87
|
-
|
|
480
|
+
const document = hasCallerId(entry) ? { _id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry);
|
|
481
|
+
await withTimeout(
|
|
482
|
+
() => this.collection.insertOne(document),
|
|
483
|
+
this.timeoutMs,
|
|
484
|
+
timeoutMessage("MongoDB", this.timeoutMs)
|
|
485
|
+
);
|
|
88
486
|
return { ok: true };
|
|
89
487
|
} catch (error) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
488
|
+
if (hasCallerId(entry) && isDuplicateKeyError(error)) {
|
|
489
|
+
return { ok: true };
|
|
490
|
+
}
|
|
491
|
+
return reportFailure({
|
|
492
|
+
adapterName: "MongoDbErrorAdapter",
|
|
493
|
+
prefix: this.prefix,
|
|
494
|
+
action: "to insert entry",
|
|
495
|
+
error
|
|
496
|
+
});
|
|
95
497
|
}
|
|
96
498
|
}
|
|
97
499
|
};
|
|
98
500
|
|
|
99
501
|
// src/adapters/composite-error-adapter.ts
|
|
502
|
+
var DEFAULT_COMPOSITE_TIMEOUT_MS = DEFAULT_TIMEOUT_MS * 3;
|
|
100
503
|
var CompositeErrorAdapter = class {
|
|
101
|
-
|
|
504
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
505
|
+
constructor(adapters, options = {}) {
|
|
102
506
|
this.adapters = adapters;
|
|
507
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
508
|
+
options.timeoutMs ?? DEFAULT_COMPOSITE_TIMEOUT_MS,
|
|
509
|
+
"CompositeErrorAdapter"
|
|
510
|
+
);
|
|
103
511
|
}
|
|
104
512
|
async log(entry) {
|
|
105
513
|
const results = await Promise.all(
|
|
106
514
|
this.adapters.map(
|
|
107
|
-
async (adapter) => {
|
|
515
|
+
async (adapter, index) => {
|
|
108
516
|
try {
|
|
109
|
-
return await
|
|
517
|
+
return await withTimeout(
|
|
518
|
+
() => adapter.log(entry),
|
|
519
|
+
this.timeoutMs,
|
|
520
|
+
`adapter ${index} (${adapter.constructor.name}) did not return within ${this.timeoutMs} ms, so the entry may or may not have been stored there`
|
|
521
|
+
);
|
|
110
522
|
} catch (error) {
|
|
523
|
+
if (error instanceof SinkTimeoutError) {
|
|
524
|
+
return { ok: false, error: error.message };
|
|
525
|
+
}
|
|
111
526
|
return {
|
|
112
527
|
ok: false,
|
|
113
528
|
error: `adapter threw instead of returning a result: ${String(error)}`
|
|
@@ -138,58 +553,101 @@ function createErrorLogStore(database, storeName = DEFAULT_STORE_NAME) {
|
|
|
138
553
|
database.createObjectStore(storeName, { keyPath: "id" }).createIndex("timestamp", "timestamp");
|
|
139
554
|
}
|
|
140
555
|
var IndexedDbErrorAdapter = class {
|
|
556
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
141
557
|
constructor(database, options = {}) {
|
|
142
558
|
this.database = database;
|
|
143
559
|
this.storeName = options.storeName ?? DEFAULT_STORE_NAME;
|
|
144
|
-
this.prefix = options.prefix ??
|
|
560
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
561
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
562
|
+
options.timeoutMs,
|
|
563
|
+
"IndexedDbErrorAdapter"
|
|
564
|
+
);
|
|
145
565
|
}
|
|
146
566
|
async log(entry) {
|
|
147
567
|
try {
|
|
148
568
|
const record = {
|
|
149
|
-
id:
|
|
569
|
+
id: resolveEntryId(entry),
|
|
150
570
|
...serializeEntry(entry)
|
|
151
571
|
};
|
|
152
|
-
await this.write(record);
|
|
572
|
+
await this.write(record, hasCallerId(entry));
|
|
153
573
|
return { ok: true };
|
|
154
574
|
} catch (error) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
575
|
+
return reportFailure({
|
|
576
|
+
adapterName: "IndexedDbErrorAdapter",
|
|
577
|
+
prefix: this.prefix,
|
|
578
|
+
action: `to write to object store "${this.storeName}"`,
|
|
579
|
+
error
|
|
580
|
+
});
|
|
160
581
|
}
|
|
161
582
|
}
|
|
162
|
-
/**
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
583
|
+
/**
|
|
584
|
+
* Resolves once the transaction commits; rejects if the request fails, the
|
|
585
|
+
* transaction aborts, or it does not finish within `timeoutMs`.
|
|
586
|
+
*
|
|
587
|
+
* Error events are handled here (`preventDefault` and `stopPropagation`), so a
|
|
588
|
+
* failed log write does not reach the `onerror` handler an app may have set on
|
|
589
|
+
* its own database object.
|
|
590
|
+
*/
|
|
591
|
+
write(record, replaceExisting) {
|
|
592
|
+
return withTimeout(
|
|
593
|
+
() => new Promise(
|
|
594
|
+
(resolve, reject) => {
|
|
595
|
+
const transaction = this.database.transaction(
|
|
596
|
+
this.storeName,
|
|
597
|
+
"readwrite"
|
|
598
|
+
);
|
|
599
|
+
const fail = (what, source) => (event) => {
|
|
600
|
+
event?.preventDefault?.();
|
|
601
|
+
event?.stopPropagation?.();
|
|
602
|
+
reject(new Error(`${what}: ${describeDomError(source.error)}`));
|
|
603
|
+
};
|
|
604
|
+
transaction.oncomplete = () => resolve();
|
|
605
|
+
transaction.onerror = fail("transaction failed", transaction);
|
|
606
|
+
transaction.onabort = fail("transaction aborted", transaction);
|
|
607
|
+
const store = transaction.objectStore(
|
|
608
|
+
this.storeName
|
|
609
|
+
);
|
|
610
|
+
const request = replaceExisting && typeof store.put === "function" ? store.put(record) : store.add(record);
|
|
611
|
+
if (typeof request === "object" && request !== null) {
|
|
612
|
+
request.onerror = fail(
|
|
613
|
+
"request failed",
|
|
614
|
+
request
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
),
|
|
619
|
+
this.timeoutMs,
|
|
620
|
+
timeoutMessage("IndexedDB", this.timeoutMs, "finish the transaction")
|
|
180
621
|
);
|
|
181
622
|
}
|
|
182
623
|
};
|
|
624
|
+
function describeDomError(error) {
|
|
625
|
+
if (error === null || error === void 0) {
|
|
626
|
+
return "no details provided";
|
|
627
|
+
}
|
|
628
|
+
const text = [error.name, error.message].filter((part) => typeof part === "string" && part !== "").join(": ");
|
|
629
|
+
return text === "" ? "no details provided" : text;
|
|
630
|
+
}
|
|
183
631
|
|
|
184
|
-
// src/
|
|
632
|
+
// src/sql-ident.ts
|
|
185
633
|
var DEFAULT_TABLE_NAME = "error_log";
|
|
186
634
|
var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
187
|
-
function
|
|
188
|
-
if (!IDENTIFIER_PATTERN.test(
|
|
635
|
+
function assertIdentifier(value, label, owner, maxLength) {
|
|
636
|
+
if (!IDENTIFIER_PATTERN.test(value)) {
|
|
189
637
|
throw new RangeError(
|
|
190
|
-
|
|
638
|
+
`${owner}: invalid ${label} "${value}". Use only letters, digits and underscores, and do not start with a digit.`
|
|
191
639
|
);
|
|
192
640
|
}
|
|
641
|
+
if (maxLength !== void 0 && value.length > maxLength) {
|
|
642
|
+
throw new RangeError(
|
|
643
|
+
`${owner}: ${label} "${value}" is ${value.length} characters long, but the limit is ${maxLength}.`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/adapters/sqlite-error-adapter.ts
|
|
649
|
+
function assertValidTableName(tableName) {
|
|
650
|
+
assertIdentifier(tableName, "table name", "SqliteErrorAdapter");
|
|
193
651
|
}
|
|
194
652
|
function buildCreateErrorLogTableSql(tableName = DEFAULT_TABLE_NAME) {
|
|
195
653
|
assertValidTableName(tableName);
|
|
@@ -199,23 +657,24 @@ function buildCreateErrorLogTableSql(tableName = DEFAULT_TABLE_NAME) {
|
|
|
199
657
|
];
|
|
200
658
|
}
|
|
201
659
|
var SqliteErrorAdapter = class {
|
|
202
|
-
/** @throws {RangeError} if `options.tableName` is not a plain identifier. */
|
|
660
|
+
/** @throws {RangeError} if `options.tableName` is not a plain identifier or `options.timeoutMs` is invalid. */
|
|
203
661
|
constructor(client, options = {}) {
|
|
204
662
|
this.client = client;
|
|
205
663
|
this.tableReady = null;
|
|
206
664
|
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
207
665
|
assertValidTableName(this.tableName);
|
|
208
666
|
this.createTable = options.createTable ?? true;
|
|
209
|
-
this.prefix = options.prefix ??
|
|
667
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
668
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "SqliteErrorAdapter");
|
|
210
669
|
}
|
|
211
670
|
async log(entry) {
|
|
212
671
|
try {
|
|
213
672
|
await this.ensureTable();
|
|
214
673
|
const serialized = serializeEntry(entry);
|
|
215
|
-
await this.
|
|
216
|
-
`INSERT INTO ${this.tableName} (id, code, message, cause, source, occurred_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
674
|
+
await this.run(
|
|
675
|
+
`INSERT INTO ${this.tableName} (id, code, message, cause, source, occurred_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO NOTHING`,
|
|
217
676
|
[
|
|
218
|
-
|
|
677
|
+
resolveEntryId(entry),
|
|
219
678
|
serialized.code,
|
|
220
679
|
serialized.message,
|
|
221
680
|
serialized.cause === void 0 ? null : JSON.stringify(serialized.cause),
|
|
@@ -225,11 +684,12 @@ var SqliteErrorAdapter = class {
|
|
|
225
684
|
);
|
|
226
685
|
return { ok: true };
|
|
227
686
|
} catch (error) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
687
|
+
return reportFailure({
|
|
688
|
+
adapterName: "SqliteErrorAdapter",
|
|
689
|
+
prefix: this.prefix,
|
|
690
|
+
action: `to write to table "${this.tableName}"`,
|
|
691
|
+
error
|
|
692
|
+
});
|
|
233
693
|
}
|
|
234
694
|
}
|
|
235
695
|
/** Creates the table once. A failed attempt is retried on the next log call. */
|
|
@@ -249,9 +709,17 @@ var SqliteErrorAdapter = class {
|
|
|
249
709
|
}
|
|
250
710
|
async runCreateStatements() {
|
|
251
711
|
for (const sql of buildCreateErrorLogTableSql(this.tableName)) {
|
|
252
|
-
await this.
|
|
712
|
+
await this.run(sql, []);
|
|
253
713
|
}
|
|
254
714
|
}
|
|
715
|
+
/** Runs one statement, giving up waiting after `timeoutMs`. */
|
|
716
|
+
run(sql, params) {
|
|
717
|
+
return withTimeout(
|
|
718
|
+
() => this.client.run(sql, params),
|
|
719
|
+
this.timeoutMs,
|
|
720
|
+
timeoutMessage("SQLite", this.timeoutMs)
|
|
721
|
+
);
|
|
722
|
+
}
|
|
255
723
|
};
|
|
256
724
|
|
|
257
725
|
// src/adapters/web-storage-error-adapter.ts
|
|
@@ -270,7 +738,8 @@ var WebStorageErrorAdapter = class {
|
|
|
270
738
|
this.storage = storage;
|
|
271
739
|
this.key = options.key ?? DEFAULT_STORAGE_KEY;
|
|
272
740
|
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
273
|
-
this.prefix = options.prefix ??
|
|
741
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
742
|
+
this.locks = options.locks === void 0 ? defaultLockManager() : options.locks;
|
|
274
743
|
if (!Number.isInteger(this.maxEntries) || this.maxEntries < 1) {
|
|
275
744
|
throw new RangeError(
|
|
276
745
|
`WebStorageErrorAdapter: maxEntries must be a positive integer, received ${String(options.maxEntries)}.`
|
|
@@ -280,23 +749,49 @@ var WebStorageErrorAdapter = class {
|
|
|
280
749
|
async log(entry) {
|
|
281
750
|
try {
|
|
282
751
|
const record = {
|
|
283
|
-
id:
|
|
752
|
+
id: resolveEntryId(entry),
|
|
284
753
|
...serializeEntry(entry)
|
|
285
754
|
};
|
|
286
|
-
this.
|
|
755
|
+
await this.appendLocked(record);
|
|
287
756
|
return { ok: true };
|
|
288
757
|
} catch (error) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
758
|
+
return reportFailure({
|
|
759
|
+
adapterName: "WebStorageErrorAdapter",
|
|
760
|
+
prefix: this.prefix,
|
|
761
|
+
action: `to write to storage key "${this.key}"`,
|
|
762
|
+
error
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Appends under a cross-tab lock when one is available. If the lock itself
|
|
768
|
+
* cannot be taken (for example a sandboxed frame), the entry is still written
|
|
769
|
+
* without it rather than lost.
|
|
770
|
+
*/
|
|
771
|
+
async appendLocked(record) {
|
|
772
|
+
if (this.locks === null) {
|
|
773
|
+
this.append(record);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
let appended = false;
|
|
777
|
+
try {
|
|
778
|
+
await this.locks.request(`error-log-adapter:${this.key}`, () => {
|
|
779
|
+
appended = true;
|
|
780
|
+
this.append(record);
|
|
781
|
+
});
|
|
782
|
+
} catch (error) {
|
|
783
|
+
if (appended) {
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
786
|
+
this.append(record);
|
|
294
787
|
}
|
|
295
788
|
}
|
|
296
|
-
/** Reads, appends, trims, and writes back. Runs without awaiting, so calls never interleave. */
|
|
789
|
+
/** Reads, appends, trims, and writes back. Runs without awaiting, so calls in one tab never interleave. */
|
|
297
790
|
append(record) {
|
|
298
791
|
const records = [
|
|
299
|
-
...this.readRecords()
|
|
792
|
+
...this.readRecords().filter(
|
|
793
|
+
(existing) => existing?.id !== record.id
|
|
794
|
+
),
|
|
300
795
|
record
|
|
301
796
|
].slice(-this.maxEntries);
|
|
302
797
|
for (; ; ) {
|
|
@@ -322,16 +817,51 @@ var WebStorageErrorAdapter = class {
|
|
|
322
817
|
if (Array.isArray(parsed)) {
|
|
323
818
|
return parsed;
|
|
324
819
|
}
|
|
325
|
-
|
|
326
|
-
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" did not hold an array, so it was replaced.`
|
|
327
|
-
);
|
|
820
|
+
this.setAside(stored, "did not hold an array");
|
|
328
821
|
} catch (error) {
|
|
329
|
-
|
|
330
|
-
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" held invalid JSON (${describeError(error)}), so it was replaced.`
|
|
331
|
-
);
|
|
822
|
+
this.setAside(stored, `held invalid JSON (${describeError(error)})`);
|
|
332
823
|
}
|
|
333
824
|
return [];
|
|
334
825
|
}
|
|
826
|
+
/**
|
|
827
|
+
* Copies an unusable value to `<key>.corrupt` before it is replaced, so data
|
|
828
|
+
* the host app stored under the same key is not silently destroyed. Only the
|
|
829
|
+
* first one is kept; a later unusable value would not overwrite it.
|
|
830
|
+
*/
|
|
831
|
+
setAside(stored, reason) {
|
|
832
|
+
const backupKey = `${this.key}${CORRUPT_KEY_SUFFIX}`;
|
|
833
|
+
let savedTo = "but it could not be copied aside";
|
|
834
|
+
try {
|
|
835
|
+
if (this.storage.getItem(backupKey) === null) {
|
|
836
|
+
this.storage.setItem(backupKey, stored);
|
|
837
|
+
savedTo = `and a copy was kept under "${backupKey}"`;
|
|
838
|
+
} else {
|
|
839
|
+
savedTo = `and "${backupKey}" already holds an earlier copy`;
|
|
840
|
+
}
|
|
841
|
+
} catch {
|
|
842
|
+
}
|
|
843
|
+
console.error(
|
|
844
|
+
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" ${reason}, so it was replaced, ${savedTo}.`
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
var CORRUPT_KEY_SUFFIX = ".corrupt";
|
|
849
|
+
function defaultLockManager() {
|
|
850
|
+
const locks = globalThis.navigator?.locks;
|
|
851
|
+
return typeof locks?.request === "function" ? locks : null;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/queue.ts
|
|
855
|
+
var SerialQueue = class {
|
|
856
|
+
constructor() {
|
|
857
|
+
this.tail = Promise.resolve();
|
|
858
|
+
}
|
|
859
|
+
/** Starts `task` once every earlier task has finished, and resolves with its result. */
|
|
860
|
+
run(task) {
|
|
861
|
+
const result = this.tail.then(task);
|
|
862
|
+
this.tail = result.catch(() => void 0);
|
|
863
|
+
return result;
|
|
864
|
+
}
|
|
335
865
|
};
|
|
336
866
|
|
|
337
867
|
// src/file-system.ts
|
|
@@ -339,6 +869,7 @@ async function loadNodeFileSystem() {
|
|
|
339
869
|
const moduleName = "node:fs/promises";
|
|
340
870
|
return await import(moduleName);
|
|
341
871
|
}
|
|
872
|
+
var ROOT_PATTERN = /^(?:[A-Za-z]:)?[\\/]+$/;
|
|
342
873
|
function parentDirectory(filePath) {
|
|
343
874
|
const lastSeparator = Math.max(
|
|
344
875
|
filePath.lastIndexOf("/"),
|
|
@@ -347,7 +878,11 @@ function parentDirectory(filePath) {
|
|
|
347
878
|
if (lastSeparator < 0) {
|
|
348
879
|
return null;
|
|
349
880
|
}
|
|
350
|
-
|
|
881
|
+
const directory = filePath.slice(0, lastSeparator + 1);
|
|
882
|
+
if (ROOT_PATTERN.test(directory)) {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
return filePath.slice(0, lastSeparator);
|
|
351
886
|
}
|
|
352
887
|
function isMissingFileError(error) {
|
|
353
888
|
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
@@ -393,24 +928,20 @@ var FlatFileErrorAdapter = class {
|
|
|
393
928
|
constructor(filePath, options = {}) {
|
|
394
929
|
this.filePath = filePath;
|
|
395
930
|
this.directoryReady = false;
|
|
396
|
-
this.queue =
|
|
931
|
+
this.queue = new SerialQueue();
|
|
397
932
|
this.fs = options.fs;
|
|
398
933
|
this.formatLine = options.formatLine ?? formatErrorLogLine;
|
|
399
934
|
this.maxBytes = options.maxBytes;
|
|
400
935
|
this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
401
936
|
this.createDirectory = options.createDirectory ?? true;
|
|
402
|
-
this.prefix = options.prefix ??
|
|
937
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
403
938
|
if (this.maxBytes !== void 0) {
|
|
404
939
|
assertPositiveInteger(this.maxBytes, "maxBytes");
|
|
405
940
|
}
|
|
406
941
|
assertPositiveInteger(this.maxFiles, "maxFiles");
|
|
407
942
|
}
|
|
408
943
|
log(entry) {
|
|
409
|
-
|
|
410
|
-
() => this.write(entry)
|
|
411
|
-
);
|
|
412
|
-
this.queue = result.catch(() => void 0);
|
|
413
|
-
return result;
|
|
944
|
+
return this.queue.run(() => this.write(entry));
|
|
414
945
|
}
|
|
415
946
|
async write(entry) {
|
|
416
947
|
try {
|
|
@@ -423,11 +954,12 @@ var FlatFileErrorAdapter = class {
|
|
|
423
954
|
await fs.appendFile(this.filePath, line);
|
|
424
955
|
return { ok: true };
|
|
425
956
|
} catch (error) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
957
|
+
return reportFailure({
|
|
958
|
+
adapterName: "FlatFileErrorAdapter",
|
|
959
|
+
prefix: this.prefix,
|
|
960
|
+
action: `to append to "${this.filePath}"`,
|
|
961
|
+
error
|
|
962
|
+
});
|
|
431
963
|
}
|
|
432
964
|
}
|
|
433
965
|
/** Uses the injected file system, or loads `node:fs/promises` on first use. */
|
|
@@ -502,13 +1034,13 @@ var JsonFileErrorAdapter = class {
|
|
|
502
1034
|
constructor(filePath, options = {}) {
|
|
503
1035
|
this.filePath = filePath;
|
|
504
1036
|
this.directoryReady = false;
|
|
505
|
-
this.queue =
|
|
1037
|
+
this.queue = new SerialQueue();
|
|
506
1038
|
this.mode = options.mode ?? "ndjson";
|
|
507
1039
|
this.maxEntries = options.maxEntries;
|
|
508
1040
|
this.indent = options.indent ?? DEFAULT_INDENT;
|
|
509
1041
|
this.fs = options.fs;
|
|
510
1042
|
this.createDirectory = options.createDirectory ?? true;
|
|
511
|
-
this.prefix = options.prefix ??
|
|
1043
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
512
1044
|
if (this.maxEntries !== void 0) {
|
|
513
1045
|
if (this.mode !== "array") {
|
|
514
1046
|
throw new RangeError(
|
|
@@ -528,16 +1060,12 @@ var JsonFileErrorAdapter = class {
|
|
|
528
1060
|
}
|
|
529
1061
|
}
|
|
530
1062
|
log(entry) {
|
|
531
|
-
|
|
532
|
-
() => this.write(entry)
|
|
533
|
-
);
|
|
534
|
-
this.queue = result.catch(() => void 0);
|
|
535
|
-
return result;
|
|
1063
|
+
return this.queue.run(() => this.write(entry));
|
|
536
1064
|
}
|
|
537
1065
|
async write(entry) {
|
|
538
1066
|
try {
|
|
539
1067
|
const record = {
|
|
540
|
-
id:
|
|
1068
|
+
id: resolveEntryId(entry),
|
|
541
1069
|
...serializeEntry(entry)
|
|
542
1070
|
};
|
|
543
1071
|
const fs = await this.getFileSystem();
|
|
@@ -550,11 +1078,12 @@ var JsonFileErrorAdapter = class {
|
|
|
550
1078
|
}
|
|
551
1079
|
return { ok: true };
|
|
552
1080
|
} catch (error) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1081
|
+
return reportFailure({
|
|
1082
|
+
adapterName: "JsonFileErrorAdapter",
|
|
1083
|
+
prefix: this.prefix,
|
|
1084
|
+
action: `to write "${this.filePath}" in ${this.mode} mode`,
|
|
1085
|
+
error
|
|
1086
|
+
});
|
|
558
1087
|
}
|
|
559
1088
|
}
|
|
560
1089
|
async getFileSystem() {
|
|
@@ -580,7 +1109,9 @@ var JsonFileErrorAdapter = class {
|
|
|
580
1109
|
*/
|
|
581
1110
|
async rewriteArray(fs, record) {
|
|
582
1111
|
const records = [
|
|
583
|
-
...await this.readRecords(fs)
|
|
1112
|
+
...(await this.readRecords(fs)).filter(
|
|
1113
|
+
(existing) => existing?.id !== record.id
|
|
1114
|
+
),
|
|
584
1115
|
record
|
|
585
1116
|
];
|
|
586
1117
|
const kept = this.maxEntries === void 0 ? records : records.slice(-this.maxEntries);
|
|
@@ -630,25 +1161,14 @@ var JsonFileErrorAdapter = class {
|
|
|
630
1161
|
};
|
|
631
1162
|
|
|
632
1163
|
// src/adapters/sql-error-adapter.ts
|
|
633
|
-
var DEFAULT_TABLE_NAME2 = "error_log";
|
|
634
1164
|
var MAX_TABLE_NAME_LENGTH = 47;
|
|
635
|
-
var IDENTIFIER_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
636
1165
|
function assertValidTableName2(tableName, adapterName) {
|
|
637
|
-
|
|
638
|
-
throw new RangeError(
|
|
639
|
-
`${adapterName}: invalid table name "${tableName}". Use only letters, digits and underscores, and do not start with a digit.`
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
if (tableName.length > MAX_TABLE_NAME_LENGTH) {
|
|
643
|
-
throw new RangeError(
|
|
644
|
-
`${adapterName}: table name "${tableName}" is ${tableName.length} characters long, but the limit is ${MAX_TABLE_NAME_LENGTH}.`
|
|
645
|
-
);
|
|
646
|
-
}
|
|
1166
|
+
assertIdentifier(tableName, "table name", adapterName, MAX_TABLE_NAME_LENGTH);
|
|
647
1167
|
}
|
|
648
1168
|
function quoteIdentifier(dialect, identifier) {
|
|
649
1169
|
return dialect === "postgres" ? `"${identifier}"` : `\`${identifier}\``;
|
|
650
1170
|
}
|
|
651
|
-
function buildErrorLogTableDdl(dialect, tableName =
|
|
1171
|
+
function buildErrorLogTableDdl(dialect, tableName = DEFAULT_TABLE_NAME) {
|
|
652
1172
|
assertValidTableName2(tableName, "buildErrorLogTableDdl");
|
|
653
1173
|
const table = quoteIdentifier(dialect, tableName);
|
|
654
1174
|
const index = quoteIdentifier(
|
|
@@ -662,7 +1182,13 @@ function buildErrorLogTableDdl(dialect, tableName = DEFAULT_TABLE_NAME2) {
|
|
|
662
1182
|
];
|
|
663
1183
|
}
|
|
664
1184
|
return [
|
|
665
|
-
`CREATE TABLE IF NOT EXISTS ${table} (id CHAR(36) NOT NULL PRIMARY KEY, code
|
|
1185
|
+
`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))`
|
|
1186
|
+
];
|
|
1187
|
+
}
|
|
1188
|
+
function buildMySqlWidenColumnsSql(tableName = DEFAULT_TABLE_NAME) {
|
|
1189
|
+
assertValidTableName2(tableName, "buildMySqlWidenColumnsSql");
|
|
1190
|
+
return [
|
|
1191
|
+
`ALTER TABLE ${quoteIdentifier("mysql", tableName)} MODIFY code TEXT NOT NULL, MODIFY source TEXT NULL`
|
|
666
1192
|
];
|
|
667
1193
|
}
|
|
668
1194
|
function toMysqlDateTime(date) {
|
|
@@ -674,10 +1200,11 @@ var SqlErrorAdapter = class {
|
|
|
674
1200
|
this.dialect = dialect;
|
|
675
1201
|
this.adapterName = adapterName;
|
|
676
1202
|
this.tableReady = null;
|
|
677
|
-
this.tableName = options.tableName ??
|
|
1203
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
678
1204
|
assertValidTableName2(this.tableName, adapterName);
|
|
679
1205
|
this.createTable = options.createTable ?? true;
|
|
680
|
-
this.prefix = options.prefix ??
|
|
1206
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1207
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, adapterName);
|
|
681
1208
|
}
|
|
682
1209
|
async log(entry) {
|
|
683
1210
|
try {
|
|
@@ -689,7 +1216,7 @@ var SqlErrorAdapter = class {
|
|
|
689
1216
|
);
|
|
690
1217
|
}
|
|
691
1218
|
const params = [
|
|
692
|
-
|
|
1219
|
+
resolveEntryId(entry),
|
|
693
1220
|
serialized.code,
|
|
694
1221
|
serialized.message,
|
|
695
1222
|
serialized.cause === void 0 ? null : JSON.stringify(serialized.cause),
|
|
@@ -697,19 +1224,21 @@ var SqlErrorAdapter = class {
|
|
|
697
1224
|
this.dialect === "postgres" ? occurredAt.toISOString() : toMysqlDateTime(occurredAt)
|
|
698
1225
|
];
|
|
699
1226
|
await this.ensureTable();
|
|
700
|
-
await this.
|
|
1227
|
+
await this.query(this.insertSql(), params);
|
|
701
1228
|
return { ok: true };
|
|
702
1229
|
} catch (error) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1230
|
+
return reportFailure({
|
|
1231
|
+
adapterName: this.adapterName,
|
|
1232
|
+
prefix: this.prefix,
|
|
1233
|
+
action: `to write to table "${this.tableName}"`,
|
|
1234
|
+
error
|
|
1235
|
+
});
|
|
708
1236
|
}
|
|
709
1237
|
}
|
|
710
1238
|
insertSql() {
|
|
711
1239
|
const placeholders = this.dialect === "postgres" ? "$1, $2, $3, $4, $5, $6" : "?, ?, ?, ?, ?, ?";
|
|
712
|
-
|
|
1240
|
+
const onRepeat = this.dialect === "postgres" ? "ON CONFLICT (id) DO NOTHING" : "ON DUPLICATE KEY UPDATE id = id";
|
|
1241
|
+
return `INSERT INTO ${quoteIdentifier(this.dialect, this.tableName)} (id, code, message, cause, source, occurred_at) VALUES (${placeholders}) ${onRepeat}`;
|
|
713
1242
|
}
|
|
714
1243
|
/** Creates the table once. A failed attempt is retried on the next log call. */
|
|
715
1244
|
ensureTable() {
|
|
@@ -728,9 +1257,20 @@ var SqlErrorAdapter = class {
|
|
|
728
1257
|
}
|
|
729
1258
|
async runCreateStatements() {
|
|
730
1259
|
for (const sql of buildErrorLogTableDdl(this.dialect, this.tableName)) {
|
|
731
|
-
await this.
|
|
1260
|
+
await this.query(sql, []);
|
|
732
1261
|
}
|
|
733
1262
|
}
|
|
1263
|
+
/** Runs one statement, giving up waiting after `timeoutMs`. */
|
|
1264
|
+
query(sql, params) {
|
|
1265
|
+
return withTimeout(
|
|
1266
|
+
() => this.client.query(sql, params),
|
|
1267
|
+
this.timeoutMs,
|
|
1268
|
+
timeoutMessage(
|
|
1269
|
+
this.dialect === "postgres" ? "Postgres" : "MySQL",
|
|
1270
|
+
this.timeoutMs
|
|
1271
|
+
)
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
734
1274
|
};
|
|
735
1275
|
var PostgresErrorAdapter = class extends SqlErrorAdapter {
|
|
736
1276
|
/** @throws {RangeError} if `options.tableName` is not a plain identifier of 47 characters or fewer. */
|
|
@@ -755,7 +1295,8 @@ var RedisErrorAdapter = class {
|
|
|
755
1295
|
this.key = options.key ?? DEFAULT_REDIS_KEY;
|
|
756
1296
|
this.maxEntries = options.maxEntries;
|
|
757
1297
|
this.side = options.side ?? "right";
|
|
758
|
-
this.prefix = options.prefix ??
|
|
1298
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1299
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "RedisErrorAdapter");
|
|
759
1300
|
if (this.mode !== "list" && this.mode !== "stream") {
|
|
760
1301
|
throw new RangeError(
|
|
761
1302
|
`RedisErrorAdapter: mode must be "list" or "stream", received "${String(options.mode)}".`
|
|
@@ -777,11 +1318,11 @@ var RedisErrorAdapter = class {
|
|
|
777
1318
|
}
|
|
778
1319
|
async log(entry) {
|
|
779
1320
|
try {
|
|
780
|
-
const record = { id:
|
|
1321
|
+
const record = { id: resolveEntryId(entry), ...serializeEntry(entry) };
|
|
781
1322
|
if (this.mode === "stream") {
|
|
782
|
-
await this.
|
|
1323
|
+
await this.send(this.buildStreamCommand(record));
|
|
783
1324
|
} else {
|
|
784
|
-
await this.
|
|
1325
|
+
await this.send([
|
|
785
1326
|
this.side === "left" ? "LPUSH" : "RPUSH",
|
|
786
1327
|
this.key,
|
|
787
1328
|
JSON.stringify(record)
|
|
@@ -790,11 +1331,12 @@ var RedisErrorAdapter = class {
|
|
|
790
1331
|
}
|
|
791
1332
|
return { ok: true };
|
|
792
1333
|
} catch (error) {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1334
|
+
return reportFailure({
|
|
1335
|
+
adapterName: "RedisErrorAdapter",
|
|
1336
|
+
prefix: this.prefix,
|
|
1337
|
+
action: `to write to ${this.mode} "${this.key}"`,
|
|
1338
|
+
error
|
|
1339
|
+
});
|
|
798
1340
|
}
|
|
799
1341
|
}
|
|
800
1342
|
buildStreamCommand(record) {
|
|
@@ -826,6 +1368,14 @@ var RedisErrorAdapter = class {
|
|
|
826
1368
|
* stored by now, so a trim failure is reported but does not fail the call;
|
|
827
1369
|
* the next successful write trims the list again.
|
|
828
1370
|
*/
|
|
1371
|
+
/** Sends one command, giving up waiting after `timeoutMs`. */
|
|
1372
|
+
send(args) {
|
|
1373
|
+
return withTimeout(
|
|
1374
|
+
() => this.client.sendCommand(args),
|
|
1375
|
+
this.timeoutMs,
|
|
1376
|
+
timeoutMessage("Redis", this.timeoutMs)
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
829
1379
|
async trimList() {
|
|
830
1380
|
if (this.maxEntries === void 0) {
|
|
831
1381
|
return;
|
|
@@ -833,7 +1383,7 @@ var RedisErrorAdapter = class {
|
|
|
833
1383
|
const keep = this.maxEntries;
|
|
834
1384
|
const [start, stop] = this.side === "right" ? [String(-keep), "-1"] : ["0", String(keep - 1)];
|
|
835
1385
|
try {
|
|
836
|
-
await this.
|
|
1386
|
+
await this.send(["LTRIM", this.key, start, stop]);
|
|
837
1387
|
} catch (error) {
|
|
838
1388
|
console.error(
|
|
839
1389
|
`${this.prefix} RedisErrorAdapter: stored the entry but could not trim list "${this.key}" to ${keep} entries: ${describeError(error)}`
|
|
@@ -860,11 +1410,12 @@ var S3ErrorAdapter = class {
|
|
|
860
1410
|
this.bucket = bucket;
|
|
861
1411
|
this.buffer = [];
|
|
862
1412
|
this.timer = null;
|
|
863
|
-
this.flushQueue =
|
|
1413
|
+
this.flushQueue = new SerialQueue();
|
|
864
1414
|
this.keyPrefix = normalizePrefix(options.keyPrefix ?? DEFAULT_S3_PREFIX);
|
|
865
1415
|
this.batch = options.batch;
|
|
866
1416
|
this.contentType = options.contentType ?? (this.batch === void 0 ? "application/json" : "application/x-ndjson");
|
|
867
|
-
this.logPrefix = options.prefix ??
|
|
1417
|
+
this.logPrefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1418
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "S3ErrorAdapter");
|
|
868
1419
|
if (bucket === "") {
|
|
869
1420
|
throw new RangeError("S3ErrorAdapter: bucket must not be empty.");
|
|
870
1421
|
}
|
|
@@ -884,7 +1435,7 @@ var S3ErrorAdapter = class {
|
|
|
884
1435
|
async log(entry) {
|
|
885
1436
|
try {
|
|
886
1437
|
const record = {
|
|
887
|
-
id:
|
|
1438
|
+
id: resolveEntryId(entry),
|
|
888
1439
|
...serializeEntry(entry)
|
|
889
1440
|
};
|
|
890
1441
|
const occurredAt = new Date(record.timestamp);
|
|
@@ -909,11 +1460,9 @@ var S3ErrorAdapter = class {
|
|
|
909
1460
|
* Entries from a failed upload stay buffered and are retried by the next flush.
|
|
910
1461
|
*/
|
|
911
1462
|
flush() {
|
|
912
|
-
|
|
1463
|
+
return this.flushQueue.run(
|
|
913
1464
|
() => this.uploadBuffered()
|
|
914
1465
|
);
|
|
915
|
-
this.flushQueue = result.catch(() => void 0);
|
|
916
|
-
return result;
|
|
917
1466
|
}
|
|
918
1467
|
async addToBatch(record, batch) {
|
|
919
1468
|
this.buffer.push(record);
|
|
@@ -952,7 +1501,7 @@ var S3ErrorAdapter = class {
|
|
|
952
1501
|
return { ok: true };
|
|
953
1502
|
}
|
|
954
1503
|
try {
|
|
955
|
-
const key = `${this.keyPrefix}${datePath(/* @__PURE__ */ new Date())}-${
|
|
1504
|
+
const key = `${this.keyPrefix}${datePath(/* @__PURE__ */ new Date())}-${generateId()}.ndjson`;
|
|
956
1505
|
const body = pending.map((record) => JSON.stringify(record)).join("\n");
|
|
957
1506
|
await this.upload(key, `${body}
|
|
958
1507
|
`);
|
|
@@ -969,64 +1518,41 @@ var S3ErrorAdapter = class {
|
|
|
969
1518
|
}
|
|
970
1519
|
}
|
|
971
1520
|
upload(key, body) {
|
|
972
|
-
return
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1521
|
+
return withTimeout(
|
|
1522
|
+
() => this.client.putObject({
|
|
1523
|
+
Bucket: this.bucket,
|
|
1524
|
+
Key: key,
|
|
1525
|
+
Body: body,
|
|
1526
|
+
ContentType: this.contentType
|
|
1527
|
+
}),
|
|
1528
|
+
this.timeoutMs,
|
|
1529
|
+
timeoutMessage("S3", this.timeoutMs)
|
|
1530
|
+
);
|
|
978
1531
|
}
|
|
979
1532
|
fail(error, bufferedCount) {
|
|
980
|
-
console.error(`${this.logPrefix} S3ErrorAdapter:`, error);
|
|
981
1533
|
const retry = bufferedCount === void 0 ? "" : ` (${bufferedCount} buffered ${bufferedCount === 1 ? "entry was" : "entries were"} kept for the next flush)`;
|
|
982
|
-
return {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1534
|
+
return reportFailure({
|
|
1535
|
+
adapterName: "S3ErrorAdapter",
|
|
1536
|
+
prefix: this.logPrefix,
|
|
1537
|
+
action: `to upload to bucket "${this.bucket}"`,
|
|
1538
|
+
error,
|
|
1539
|
+
reason: `${describeError(error)}${retry}`
|
|
1540
|
+
});
|
|
986
1541
|
}
|
|
987
1542
|
};
|
|
988
1543
|
|
|
989
1544
|
// src/adapters/firebase-error-adapter.ts
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
`${adapter}: timeoutMs must be a positive number or Infinity, received ${String(options.timeoutMs)}.`
|
|
996
|
-
);
|
|
997
|
-
}
|
|
998
|
-
return timeoutMs;
|
|
999
|
-
}
|
|
1000
|
-
function waitForWrite(write, timeoutMs) {
|
|
1001
|
-
if (timeoutMs === Infinity) {
|
|
1002
|
-
return Promise.resolve(write);
|
|
1003
|
-
}
|
|
1004
|
-
return new Promise(
|
|
1005
|
-
(resolve, reject) => {
|
|
1006
|
-
const timer = setTimeout(() => {
|
|
1007
|
-
reject(
|
|
1008
|
-
new Error(
|
|
1009
|
-
`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)`
|
|
1010
|
-
)
|
|
1011
|
-
);
|
|
1012
|
-
}, timeoutMs);
|
|
1013
|
-
write.then(
|
|
1014
|
-
(value) => {
|
|
1015
|
-
clearTimeout(timer);
|
|
1016
|
-
resolve(value);
|
|
1017
|
-
},
|
|
1018
|
-
(reason) => {
|
|
1019
|
-
clearTimeout(timer);
|
|
1020
|
-
reject(reason);
|
|
1021
|
-
}
|
|
1022
|
-
);
|
|
1023
|
-
}
|
|
1545
|
+
function waitForWrite(start, timeoutMs) {
|
|
1546
|
+
return withTimeout(
|
|
1547
|
+
start,
|
|
1548
|
+
timeoutMs,
|
|
1549
|
+
`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)`
|
|
1024
1550
|
);
|
|
1025
1551
|
}
|
|
1026
1552
|
function buildRecord(entry) {
|
|
1027
1553
|
const serialized = serializeEntry(entry);
|
|
1028
1554
|
const record = {
|
|
1029
|
-
id:
|
|
1555
|
+
id: resolveEntryId(entry),
|
|
1030
1556
|
code: serialized.code,
|
|
1031
1557
|
message: serialized.message,
|
|
1032
1558
|
timestamp: serialized.timestamp
|
|
@@ -1043,23 +1569,27 @@ var FirestoreErrorAdapter = class {
|
|
|
1043
1569
|
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
|
|
1044
1570
|
constructor(collection, options = {}) {
|
|
1045
1571
|
this.collection = collection;
|
|
1046
|
-
this.prefix = options.prefix ??
|
|
1047
|
-
this.timeoutMs =
|
|
1572
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1573
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1574
|
+
options.timeoutMs,
|
|
1575
|
+
"FirestoreErrorAdapter"
|
|
1576
|
+
);
|
|
1048
1577
|
}
|
|
1049
1578
|
async log(entry) {
|
|
1050
1579
|
try {
|
|
1051
1580
|
const record = buildRecord(entry);
|
|
1052
1581
|
await waitForWrite(
|
|
1053
|
-
this.collection.doc(record.id).set(record),
|
|
1582
|
+
() => this.collection.doc(record.id).set(record),
|
|
1054
1583
|
this.timeoutMs
|
|
1055
1584
|
);
|
|
1056
1585
|
return { ok: true };
|
|
1057
1586
|
} catch (error) {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1587
|
+
return reportFailure({
|
|
1588
|
+
adapterName: "FirestoreErrorAdapter",
|
|
1589
|
+
prefix: this.prefix,
|
|
1590
|
+
action: `to write the document`,
|
|
1591
|
+
error
|
|
1592
|
+
});
|
|
1063
1593
|
}
|
|
1064
1594
|
}
|
|
1065
1595
|
};
|
|
@@ -1067,34 +1597,51 @@ var RealtimeDatabaseErrorAdapter = class {
|
|
|
1067
1597
|
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
|
|
1068
1598
|
constructor(reference, options = {}) {
|
|
1069
1599
|
this.reference = reference;
|
|
1070
|
-
this.prefix = options.prefix ??
|
|
1071
|
-
this.timeoutMs =
|
|
1600
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1601
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1602
|
+
options.timeoutMs,
|
|
1603
|
+
"RealtimeDatabaseErrorAdapter"
|
|
1604
|
+
);
|
|
1072
1605
|
}
|
|
1073
1606
|
async log(entry) {
|
|
1074
1607
|
try {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1608
|
+
const record = buildRecord(entry);
|
|
1609
|
+
let write;
|
|
1610
|
+
if (hasCallerId(entry)) {
|
|
1611
|
+
if (this.reference.child === void 0) {
|
|
1612
|
+
throw new TypeError(
|
|
1613
|
+
"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"
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
write = this.reference.child(record.id).set(record);
|
|
1617
|
+
} else {
|
|
1618
|
+
write = this.reference.push(record);
|
|
1619
|
+
}
|
|
1620
|
+
await waitForWrite(() => write, this.timeoutMs);
|
|
1079
1621
|
return { ok: true };
|
|
1080
1622
|
} catch (error) {
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1623
|
+
return reportFailure({
|
|
1624
|
+
adapterName: "RealtimeDatabaseErrorAdapter",
|
|
1625
|
+
prefix: this.prefix,
|
|
1626
|
+
action: `to write the entry`,
|
|
1627
|
+
error
|
|
1628
|
+
});
|
|
1086
1629
|
}
|
|
1087
1630
|
}
|
|
1088
1631
|
};
|
|
1089
1632
|
|
|
1090
1633
|
// src/adapters/supabase-error-adapter.ts
|
|
1091
|
-
var
|
|
1092
|
-
|
|
1634
|
+
var POLICY_MAX_CODE_LENGTH = 255;
|
|
1635
|
+
var POLICY_MAX_MESSAGE_LENGTH = 4096;
|
|
1636
|
+
var POLICY_MAX_CAUSE_BYTES = 8192;
|
|
1637
|
+
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})`;
|
|
1638
|
+
function buildSupabaseSetupSql(tableName = DEFAULT_TABLE_NAME, options = {}) {
|
|
1093
1639
|
const insertRoles = options.insertRoles ?? [];
|
|
1094
1640
|
for (const role of insertRoles) {
|
|
1095
|
-
|
|
1641
|
+
assertIdentifier(role, "role name", "buildSupabaseSetupSql");
|
|
1642
|
+
if ((role.toLowerCase() === "anon" || role.toLowerCase() === "public") && options.allowPublicInsert !== true) {
|
|
1096
1643
|
throw new RangeError(
|
|
1097
|
-
`buildSupabaseSetupSql:
|
|
1644
|
+
`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.`
|
|
1098
1645
|
);
|
|
1099
1646
|
}
|
|
1100
1647
|
}
|
|
@@ -1106,7 +1653,7 @@ function buildSupabaseSetupSql(tableName = DEFAULT_TABLE_NAME2, options = {}) {
|
|
|
1106
1653
|
const policy = `${tableName}_insert_only`;
|
|
1107
1654
|
statements.push(
|
|
1108
1655
|
`DROP POLICY IF EXISTS "${policy}" ON "${tableName}"`,
|
|
1109
|
-
`CREATE POLICY "${policy}" ON "${tableName}" FOR INSERT TO ${insertRoles.map((role) => `"${role}"`).join(", ")} WITH CHECK (
|
|
1656
|
+
`CREATE POLICY "${policy}" ON "${tableName}" FOR INSERT TO ${insertRoles.map((role) => `"${role}"`).join(", ")} WITH CHECK (${INSERT_POLICY_CHECK})`
|
|
1110
1657
|
);
|
|
1111
1658
|
}
|
|
1112
1659
|
return statements;
|
|
@@ -1136,49 +1683,97 @@ function describeSupabaseError(result) {
|
|
|
1136
1683
|
return parts.join("; ");
|
|
1137
1684
|
}
|
|
1138
1685
|
var SupabaseErrorAdapter = class {
|
|
1139
|
-
/** @throws {RangeError} if `options.tableName` is
|
|
1686
|
+
/** @throws {RangeError} if `options.tableName` is not a plain identifier or `options.timeoutMs` is invalid. */
|
|
1140
1687
|
constructor(client, options = {}) {
|
|
1141
1688
|
this.client = client;
|
|
1142
|
-
this.tableName = options.tableName ??
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1689
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
1690
|
+
assertIdentifier(this.tableName, "table name", "SupabaseErrorAdapter");
|
|
1691
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1692
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1693
|
+
options.timeoutMs,
|
|
1694
|
+
"SupabaseErrorAdapter"
|
|
1695
|
+
);
|
|
1149
1696
|
}
|
|
1150
1697
|
async log(entry) {
|
|
1151
1698
|
try {
|
|
1152
1699
|
const serialized = serializeEntry(entry);
|
|
1153
1700
|
const row = {
|
|
1154
|
-
id:
|
|
1701
|
+
id: resolveEntryId(entry),
|
|
1155
1702
|
code: serialized.code,
|
|
1156
1703
|
message: serialized.message,
|
|
1157
1704
|
cause: serialized.cause === void 0 ? null : serialized.cause,
|
|
1158
1705
|
source: serialized.source ?? null,
|
|
1159
1706
|
occurred_at: serialized.timestamp
|
|
1160
1707
|
};
|
|
1161
|
-
const result = await
|
|
1708
|
+
const result = await withTimeout(
|
|
1709
|
+
() => this.client.from(this.tableName).insert(row),
|
|
1710
|
+
this.timeoutMs,
|
|
1711
|
+
timeoutMessage("Supabase", this.timeoutMs)
|
|
1712
|
+
);
|
|
1162
1713
|
if (result.error) {
|
|
1163
|
-
|
|
1164
|
-
|
|
1714
|
+
if (hasCallerId(entry) && result.error.code === "23505") {
|
|
1715
|
+
return { ok: true };
|
|
1716
|
+
}
|
|
1717
|
+
return this.fail(result, describeSupabaseError(result));
|
|
1165
1718
|
}
|
|
1166
1719
|
return { ok: true };
|
|
1167
1720
|
} catch (error) {
|
|
1168
|
-
|
|
1169
|
-
return this.fail(describeError(error));
|
|
1721
|
+
return this.fail(error, describeError(error));
|
|
1170
1722
|
}
|
|
1171
1723
|
}
|
|
1172
|
-
fail(reason) {
|
|
1173
|
-
return {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1724
|
+
fail(logged, reason) {
|
|
1725
|
+
return reportFailure({
|
|
1726
|
+
adapterName: "SupabaseErrorAdapter",
|
|
1727
|
+
prefix: this.prefix,
|
|
1728
|
+
action: `to insert into table "${this.tableName}"`,
|
|
1729
|
+
error: logged,
|
|
1730
|
+
reason
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1735
|
+
// src/adapters/event-emitter-error-adapter.ts
|
|
1736
|
+
var DEFAULT_EVENT_NAME = "error-log";
|
|
1737
|
+
var EventEmitterErrorAdapter = class {
|
|
1738
|
+
/** @throws {RangeError} if `options.eventName` is empty. */
|
|
1739
|
+
constructor(emitter, options = {}) {
|
|
1740
|
+
this.emitter = emitter;
|
|
1741
|
+
this.eventName = options.eventName ?? DEFAULT_EVENT_NAME;
|
|
1742
|
+
if (this.eventName === "") {
|
|
1743
|
+
throw new RangeError(
|
|
1744
|
+
"EventEmitterErrorAdapter: eventName must not be empty."
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
this.requireListener = options.requireListener ?? false;
|
|
1748
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1749
|
+
}
|
|
1750
|
+
async log(entry) {
|
|
1751
|
+
try {
|
|
1752
|
+
const record = {
|
|
1753
|
+
id: resolveEntryId(entry),
|
|
1754
|
+
...serializeEntry(entry)
|
|
1755
|
+
};
|
|
1756
|
+
const delivered = this.emitter.emit(this.eventName, record);
|
|
1757
|
+
if (this.requireListener && delivered === false) {
|
|
1758
|
+
throw new Error(
|
|
1759
|
+
`no listener is attached to "${this.eventName}", so nothing received the entry`
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
return { ok: true };
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
return reportFailure({
|
|
1765
|
+
adapterName: "EventEmitterErrorAdapter",
|
|
1766
|
+
prefix: this.prefix,
|
|
1767
|
+
action: `to emit "${this.eventName}"`,
|
|
1768
|
+
error
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1177
1771
|
}
|
|
1178
1772
|
};
|
|
1179
1773
|
export {
|
|
1180
1774
|
CompositeErrorAdapter,
|
|
1181
1775
|
ConsoleErrorAdapter,
|
|
1776
|
+
EventEmitterErrorAdapter,
|
|
1182
1777
|
FirestoreErrorAdapter,
|
|
1183
1778
|
FlatFileErrorAdapter,
|
|
1184
1779
|
IndexedDbErrorAdapter,
|
|
@@ -1195,6 +1790,7 @@ export {
|
|
|
1195
1790
|
WebhookErrorAdapter,
|
|
1196
1791
|
buildCreateErrorLogTableSql,
|
|
1197
1792
|
buildErrorLogTableDdl,
|
|
1793
|
+
buildMySqlWidenColumnsSql,
|
|
1198
1794
|
buildSupabaseSetupSql,
|
|
1199
1795
|
createErrorLogStore,
|
|
1200
1796
|
formatErrorLogLine,
|