@richardmcquiston01/error-log-adapter 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -13
- package/dist/index.cjs +650 -234
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +224 -22
- package/dist/index.d.ts +224 -22
- package/dist/index.mjs +649 -234
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/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,14 +140,135 @@ 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 LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
233
|
+
"localhost",
|
|
234
|
+
"127.0.0.1",
|
|
235
|
+
"[::1]"
|
|
236
|
+
]);
|
|
237
|
+
function parseWebhookUrl(url, allowInsecure) {
|
|
238
|
+
let parsed;
|
|
239
|
+
try {
|
|
240
|
+
parsed = new URL(url);
|
|
241
|
+
} catch {
|
|
242
|
+
throw new RangeError(
|
|
243
|
+
"WebhookErrorAdapter: the webhook URL is not a valid absolute URL (for example https://example.com/errors)."
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (parsed.protocol === "https:") {
|
|
247
|
+
return parsed;
|
|
248
|
+
}
|
|
249
|
+
if (parsed.protocol === "http:" && (allowInsecure || LOOPBACK_HOSTS.has(parsed.hostname))) {
|
|
250
|
+
return parsed;
|
|
251
|
+
}
|
|
252
|
+
throw new RangeError(
|
|
253
|
+
`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.`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
45
256
|
var WebhookErrorAdapter = class {
|
|
257
|
+
/** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or `timeoutMs` is invalid. */
|
|
46
258
|
constructor(url, options = {}) {
|
|
47
259
|
this.url = url;
|
|
48
260
|
this.options = options;
|
|
49
|
-
this.prefix = options.prefix ??
|
|
261
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
262
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "WebhookErrorAdapter");
|
|
263
|
+
this.target = parseWebhookUrl(url, options.allowInsecure ?? false).origin;
|
|
50
264
|
}
|
|
51
265
|
async log(entry) {
|
|
266
|
+
const controller = new AbortController();
|
|
267
|
+
let timedOut = false;
|
|
268
|
+
const timer = this.timeoutMs === Infinity ? void 0 : setTimeout(() => {
|
|
269
|
+
timedOut = true;
|
|
270
|
+
controller.abort();
|
|
271
|
+
}, this.timeoutMs);
|
|
52
272
|
try {
|
|
53
273
|
const response = await fetch(this.url, {
|
|
54
274
|
method: "POST",
|
|
@@ -56,58 +276,112 @@ var WebhookErrorAdapter = class {
|
|
|
56
276
|
"Content-Type": "application/json",
|
|
57
277
|
...this.options.headers
|
|
58
278
|
},
|
|
59
|
-
|
|
279
|
+
// A caller-supplied id goes along so the receiver can drop a repeat.
|
|
280
|
+
body: JSON.stringify(
|
|
281
|
+
hasCallerId(entry) ? { id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry)
|
|
282
|
+
),
|
|
283
|
+
// A redirect would turn the POST into a GET without a body (302, 303)
|
|
284
|
+
// or re-send the entry and headers to another host (307, 308).
|
|
285
|
+
redirect: "manual",
|
|
286
|
+
signal: controller.signal
|
|
60
287
|
});
|
|
288
|
+
if (response.type === "opaqueredirect") {
|
|
289
|
+
return this.fail(this.redirectMessage("a redirect"));
|
|
290
|
+
}
|
|
61
291
|
if (!response.ok) {
|
|
292
|
+
if (response.status >= 300 && response.status < 400) {
|
|
293
|
+
return this.fail(this.redirectMessage(`HTTP ${response.status}`));
|
|
294
|
+
}
|
|
62
295
|
return this.fail(
|
|
63
|
-
`webhook ${this.
|
|
296
|
+
`webhook ${this.target} responded with HTTP ${response.status}`
|
|
64
297
|
);
|
|
65
298
|
}
|
|
66
299
|
return { ok: true };
|
|
67
300
|
} catch (error) {
|
|
68
301
|
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
302
|
+
if (timedOut) {
|
|
303
|
+
return this.fail(
|
|
304
|
+
timeoutMessage(`webhook ${this.target}`, this.timeoutMs)
|
|
305
|
+
);
|
|
306
|
+
}
|
|
69
307
|
return this.fail(
|
|
70
|
-
`could not reach webhook ${this.
|
|
308
|
+
`could not reach webhook ${this.target}: ${this.redact(describeError(error))}`
|
|
71
309
|
);
|
|
310
|
+
} finally {
|
|
311
|
+
clearTimeout(timer);
|
|
72
312
|
}
|
|
73
313
|
}
|
|
314
|
+
redirectMessage(what) {
|
|
315
|
+
return `webhook ${this.target} responded with ${what}, which is not followed because the entry would not arrive intact; use the final URL instead`;
|
|
316
|
+
}
|
|
317
|
+
/** Replaces the full URL in a message with its origin, in case the runtime put it there. */
|
|
318
|
+
redact(message) {
|
|
319
|
+
return message.split(this.url).join(this.target);
|
|
320
|
+
}
|
|
74
321
|
fail(reason) {
|
|
75
322
|
return { ok: false, error: `WebhookErrorAdapter failed: ${reason}` };
|
|
76
323
|
}
|
|
77
324
|
};
|
|
78
325
|
|
|
79
326
|
// src/adapters/mongodb-error-adapter.ts
|
|
327
|
+
function isDuplicateKeyError(error) {
|
|
328
|
+
return typeof error === "object" && error !== null && error.code === 11e3;
|
|
329
|
+
}
|
|
80
330
|
var MongoDbErrorAdapter = class {
|
|
331
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
81
332
|
constructor(collection, options = {}) {
|
|
82
333
|
this.collection = collection;
|
|
83
|
-
this.prefix = options.prefix ??
|
|
334
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
335
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "MongoDbErrorAdapter");
|
|
84
336
|
}
|
|
85
337
|
async log(entry) {
|
|
86
338
|
try {
|
|
87
|
-
|
|
339
|
+
const document = hasCallerId(entry) ? { _id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry);
|
|
340
|
+
await withTimeout(
|
|
341
|
+
() => this.collection.insertOne(document),
|
|
342
|
+
this.timeoutMs,
|
|
343
|
+
timeoutMessage("MongoDB", this.timeoutMs)
|
|
344
|
+
);
|
|
88
345
|
return { ok: true };
|
|
89
346
|
} catch (error) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
347
|
+
if (hasCallerId(entry) && isDuplicateKeyError(error)) {
|
|
348
|
+
return { ok: true };
|
|
349
|
+
}
|
|
350
|
+
return reportFailure({
|
|
351
|
+
adapterName: "MongoDbErrorAdapter",
|
|
352
|
+
prefix: this.prefix,
|
|
353
|
+
action: "to insert entry",
|
|
354
|
+
error
|
|
355
|
+
});
|
|
95
356
|
}
|
|
96
357
|
}
|
|
97
358
|
};
|
|
98
359
|
|
|
99
360
|
// src/adapters/composite-error-adapter.ts
|
|
361
|
+
var DEFAULT_COMPOSITE_TIMEOUT_MS = DEFAULT_TIMEOUT_MS * 3;
|
|
100
362
|
var CompositeErrorAdapter = class {
|
|
101
|
-
|
|
363
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
364
|
+
constructor(adapters, options = {}) {
|
|
102
365
|
this.adapters = adapters;
|
|
366
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
367
|
+
options.timeoutMs ?? DEFAULT_COMPOSITE_TIMEOUT_MS,
|
|
368
|
+
"CompositeErrorAdapter"
|
|
369
|
+
);
|
|
103
370
|
}
|
|
104
371
|
async log(entry) {
|
|
105
372
|
const results = await Promise.all(
|
|
106
373
|
this.adapters.map(
|
|
107
|
-
async (adapter) => {
|
|
374
|
+
async (adapter, index) => {
|
|
108
375
|
try {
|
|
109
|
-
return await
|
|
376
|
+
return await withTimeout(
|
|
377
|
+
() => adapter.log(entry),
|
|
378
|
+
this.timeoutMs,
|
|
379
|
+
`adapter ${index} (${adapter.constructor.name}) did not return within ${this.timeoutMs} ms, so the entry may or may not have been stored there`
|
|
380
|
+
);
|
|
110
381
|
} catch (error) {
|
|
382
|
+
if (error instanceof SinkTimeoutError) {
|
|
383
|
+
return { ok: false, error: error.message };
|
|
384
|
+
}
|
|
111
385
|
return {
|
|
112
386
|
ok: false,
|
|
113
387
|
error: `adapter threw instead of returning a result: ${String(error)}`
|
|
@@ -138,58 +412,101 @@ function createErrorLogStore(database, storeName = DEFAULT_STORE_NAME) {
|
|
|
138
412
|
database.createObjectStore(storeName, { keyPath: "id" }).createIndex("timestamp", "timestamp");
|
|
139
413
|
}
|
|
140
414
|
var IndexedDbErrorAdapter = class {
|
|
415
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
141
416
|
constructor(database, options = {}) {
|
|
142
417
|
this.database = database;
|
|
143
418
|
this.storeName = options.storeName ?? DEFAULT_STORE_NAME;
|
|
144
|
-
this.prefix = options.prefix ??
|
|
419
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
420
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
421
|
+
options.timeoutMs,
|
|
422
|
+
"IndexedDbErrorAdapter"
|
|
423
|
+
);
|
|
145
424
|
}
|
|
146
425
|
async log(entry) {
|
|
147
426
|
try {
|
|
148
427
|
const record = {
|
|
149
|
-
id:
|
|
428
|
+
id: resolveEntryId(entry),
|
|
150
429
|
...serializeEntry(entry)
|
|
151
430
|
};
|
|
152
|
-
await this.write(record);
|
|
431
|
+
await this.write(record, hasCallerId(entry));
|
|
153
432
|
return { ok: true };
|
|
154
433
|
} catch (error) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
434
|
+
return reportFailure({
|
|
435
|
+
adapterName: "IndexedDbErrorAdapter",
|
|
436
|
+
prefix: this.prefix,
|
|
437
|
+
action: `to write to object store "${this.storeName}"`,
|
|
438
|
+
error
|
|
439
|
+
});
|
|
160
440
|
}
|
|
161
441
|
}
|
|
162
|
-
/**
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
442
|
+
/**
|
|
443
|
+
* Resolves once the transaction commits; rejects if the request fails, the
|
|
444
|
+
* transaction aborts, or it does not finish within `timeoutMs`.
|
|
445
|
+
*
|
|
446
|
+
* Error events are handled here (`preventDefault` and `stopPropagation`), so a
|
|
447
|
+
* failed log write does not reach the `onerror` handler an app may have set on
|
|
448
|
+
* its own database object.
|
|
449
|
+
*/
|
|
450
|
+
write(record, replaceExisting) {
|
|
451
|
+
return withTimeout(
|
|
452
|
+
() => new Promise(
|
|
453
|
+
(resolve, reject) => {
|
|
454
|
+
const transaction = this.database.transaction(
|
|
455
|
+
this.storeName,
|
|
456
|
+
"readwrite"
|
|
457
|
+
);
|
|
458
|
+
const fail = (what, source) => (event) => {
|
|
459
|
+
event?.preventDefault?.();
|
|
460
|
+
event?.stopPropagation?.();
|
|
461
|
+
reject(new Error(`${what}: ${describeDomError(source.error)}`));
|
|
462
|
+
};
|
|
463
|
+
transaction.oncomplete = () => resolve();
|
|
464
|
+
transaction.onerror = fail("transaction failed", transaction);
|
|
465
|
+
transaction.onabort = fail("transaction aborted", transaction);
|
|
466
|
+
const store = transaction.objectStore(
|
|
467
|
+
this.storeName
|
|
468
|
+
);
|
|
469
|
+
const request = replaceExisting && typeof store.put === "function" ? store.put(record) : store.add(record);
|
|
470
|
+
if (typeof request === "object" && request !== null) {
|
|
471
|
+
request.onerror = fail(
|
|
472
|
+
"request failed",
|
|
473
|
+
request
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
),
|
|
478
|
+
this.timeoutMs,
|
|
479
|
+
timeoutMessage("IndexedDB", this.timeoutMs, "finish the transaction")
|
|
180
480
|
);
|
|
181
481
|
}
|
|
182
482
|
};
|
|
483
|
+
function describeDomError(error) {
|
|
484
|
+
if (error === null || error === void 0) {
|
|
485
|
+
return "no details provided";
|
|
486
|
+
}
|
|
487
|
+
const text = [error.name, error.message].filter((part) => typeof part === "string" && part !== "").join(": ");
|
|
488
|
+
return text === "" ? "no details provided" : text;
|
|
489
|
+
}
|
|
183
490
|
|
|
184
|
-
// src/
|
|
491
|
+
// src/sql-ident.ts
|
|
185
492
|
var DEFAULT_TABLE_NAME = "error_log";
|
|
186
493
|
var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
187
|
-
function
|
|
188
|
-
if (!IDENTIFIER_PATTERN.test(
|
|
494
|
+
function assertIdentifier(value, label, owner, maxLength) {
|
|
495
|
+
if (!IDENTIFIER_PATTERN.test(value)) {
|
|
189
496
|
throw new RangeError(
|
|
190
|
-
|
|
497
|
+
`${owner}: invalid ${label} "${value}". Use only letters, digits and underscores, and do not start with a digit.`
|
|
191
498
|
);
|
|
192
499
|
}
|
|
500
|
+
if (maxLength !== void 0 && value.length > maxLength) {
|
|
501
|
+
throw new RangeError(
|
|
502
|
+
`${owner}: ${label} "${value}" is ${value.length} characters long, but the limit is ${maxLength}.`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/adapters/sqlite-error-adapter.ts
|
|
508
|
+
function assertValidTableName(tableName) {
|
|
509
|
+
assertIdentifier(tableName, "table name", "SqliteErrorAdapter");
|
|
193
510
|
}
|
|
194
511
|
function buildCreateErrorLogTableSql(tableName = DEFAULT_TABLE_NAME) {
|
|
195
512
|
assertValidTableName(tableName);
|
|
@@ -199,23 +516,24 @@ function buildCreateErrorLogTableSql(tableName = DEFAULT_TABLE_NAME) {
|
|
|
199
516
|
];
|
|
200
517
|
}
|
|
201
518
|
var SqliteErrorAdapter = class {
|
|
202
|
-
/** @throws {RangeError} if `options.tableName` is not a plain identifier. */
|
|
519
|
+
/** @throws {RangeError} if `options.tableName` is not a plain identifier or `options.timeoutMs` is invalid. */
|
|
203
520
|
constructor(client, options = {}) {
|
|
204
521
|
this.client = client;
|
|
205
522
|
this.tableReady = null;
|
|
206
523
|
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
207
524
|
assertValidTableName(this.tableName);
|
|
208
525
|
this.createTable = options.createTable ?? true;
|
|
209
|
-
this.prefix = options.prefix ??
|
|
526
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
527
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "SqliteErrorAdapter");
|
|
210
528
|
}
|
|
211
529
|
async log(entry) {
|
|
212
530
|
try {
|
|
213
531
|
await this.ensureTable();
|
|
214
532
|
const serialized = serializeEntry(entry);
|
|
215
|
-
await this.
|
|
216
|
-
`INSERT INTO ${this.tableName} (id, code, message, cause, source, occurred_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
533
|
+
await this.run(
|
|
534
|
+
`INSERT INTO ${this.tableName} (id, code, message, cause, source, occurred_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO NOTHING`,
|
|
217
535
|
[
|
|
218
|
-
|
|
536
|
+
resolveEntryId(entry),
|
|
219
537
|
serialized.code,
|
|
220
538
|
serialized.message,
|
|
221
539
|
serialized.cause === void 0 ? null : JSON.stringify(serialized.cause),
|
|
@@ -225,11 +543,12 @@ var SqliteErrorAdapter = class {
|
|
|
225
543
|
);
|
|
226
544
|
return { ok: true };
|
|
227
545
|
} catch (error) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
546
|
+
return reportFailure({
|
|
547
|
+
adapterName: "SqliteErrorAdapter",
|
|
548
|
+
prefix: this.prefix,
|
|
549
|
+
action: `to write to table "${this.tableName}"`,
|
|
550
|
+
error
|
|
551
|
+
});
|
|
233
552
|
}
|
|
234
553
|
}
|
|
235
554
|
/** Creates the table once. A failed attempt is retried on the next log call. */
|
|
@@ -249,9 +568,17 @@ var SqliteErrorAdapter = class {
|
|
|
249
568
|
}
|
|
250
569
|
async runCreateStatements() {
|
|
251
570
|
for (const sql of buildCreateErrorLogTableSql(this.tableName)) {
|
|
252
|
-
await this.
|
|
571
|
+
await this.run(sql, []);
|
|
253
572
|
}
|
|
254
573
|
}
|
|
574
|
+
/** Runs one statement, giving up waiting after `timeoutMs`. */
|
|
575
|
+
run(sql, params) {
|
|
576
|
+
return withTimeout(
|
|
577
|
+
() => this.client.run(sql, params),
|
|
578
|
+
this.timeoutMs,
|
|
579
|
+
timeoutMessage("SQLite", this.timeoutMs)
|
|
580
|
+
);
|
|
581
|
+
}
|
|
255
582
|
};
|
|
256
583
|
|
|
257
584
|
// src/adapters/web-storage-error-adapter.ts
|
|
@@ -270,7 +597,8 @@ var WebStorageErrorAdapter = class {
|
|
|
270
597
|
this.storage = storage;
|
|
271
598
|
this.key = options.key ?? DEFAULT_STORAGE_KEY;
|
|
272
599
|
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
273
|
-
this.prefix = options.prefix ??
|
|
600
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
601
|
+
this.locks = options.locks === void 0 ? defaultLockManager() : options.locks;
|
|
274
602
|
if (!Number.isInteger(this.maxEntries) || this.maxEntries < 1) {
|
|
275
603
|
throw new RangeError(
|
|
276
604
|
`WebStorageErrorAdapter: maxEntries must be a positive integer, received ${String(options.maxEntries)}.`
|
|
@@ -280,23 +608,49 @@ var WebStorageErrorAdapter = class {
|
|
|
280
608
|
async log(entry) {
|
|
281
609
|
try {
|
|
282
610
|
const record = {
|
|
283
|
-
id:
|
|
611
|
+
id: resolveEntryId(entry),
|
|
284
612
|
...serializeEntry(entry)
|
|
285
613
|
};
|
|
286
|
-
this.
|
|
614
|
+
await this.appendLocked(record);
|
|
287
615
|
return { ok: true };
|
|
288
616
|
} catch (error) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
617
|
+
return reportFailure({
|
|
618
|
+
adapterName: "WebStorageErrorAdapter",
|
|
619
|
+
prefix: this.prefix,
|
|
620
|
+
action: `to write to storage key "${this.key}"`,
|
|
621
|
+
error
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Appends under a cross-tab lock when one is available. If the lock itself
|
|
627
|
+
* cannot be taken (for example a sandboxed frame), the entry is still written
|
|
628
|
+
* without it rather than lost.
|
|
629
|
+
*/
|
|
630
|
+
async appendLocked(record) {
|
|
631
|
+
if (this.locks === null) {
|
|
632
|
+
this.append(record);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
let appended = false;
|
|
636
|
+
try {
|
|
637
|
+
await this.locks.request(`error-log-adapter:${this.key}`, () => {
|
|
638
|
+
appended = true;
|
|
639
|
+
this.append(record);
|
|
640
|
+
});
|
|
641
|
+
} catch (error) {
|
|
642
|
+
if (appended) {
|
|
643
|
+
throw error;
|
|
644
|
+
}
|
|
645
|
+
this.append(record);
|
|
294
646
|
}
|
|
295
647
|
}
|
|
296
|
-
/** Reads, appends, trims, and writes back. Runs without awaiting, so calls never interleave. */
|
|
648
|
+
/** Reads, appends, trims, and writes back. Runs without awaiting, so calls in one tab never interleave. */
|
|
297
649
|
append(record) {
|
|
298
650
|
const records = [
|
|
299
|
-
...this.readRecords()
|
|
651
|
+
...this.readRecords().filter(
|
|
652
|
+
(existing) => existing?.id !== record.id
|
|
653
|
+
),
|
|
300
654
|
record
|
|
301
655
|
].slice(-this.maxEntries);
|
|
302
656
|
for (; ; ) {
|
|
@@ -322,16 +676,51 @@ var WebStorageErrorAdapter = class {
|
|
|
322
676
|
if (Array.isArray(parsed)) {
|
|
323
677
|
return parsed;
|
|
324
678
|
}
|
|
325
|
-
|
|
326
|
-
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" did not hold an array, so it was replaced.`
|
|
327
|
-
);
|
|
679
|
+
this.setAside(stored, "did not hold an array");
|
|
328
680
|
} catch (error) {
|
|
329
|
-
|
|
330
|
-
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" held invalid JSON (${describeError(error)}), so it was replaced.`
|
|
331
|
-
);
|
|
681
|
+
this.setAside(stored, `held invalid JSON (${describeError(error)})`);
|
|
332
682
|
}
|
|
333
683
|
return [];
|
|
334
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* Copies an unusable value to `<key>.corrupt` before it is replaced, so data
|
|
687
|
+
* the host app stored under the same key is not silently destroyed. Only the
|
|
688
|
+
* first one is kept; a later unusable value would not overwrite it.
|
|
689
|
+
*/
|
|
690
|
+
setAside(stored, reason) {
|
|
691
|
+
const backupKey = `${this.key}${CORRUPT_KEY_SUFFIX}`;
|
|
692
|
+
let savedTo = "but it could not be copied aside";
|
|
693
|
+
try {
|
|
694
|
+
if (this.storage.getItem(backupKey) === null) {
|
|
695
|
+
this.storage.setItem(backupKey, stored);
|
|
696
|
+
savedTo = `and a copy was kept under "${backupKey}"`;
|
|
697
|
+
} else {
|
|
698
|
+
savedTo = `and "${backupKey}" already holds an earlier copy`;
|
|
699
|
+
}
|
|
700
|
+
} catch {
|
|
701
|
+
}
|
|
702
|
+
console.error(
|
|
703
|
+
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" ${reason}, so it was replaced, ${savedTo}.`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
var CORRUPT_KEY_SUFFIX = ".corrupt";
|
|
708
|
+
function defaultLockManager() {
|
|
709
|
+
const locks = globalThis.navigator?.locks;
|
|
710
|
+
return typeof locks?.request === "function" ? locks : null;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/queue.ts
|
|
714
|
+
var SerialQueue = class {
|
|
715
|
+
constructor() {
|
|
716
|
+
this.tail = Promise.resolve();
|
|
717
|
+
}
|
|
718
|
+
/** Starts `task` once every earlier task has finished, and resolves with its result. */
|
|
719
|
+
run(task) {
|
|
720
|
+
const result = this.tail.then(task);
|
|
721
|
+
this.tail = result.catch(() => void 0);
|
|
722
|
+
return result;
|
|
723
|
+
}
|
|
335
724
|
};
|
|
336
725
|
|
|
337
726
|
// src/file-system.ts
|
|
@@ -339,6 +728,7 @@ async function loadNodeFileSystem() {
|
|
|
339
728
|
const moduleName = "node:fs/promises";
|
|
340
729
|
return await import(moduleName);
|
|
341
730
|
}
|
|
731
|
+
var ROOT_PATTERN = /^(?:[A-Za-z]:)?[\\/]+$/;
|
|
342
732
|
function parentDirectory(filePath) {
|
|
343
733
|
const lastSeparator = Math.max(
|
|
344
734
|
filePath.lastIndexOf("/"),
|
|
@@ -347,7 +737,11 @@ function parentDirectory(filePath) {
|
|
|
347
737
|
if (lastSeparator < 0) {
|
|
348
738
|
return null;
|
|
349
739
|
}
|
|
350
|
-
|
|
740
|
+
const directory = filePath.slice(0, lastSeparator + 1);
|
|
741
|
+
if (ROOT_PATTERN.test(directory)) {
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
return filePath.slice(0, lastSeparator);
|
|
351
745
|
}
|
|
352
746
|
function isMissingFileError(error) {
|
|
353
747
|
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
@@ -393,24 +787,20 @@ var FlatFileErrorAdapter = class {
|
|
|
393
787
|
constructor(filePath, options = {}) {
|
|
394
788
|
this.filePath = filePath;
|
|
395
789
|
this.directoryReady = false;
|
|
396
|
-
this.queue =
|
|
790
|
+
this.queue = new SerialQueue();
|
|
397
791
|
this.fs = options.fs;
|
|
398
792
|
this.formatLine = options.formatLine ?? formatErrorLogLine;
|
|
399
793
|
this.maxBytes = options.maxBytes;
|
|
400
794
|
this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
401
795
|
this.createDirectory = options.createDirectory ?? true;
|
|
402
|
-
this.prefix = options.prefix ??
|
|
796
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
403
797
|
if (this.maxBytes !== void 0) {
|
|
404
798
|
assertPositiveInteger(this.maxBytes, "maxBytes");
|
|
405
799
|
}
|
|
406
800
|
assertPositiveInteger(this.maxFiles, "maxFiles");
|
|
407
801
|
}
|
|
408
802
|
log(entry) {
|
|
409
|
-
|
|
410
|
-
() => this.write(entry)
|
|
411
|
-
);
|
|
412
|
-
this.queue = result.catch(() => void 0);
|
|
413
|
-
return result;
|
|
803
|
+
return this.queue.run(() => this.write(entry));
|
|
414
804
|
}
|
|
415
805
|
async write(entry) {
|
|
416
806
|
try {
|
|
@@ -423,11 +813,12 @@ var FlatFileErrorAdapter = class {
|
|
|
423
813
|
await fs.appendFile(this.filePath, line);
|
|
424
814
|
return { ok: true };
|
|
425
815
|
} catch (error) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
816
|
+
return reportFailure({
|
|
817
|
+
adapterName: "FlatFileErrorAdapter",
|
|
818
|
+
prefix: this.prefix,
|
|
819
|
+
action: `to append to "${this.filePath}"`,
|
|
820
|
+
error
|
|
821
|
+
});
|
|
431
822
|
}
|
|
432
823
|
}
|
|
433
824
|
/** Uses the injected file system, or loads `node:fs/promises` on first use. */
|
|
@@ -502,13 +893,13 @@ var JsonFileErrorAdapter = class {
|
|
|
502
893
|
constructor(filePath, options = {}) {
|
|
503
894
|
this.filePath = filePath;
|
|
504
895
|
this.directoryReady = false;
|
|
505
|
-
this.queue =
|
|
896
|
+
this.queue = new SerialQueue();
|
|
506
897
|
this.mode = options.mode ?? "ndjson";
|
|
507
898
|
this.maxEntries = options.maxEntries;
|
|
508
899
|
this.indent = options.indent ?? DEFAULT_INDENT;
|
|
509
900
|
this.fs = options.fs;
|
|
510
901
|
this.createDirectory = options.createDirectory ?? true;
|
|
511
|
-
this.prefix = options.prefix ??
|
|
902
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
512
903
|
if (this.maxEntries !== void 0) {
|
|
513
904
|
if (this.mode !== "array") {
|
|
514
905
|
throw new RangeError(
|
|
@@ -528,16 +919,12 @@ var JsonFileErrorAdapter = class {
|
|
|
528
919
|
}
|
|
529
920
|
}
|
|
530
921
|
log(entry) {
|
|
531
|
-
|
|
532
|
-
() => this.write(entry)
|
|
533
|
-
);
|
|
534
|
-
this.queue = result.catch(() => void 0);
|
|
535
|
-
return result;
|
|
922
|
+
return this.queue.run(() => this.write(entry));
|
|
536
923
|
}
|
|
537
924
|
async write(entry) {
|
|
538
925
|
try {
|
|
539
926
|
const record = {
|
|
540
|
-
id:
|
|
927
|
+
id: resolveEntryId(entry),
|
|
541
928
|
...serializeEntry(entry)
|
|
542
929
|
};
|
|
543
930
|
const fs = await this.getFileSystem();
|
|
@@ -550,11 +937,12 @@ var JsonFileErrorAdapter = class {
|
|
|
550
937
|
}
|
|
551
938
|
return { ok: true };
|
|
552
939
|
} catch (error) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
940
|
+
return reportFailure({
|
|
941
|
+
adapterName: "JsonFileErrorAdapter",
|
|
942
|
+
prefix: this.prefix,
|
|
943
|
+
action: `to write "${this.filePath}" in ${this.mode} mode`,
|
|
944
|
+
error
|
|
945
|
+
});
|
|
558
946
|
}
|
|
559
947
|
}
|
|
560
948
|
async getFileSystem() {
|
|
@@ -580,7 +968,9 @@ var JsonFileErrorAdapter = class {
|
|
|
580
968
|
*/
|
|
581
969
|
async rewriteArray(fs, record) {
|
|
582
970
|
const records = [
|
|
583
|
-
...await this.readRecords(fs)
|
|
971
|
+
...(await this.readRecords(fs)).filter(
|
|
972
|
+
(existing) => existing?.id !== record.id
|
|
973
|
+
),
|
|
584
974
|
record
|
|
585
975
|
];
|
|
586
976
|
const kept = this.maxEntries === void 0 ? records : records.slice(-this.maxEntries);
|
|
@@ -630,25 +1020,14 @@ var JsonFileErrorAdapter = class {
|
|
|
630
1020
|
};
|
|
631
1021
|
|
|
632
1022
|
// src/adapters/sql-error-adapter.ts
|
|
633
|
-
var DEFAULT_TABLE_NAME2 = "error_log";
|
|
634
1023
|
var MAX_TABLE_NAME_LENGTH = 47;
|
|
635
|
-
var IDENTIFIER_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
636
1024
|
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
|
-
}
|
|
1025
|
+
assertIdentifier(tableName, "table name", adapterName, MAX_TABLE_NAME_LENGTH);
|
|
647
1026
|
}
|
|
648
1027
|
function quoteIdentifier(dialect, identifier) {
|
|
649
1028
|
return dialect === "postgres" ? `"${identifier}"` : `\`${identifier}\``;
|
|
650
1029
|
}
|
|
651
|
-
function buildErrorLogTableDdl(dialect, tableName =
|
|
1030
|
+
function buildErrorLogTableDdl(dialect, tableName = DEFAULT_TABLE_NAME) {
|
|
652
1031
|
assertValidTableName2(tableName, "buildErrorLogTableDdl");
|
|
653
1032
|
const table = quoteIdentifier(dialect, tableName);
|
|
654
1033
|
const index = quoteIdentifier(
|
|
@@ -662,7 +1041,13 @@ function buildErrorLogTableDdl(dialect, tableName = DEFAULT_TABLE_NAME2) {
|
|
|
662
1041
|
];
|
|
663
1042
|
}
|
|
664
1043
|
return [
|
|
665
|
-
`CREATE TABLE IF NOT EXISTS ${table} (id CHAR(36) NOT NULL PRIMARY KEY, code
|
|
1044
|
+
`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))`
|
|
1045
|
+
];
|
|
1046
|
+
}
|
|
1047
|
+
function buildMySqlWidenColumnsSql(tableName = DEFAULT_TABLE_NAME) {
|
|
1048
|
+
assertValidTableName2(tableName, "buildMySqlWidenColumnsSql");
|
|
1049
|
+
return [
|
|
1050
|
+
`ALTER TABLE ${quoteIdentifier("mysql", tableName)} MODIFY code TEXT NOT NULL, MODIFY source TEXT NULL`
|
|
666
1051
|
];
|
|
667
1052
|
}
|
|
668
1053
|
function toMysqlDateTime(date) {
|
|
@@ -674,10 +1059,11 @@ var SqlErrorAdapter = class {
|
|
|
674
1059
|
this.dialect = dialect;
|
|
675
1060
|
this.adapterName = adapterName;
|
|
676
1061
|
this.tableReady = null;
|
|
677
|
-
this.tableName = options.tableName ??
|
|
1062
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
678
1063
|
assertValidTableName2(this.tableName, adapterName);
|
|
679
1064
|
this.createTable = options.createTable ?? true;
|
|
680
|
-
this.prefix = options.prefix ??
|
|
1065
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1066
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, adapterName);
|
|
681
1067
|
}
|
|
682
1068
|
async log(entry) {
|
|
683
1069
|
try {
|
|
@@ -689,7 +1075,7 @@ var SqlErrorAdapter = class {
|
|
|
689
1075
|
);
|
|
690
1076
|
}
|
|
691
1077
|
const params = [
|
|
692
|
-
|
|
1078
|
+
resolveEntryId(entry),
|
|
693
1079
|
serialized.code,
|
|
694
1080
|
serialized.message,
|
|
695
1081
|
serialized.cause === void 0 ? null : JSON.stringify(serialized.cause),
|
|
@@ -697,19 +1083,21 @@ var SqlErrorAdapter = class {
|
|
|
697
1083
|
this.dialect === "postgres" ? occurredAt.toISOString() : toMysqlDateTime(occurredAt)
|
|
698
1084
|
];
|
|
699
1085
|
await this.ensureTable();
|
|
700
|
-
await this.
|
|
1086
|
+
await this.query(this.insertSql(), params);
|
|
701
1087
|
return { ok: true };
|
|
702
1088
|
} catch (error) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1089
|
+
return reportFailure({
|
|
1090
|
+
adapterName: this.adapterName,
|
|
1091
|
+
prefix: this.prefix,
|
|
1092
|
+
action: `to write to table "${this.tableName}"`,
|
|
1093
|
+
error
|
|
1094
|
+
});
|
|
708
1095
|
}
|
|
709
1096
|
}
|
|
710
1097
|
insertSql() {
|
|
711
1098
|
const placeholders = this.dialect === "postgres" ? "$1, $2, $3, $4, $5, $6" : "?, ?, ?, ?, ?, ?";
|
|
712
|
-
|
|
1099
|
+
const onRepeat = this.dialect === "postgres" ? "ON CONFLICT (id) DO NOTHING" : "ON DUPLICATE KEY UPDATE id = id";
|
|
1100
|
+
return `INSERT INTO ${quoteIdentifier(this.dialect, this.tableName)} (id, code, message, cause, source, occurred_at) VALUES (${placeholders}) ${onRepeat}`;
|
|
713
1101
|
}
|
|
714
1102
|
/** Creates the table once. A failed attempt is retried on the next log call. */
|
|
715
1103
|
ensureTable() {
|
|
@@ -728,9 +1116,20 @@ var SqlErrorAdapter = class {
|
|
|
728
1116
|
}
|
|
729
1117
|
async runCreateStatements() {
|
|
730
1118
|
for (const sql of buildErrorLogTableDdl(this.dialect, this.tableName)) {
|
|
731
|
-
await this.
|
|
1119
|
+
await this.query(sql, []);
|
|
732
1120
|
}
|
|
733
1121
|
}
|
|
1122
|
+
/** Runs one statement, giving up waiting after `timeoutMs`. */
|
|
1123
|
+
query(sql, params) {
|
|
1124
|
+
return withTimeout(
|
|
1125
|
+
() => this.client.query(sql, params),
|
|
1126
|
+
this.timeoutMs,
|
|
1127
|
+
timeoutMessage(
|
|
1128
|
+
this.dialect === "postgres" ? "Postgres" : "MySQL",
|
|
1129
|
+
this.timeoutMs
|
|
1130
|
+
)
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
734
1133
|
};
|
|
735
1134
|
var PostgresErrorAdapter = class extends SqlErrorAdapter {
|
|
736
1135
|
/** @throws {RangeError} if `options.tableName` is not a plain identifier of 47 characters or fewer. */
|
|
@@ -755,7 +1154,8 @@ var RedisErrorAdapter = class {
|
|
|
755
1154
|
this.key = options.key ?? DEFAULT_REDIS_KEY;
|
|
756
1155
|
this.maxEntries = options.maxEntries;
|
|
757
1156
|
this.side = options.side ?? "right";
|
|
758
|
-
this.prefix = options.prefix ??
|
|
1157
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1158
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "RedisErrorAdapter");
|
|
759
1159
|
if (this.mode !== "list" && this.mode !== "stream") {
|
|
760
1160
|
throw new RangeError(
|
|
761
1161
|
`RedisErrorAdapter: mode must be "list" or "stream", received "${String(options.mode)}".`
|
|
@@ -777,11 +1177,11 @@ var RedisErrorAdapter = class {
|
|
|
777
1177
|
}
|
|
778
1178
|
async log(entry) {
|
|
779
1179
|
try {
|
|
780
|
-
const record = { id:
|
|
1180
|
+
const record = { id: resolveEntryId(entry), ...serializeEntry(entry) };
|
|
781
1181
|
if (this.mode === "stream") {
|
|
782
|
-
await this.
|
|
1182
|
+
await this.send(this.buildStreamCommand(record));
|
|
783
1183
|
} else {
|
|
784
|
-
await this.
|
|
1184
|
+
await this.send([
|
|
785
1185
|
this.side === "left" ? "LPUSH" : "RPUSH",
|
|
786
1186
|
this.key,
|
|
787
1187
|
JSON.stringify(record)
|
|
@@ -790,11 +1190,12 @@ var RedisErrorAdapter = class {
|
|
|
790
1190
|
}
|
|
791
1191
|
return { ok: true };
|
|
792
1192
|
} catch (error) {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1193
|
+
return reportFailure({
|
|
1194
|
+
adapterName: "RedisErrorAdapter",
|
|
1195
|
+
prefix: this.prefix,
|
|
1196
|
+
action: `to write to ${this.mode} "${this.key}"`,
|
|
1197
|
+
error
|
|
1198
|
+
});
|
|
798
1199
|
}
|
|
799
1200
|
}
|
|
800
1201
|
buildStreamCommand(record) {
|
|
@@ -826,6 +1227,14 @@ var RedisErrorAdapter = class {
|
|
|
826
1227
|
* stored by now, so a trim failure is reported but does not fail the call;
|
|
827
1228
|
* the next successful write trims the list again.
|
|
828
1229
|
*/
|
|
1230
|
+
/** Sends one command, giving up waiting after `timeoutMs`. */
|
|
1231
|
+
send(args) {
|
|
1232
|
+
return withTimeout(
|
|
1233
|
+
() => this.client.sendCommand(args),
|
|
1234
|
+
this.timeoutMs,
|
|
1235
|
+
timeoutMessage("Redis", this.timeoutMs)
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
829
1238
|
async trimList() {
|
|
830
1239
|
if (this.maxEntries === void 0) {
|
|
831
1240
|
return;
|
|
@@ -833,7 +1242,7 @@ var RedisErrorAdapter = class {
|
|
|
833
1242
|
const keep = this.maxEntries;
|
|
834
1243
|
const [start, stop] = this.side === "right" ? [String(-keep), "-1"] : ["0", String(keep - 1)];
|
|
835
1244
|
try {
|
|
836
|
-
await this.
|
|
1245
|
+
await this.send(["LTRIM", this.key, start, stop]);
|
|
837
1246
|
} catch (error) {
|
|
838
1247
|
console.error(
|
|
839
1248
|
`${this.prefix} RedisErrorAdapter: stored the entry but could not trim list "${this.key}" to ${keep} entries: ${describeError(error)}`
|
|
@@ -860,11 +1269,12 @@ var S3ErrorAdapter = class {
|
|
|
860
1269
|
this.bucket = bucket;
|
|
861
1270
|
this.buffer = [];
|
|
862
1271
|
this.timer = null;
|
|
863
|
-
this.flushQueue =
|
|
1272
|
+
this.flushQueue = new SerialQueue();
|
|
864
1273
|
this.keyPrefix = normalizePrefix(options.keyPrefix ?? DEFAULT_S3_PREFIX);
|
|
865
1274
|
this.batch = options.batch;
|
|
866
1275
|
this.contentType = options.contentType ?? (this.batch === void 0 ? "application/json" : "application/x-ndjson");
|
|
867
|
-
this.logPrefix = options.prefix ??
|
|
1276
|
+
this.logPrefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1277
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "S3ErrorAdapter");
|
|
868
1278
|
if (bucket === "") {
|
|
869
1279
|
throw new RangeError("S3ErrorAdapter: bucket must not be empty.");
|
|
870
1280
|
}
|
|
@@ -884,7 +1294,7 @@ var S3ErrorAdapter = class {
|
|
|
884
1294
|
async log(entry) {
|
|
885
1295
|
try {
|
|
886
1296
|
const record = {
|
|
887
|
-
id:
|
|
1297
|
+
id: resolveEntryId(entry),
|
|
888
1298
|
...serializeEntry(entry)
|
|
889
1299
|
};
|
|
890
1300
|
const occurredAt = new Date(record.timestamp);
|
|
@@ -909,11 +1319,9 @@ var S3ErrorAdapter = class {
|
|
|
909
1319
|
* Entries from a failed upload stay buffered and are retried by the next flush.
|
|
910
1320
|
*/
|
|
911
1321
|
flush() {
|
|
912
|
-
|
|
1322
|
+
return this.flushQueue.run(
|
|
913
1323
|
() => this.uploadBuffered()
|
|
914
1324
|
);
|
|
915
|
-
this.flushQueue = result.catch(() => void 0);
|
|
916
|
-
return result;
|
|
917
1325
|
}
|
|
918
1326
|
async addToBatch(record, batch) {
|
|
919
1327
|
this.buffer.push(record);
|
|
@@ -952,7 +1360,7 @@ var S3ErrorAdapter = class {
|
|
|
952
1360
|
return { ok: true };
|
|
953
1361
|
}
|
|
954
1362
|
try {
|
|
955
|
-
const key = `${this.keyPrefix}${datePath(/* @__PURE__ */ new Date())}-${
|
|
1363
|
+
const key = `${this.keyPrefix}${datePath(/* @__PURE__ */ new Date())}-${generateId()}.ndjson`;
|
|
956
1364
|
const body = pending.map((record) => JSON.stringify(record)).join("\n");
|
|
957
1365
|
await this.upload(key, `${body}
|
|
958
1366
|
`);
|
|
@@ -969,64 +1377,41 @@ var S3ErrorAdapter = class {
|
|
|
969
1377
|
}
|
|
970
1378
|
}
|
|
971
1379
|
upload(key, body) {
|
|
972
|
-
return
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1380
|
+
return withTimeout(
|
|
1381
|
+
() => this.client.putObject({
|
|
1382
|
+
Bucket: this.bucket,
|
|
1383
|
+
Key: key,
|
|
1384
|
+
Body: body,
|
|
1385
|
+
ContentType: this.contentType
|
|
1386
|
+
}),
|
|
1387
|
+
this.timeoutMs,
|
|
1388
|
+
timeoutMessage("S3", this.timeoutMs)
|
|
1389
|
+
);
|
|
978
1390
|
}
|
|
979
1391
|
fail(error, bufferedCount) {
|
|
980
|
-
console.error(`${this.logPrefix} S3ErrorAdapter:`, error);
|
|
981
1392
|
const retry = bufferedCount === void 0 ? "" : ` (${bufferedCount} buffered ${bufferedCount === 1 ? "entry was" : "entries were"} kept for the next flush)`;
|
|
982
|
-
return {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1393
|
+
return reportFailure({
|
|
1394
|
+
adapterName: "S3ErrorAdapter",
|
|
1395
|
+
prefix: this.logPrefix,
|
|
1396
|
+
action: `to upload to bucket "${this.bucket}"`,
|
|
1397
|
+
error,
|
|
1398
|
+
reason: `${describeError(error)}${retry}`
|
|
1399
|
+
});
|
|
986
1400
|
}
|
|
987
1401
|
};
|
|
988
1402
|
|
|
989
1403
|
// 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
|
-
}
|
|
1404
|
+
function waitForWrite(start, timeoutMs) {
|
|
1405
|
+
return withTimeout(
|
|
1406
|
+
start,
|
|
1407
|
+
timeoutMs,
|
|
1408
|
+
`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
1409
|
);
|
|
1025
1410
|
}
|
|
1026
1411
|
function buildRecord(entry) {
|
|
1027
1412
|
const serialized = serializeEntry(entry);
|
|
1028
1413
|
const record = {
|
|
1029
|
-
id:
|
|
1414
|
+
id: resolveEntryId(entry),
|
|
1030
1415
|
code: serialized.code,
|
|
1031
1416
|
message: serialized.message,
|
|
1032
1417
|
timestamp: serialized.timestamp
|
|
@@ -1043,23 +1428,27 @@ var FirestoreErrorAdapter = class {
|
|
|
1043
1428
|
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
|
|
1044
1429
|
constructor(collection, options = {}) {
|
|
1045
1430
|
this.collection = collection;
|
|
1046
|
-
this.prefix = options.prefix ??
|
|
1047
|
-
this.timeoutMs =
|
|
1431
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1432
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1433
|
+
options.timeoutMs,
|
|
1434
|
+
"FirestoreErrorAdapter"
|
|
1435
|
+
);
|
|
1048
1436
|
}
|
|
1049
1437
|
async log(entry) {
|
|
1050
1438
|
try {
|
|
1051
1439
|
const record = buildRecord(entry);
|
|
1052
1440
|
await waitForWrite(
|
|
1053
|
-
this.collection.doc(record.id).set(record),
|
|
1441
|
+
() => this.collection.doc(record.id).set(record),
|
|
1054
1442
|
this.timeoutMs
|
|
1055
1443
|
);
|
|
1056
1444
|
return { ok: true };
|
|
1057
1445
|
} catch (error) {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1446
|
+
return reportFailure({
|
|
1447
|
+
adapterName: "FirestoreErrorAdapter",
|
|
1448
|
+
prefix: this.prefix,
|
|
1449
|
+
action: `to write the document`,
|
|
1450
|
+
error
|
|
1451
|
+
});
|
|
1063
1452
|
}
|
|
1064
1453
|
}
|
|
1065
1454
|
};
|
|
@@ -1067,34 +1456,51 @@ var RealtimeDatabaseErrorAdapter = class {
|
|
|
1067
1456
|
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
|
|
1068
1457
|
constructor(reference, options = {}) {
|
|
1069
1458
|
this.reference = reference;
|
|
1070
|
-
this.prefix = options.prefix ??
|
|
1071
|
-
this.timeoutMs =
|
|
1459
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1460
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1461
|
+
options.timeoutMs,
|
|
1462
|
+
"RealtimeDatabaseErrorAdapter"
|
|
1463
|
+
);
|
|
1072
1464
|
}
|
|
1073
1465
|
async log(entry) {
|
|
1074
1466
|
try {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1467
|
+
const record = buildRecord(entry);
|
|
1468
|
+
let write;
|
|
1469
|
+
if (hasCallerId(entry)) {
|
|
1470
|
+
if (this.reference.child === void 0) {
|
|
1471
|
+
throw new TypeError(
|
|
1472
|
+
"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"
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
write = this.reference.child(record.id).set(record);
|
|
1476
|
+
} else {
|
|
1477
|
+
write = this.reference.push(record);
|
|
1478
|
+
}
|
|
1479
|
+
await waitForWrite(() => write, this.timeoutMs);
|
|
1079
1480
|
return { ok: true };
|
|
1080
1481
|
} catch (error) {
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1482
|
+
return reportFailure({
|
|
1483
|
+
adapterName: "RealtimeDatabaseErrorAdapter",
|
|
1484
|
+
prefix: this.prefix,
|
|
1485
|
+
action: `to write the entry`,
|
|
1486
|
+
error
|
|
1487
|
+
});
|
|
1086
1488
|
}
|
|
1087
1489
|
}
|
|
1088
1490
|
};
|
|
1089
1491
|
|
|
1090
1492
|
// src/adapters/supabase-error-adapter.ts
|
|
1091
|
-
var
|
|
1092
|
-
|
|
1493
|
+
var POLICY_MAX_CODE_LENGTH = 255;
|
|
1494
|
+
var POLICY_MAX_MESSAGE_LENGTH = 4096;
|
|
1495
|
+
var POLICY_MAX_CAUSE_BYTES = 8192;
|
|
1496
|
+
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})`;
|
|
1497
|
+
function buildSupabaseSetupSql(tableName = DEFAULT_TABLE_NAME, options = {}) {
|
|
1093
1498
|
const insertRoles = options.insertRoles ?? [];
|
|
1094
1499
|
for (const role of insertRoles) {
|
|
1095
|
-
|
|
1500
|
+
assertIdentifier(role, "role name", "buildSupabaseSetupSql");
|
|
1501
|
+
if ((role.toLowerCase() === "anon" || role.toLowerCase() === "public") && options.allowPublicInsert !== true) {
|
|
1096
1502
|
throw new RangeError(
|
|
1097
|
-
`buildSupabaseSetupSql:
|
|
1503
|
+
`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
1504
|
);
|
|
1099
1505
|
}
|
|
1100
1506
|
}
|
|
@@ -1106,7 +1512,7 @@ function buildSupabaseSetupSql(tableName = DEFAULT_TABLE_NAME2, options = {}) {
|
|
|
1106
1512
|
const policy = `${tableName}_insert_only`;
|
|
1107
1513
|
statements.push(
|
|
1108
1514
|
`DROP POLICY IF EXISTS "${policy}" ON "${tableName}"`,
|
|
1109
|
-
`CREATE POLICY "${policy}" ON "${tableName}" FOR INSERT TO ${insertRoles.map((role) => `"${role}"`).join(", ")} WITH CHECK (
|
|
1515
|
+
`CREATE POLICY "${policy}" ON "${tableName}" FOR INSERT TO ${insertRoles.map((role) => `"${role}"`).join(", ")} WITH CHECK (${INSERT_POLICY_CHECK})`
|
|
1110
1516
|
);
|
|
1111
1517
|
}
|
|
1112
1518
|
return statements;
|
|
@@ -1136,44 +1542,52 @@ function describeSupabaseError(result) {
|
|
|
1136
1542
|
return parts.join("; ");
|
|
1137
1543
|
}
|
|
1138
1544
|
var SupabaseErrorAdapter = class {
|
|
1139
|
-
/** @throws {RangeError} if `options.tableName` is
|
|
1545
|
+
/** @throws {RangeError} if `options.tableName` is not a plain identifier or `options.timeoutMs` is invalid. */
|
|
1140
1546
|
constructor(client, options = {}) {
|
|
1141
1547
|
this.client = client;
|
|
1142
|
-
this.tableName = options.tableName ??
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1548
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
1549
|
+
assertIdentifier(this.tableName, "table name", "SupabaseErrorAdapter");
|
|
1550
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1551
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1552
|
+
options.timeoutMs,
|
|
1553
|
+
"SupabaseErrorAdapter"
|
|
1554
|
+
);
|
|
1149
1555
|
}
|
|
1150
1556
|
async log(entry) {
|
|
1151
1557
|
try {
|
|
1152
1558
|
const serialized = serializeEntry(entry);
|
|
1153
1559
|
const row = {
|
|
1154
|
-
id:
|
|
1560
|
+
id: resolveEntryId(entry),
|
|
1155
1561
|
code: serialized.code,
|
|
1156
1562
|
message: serialized.message,
|
|
1157
1563
|
cause: serialized.cause === void 0 ? null : serialized.cause,
|
|
1158
1564
|
source: serialized.source ?? null,
|
|
1159
1565
|
occurred_at: serialized.timestamp
|
|
1160
1566
|
};
|
|
1161
|
-
const result = await
|
|
1567
|
+
const result = await withTimeout(
|
|
1568
|
+
() => this.client.from(this.tableName).insert(row),
|
|
1569
|
+
this.timeoutMs,
|
|
1570
|
+
timeoutMessage("Supabase", this.timeoutMs)
|
|
1571
|
+
);
|
|
1162
1572
|
if (result.error) {
|
|
1163
|
-
|
|
1164
|
-
|
|
1573
|
+
if (hasCallerId(entry) && result.error.code === "23505") {
|
|
1574
|
+
return { ok: true };
|
|
1575
|
+
}
|
|
1576
|
+
return this.fail(result, describeSupabaseError(result));
|
|
1165
1577
|
}
|
|
1166
1578
|
return { ok: true };
|
|
1167
1579
|
} catch (error) {
|
|
1168
|
-
|
|
1169
|
-
return this.fail(describeError(error));
|
|
1580
|
+
return this.fail(error, describeError(error));
|
|
1170
1581
|
}
|
|
1171
1582
|
}
|
|
1172
|
-
fail(reason) {
|
|
1173
|
-
return {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1583
|
+
fail(logged, reason) {
|
|
1584
|
+
return reportFailure({
|
|
1585
|
+
adapterName: "SupabaseErrorAdapter",
|
|
1586
|
+
prefix: this.prefix,
|
|
1587
|
+
action: `to insert into table "${this.tableName}"`,
|
|
1588
|
+
error: logged,
|
|
1589
|
+
reason
|
|
1590
|
+
});
|
|
1177
1591
|
}
|
|
1178
1592
|
};
|
|
1179
1593
|
export {
|
|
@@ -1195,6 +1609,7 @@ export {
|
|
|
1195
1609
|
WebhookErrorAdapter,
|
|
1196
1610
|
buildCreateErrorLogTableSql,
|
|
1197
1611
|
buildErrorLogTableDdl,
|
|
1612
|
+
buildMySqlWidenColumnsSql,
|
|
1198
1613
|
buildSupabaseSetupSql,
|
|
1199
1614
|
createErrorLogStore,
|
|
1200
1615
|
formatErrorLogLine,
|