@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.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
WebhookErrorAdapter: () => WebhookErrorAdapter,
|
|
39
39
|
buildCreateErrorLogTableSql: () => buildCreateErrorLogTableSql,
|
|
40
40
|
buildErrorLogTableDdl: () => buildErrorLogTableDdl,
|
|
41
|
+
buildMySqlWidenColumnsSql: () => buildMySqlWidenColumnsSql,
|
|
41
42
|
buildSupabaseSetupSql: () => buildSupabaseSetupSql,
|
|
42
43
|
createErrorLogStore: () => createErrorLogStore,
|
|
43
44
|
formatErrorLogLine: () => formatErrorLogLine,
|
|
@@ -47,11 +48,100 @@ __export(index_exports, {
|
|
|
47
48
|
module.exports = __toCommonJS(index_exports);
|
|
48
49
|
|
|
49
50
|
// src/serialize.ts
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
var MAX_CAUSE_DEPTH = 6;
|
|
52
|
+
function isErrorLike(value) {
|
|
53
|
+
if (value instanceof Error) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const candidate = value;
|
|
57
|
+
return typeof candidate.name === "string" && typeof candidate.message === "string" && typeof value.stack === "string";
|
|
58
|
+
}
|
|
59
|
+
function readProperty(source, key) {
|
|
60
|
+
try {
|
|
61
|
+
return source[key];
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return `[Unreadable: ${error instanceof Error ? error.message : String(error)}]`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function toJsonSafe(value, depth, ancestors) {
|
|
67
|
+
switch (typeof value) {
|
|
68
|
+
case "string":
|
|
69
|
+
case "boolean":
|
|
70
|
+
case "undefined":
|
|
71
|
+
return value;
|
|
72
|
+
case "number":
|
|
73
|
+
return Number.isFinite(value) ? value : String(value);
|
|
74
|
+
case "bigint":
|
|
75
|
+
return `${value}n`;
|
|
76
|
+
case "symbol":
|
|
77
|
+
return value.toString();
|
|
78
|
+
case "function":
|
|
79
|
+
return `[Function ${value.name || "anonymous"}]`;
|
|
80
|
+
default:
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
if (value === null) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const objectValue = value;
|
|
87
|
+
if (ancestors.has(objectValue)) {
|
|
88
|
+
return "[Circular]";
|
|
89
|
+
}
|
|
90
|
+
if (depth >= MAX_CAUSE_DEPTH) {
|
|
91
|
+
return "[Truncated]";
|
|
92
|
+
}
|
|
93
|
+
ancestors.add(objectValue);
|
|
94
|
+
try {
|
|
95
|
+
if (isErrorLike(objectValue)) {
|
|
96
|
+
const result2 = {
|
|
97
|
+
name: objectValue.name,
|
|
98
|
+
message: objectValue.message,
|
|
99
|
+
stack: objectValue.stack
|
|
100
|
+
};
|
|
101
|
+
const nested = readProperty(objectValue, "cause");
|
|
102
|
+
if (nested !== void 0) {
|
|
103
|
+
result2["cause"] = toJsonSafe(nested, depth + 1, ancestors);
|
|
104
|
+
}
|
|
105
|
+
const errors = readProperty(objectValue, "errors");
|
|
106
|
+
if (Array.isArray(errors)) {
|
|
107
|
+
result2["errors"] = errors.map(
|
|
108
|
+
(item) => toJsonSafe(item, depth + 1, ancestors)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return result2;
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
return value.map(
|
|
115
|
+
(item) => toJsonSafe(item, depth + 1, ancestors)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const toJson = readProperty(objectValue, "toJSON");
|
|
119
|
+
if (typeof toJson === "function") {
|
|
120
|
+
try {
|
|
121
|
+
return toJsonSafe(
|
|
122
|
+
toJson.call(objectValue),
|
|
123
|
+
depth + 1,
|
|
124
|
+
ancestors
|
|
125
|
+
);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return `[toJSON failed: ${error instanceof Error ? error.message : String(error)}]`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const result = {};
|
|
131
|
+
for (const key of Object.keys(objectValue)) {
|
|
132
|
+
result[key] = toJsonSafe(
|
|
133
|
+
readProperty(objectValue, key),
|
|
134
|
+
depth + 1,
|
|
135
|
+
ancestors
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
} finally {
|
|
140
|
+
ancestors.delete(objectValue);
|
|
53
141
|
}
|
|
54
|
-
|
|
142
|
+
}
|
|
143
|
+
function serializeCause(cause) {
|
|
144
|
+
return toJsonSafe(cause, 0, /* @__PURE__ */ new WeakSet());
|
|
55
145
|
}
|
|
56
146
|
function serializeEntry(entry) {
|
|
57
147
|
const serialized = {
|
|
@@ -71,10 +161,20 @@ function describeError(error) {
|
|
|
71
161
|
return error instanceof Error ? error.message : String(error);
|
|
72
162
|
}
|
|
73
163
|
|
|
164
|
+
// src/fail.ts
|
|
165
|
+
var DEFAULT_PREFIX = "[error-log-adapter]";
|
|
166
|
+
function reportFailure(report) {
|
|
167
|
+
console.error(`${report.prefix} ${report.adapterName}:`, report.error);
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
error: `${report.adapterName} failed ${report.action}: ${report.reason ?? describeError(report.error)}`
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
74
174
|
// src/adapters/console-error-adapter.ts
|
|
75
175
|
var ConsoleErrorAdapter = class {
|
|
76
176
|
constructor(options = {}) {
|
|
77
|
-
this.prefix = options.prefix ??
|
|
177
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
78
178
|
}
|
|
79
179
|
async log(entry) {
|
|
80
180
|
try {
|
|
@@ -89,14 +189,135 @@ var ConsoleErrorAdapter = class {
|
|
|
89
189
|
}
|
|
90
190
|
};
|
|
91
191
|
|
|
192
|
+
// src/uuid.ts
|
|
193
|
+
function generateId() {
|
|
194
|
+
const cryptoApi = globalThis.crypto;
|
|
195
|
+
if (cryptoApi !== void 0 && typeof cryptoApi.randomUUID === "function") {
|
|
196
|
+
return cryptoApi.randomUUID();
|
|
197
|
+
}
|
|
198
|
+
const bytes = new Uint8Array(16);
|
|
199
|
+
if (cryptoApi !== void 0 && typeof cryptoApi.getRandomValues === "function") {
|
|
200
|
+
cryptoApi.getRandomValues(bytes);
|
|
201
|
+
} else {
|
|
202
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
203
|
+
bytes[index] = Math.floor(Math.random() * 256);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
207
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
208
|
+
const hex = Array.from(
|
|
209
|
+
bytes,
|
|
210
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
211
|
+
).join("");
|
|
212
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/entry-id.ts
|
|
216
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
217
|
+
function hasCallerId(entry) {
|
|
218
|
+
return entry.id !== void 0;
|
|
219
|
+
}
|
|
220
|
+
function resolveEntryId(entry) {
|
|
221
|
+
if (entry.id === void 0) {
|
|
222
|
+
return generateId();
|
|
223
|
+
}
|
|
224
|
+
if (typeof entry.id !== "string" || !UUID_PATTERN.test(entry.id)) {
|
|
225
|
+
throw new RangeError(
|
|
226
|
+
`entry.id must be a UUID such as one from crypto.randomUUID(), received ${JSON.stringify(entry.id)}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return entry.id.toLowerCase();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/timeout.ts
|
|
233
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
234
|
+
var SinkTimeoutError = class extends Error {
|
|
235
|
+
constructor(message) {
|
|
236
|
+
super(message);
|
|
237
|
+
this.name = "SinkTimeoutError";
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
function resolveTimeoutMs(value, adapterName) {
|
|
241
|
+
const timeoutMs = value ?? DEFAULT_TIMEOUT_MS;
|
|
242
|
+
if (Number.isNaN(timeoutMs) || timeoutMs <= 0) {
|
|
243
|
+
throw new RangeError(
|
|
244
|
+
`${adapterName}: timeoutMs must be a positive number or Infinity, received ${String(value)}.`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return timeoutMs;
|
|
248
|
+
}
|
|
249
|
+
function timeoutMessage(sink, timeoutMs, action = "respond") {
|
|
250
|
+
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)`;
|
|
251
|
+
}
|
|
252
|
+
function withTimeout(start, timeoutMs, message) {
|
|
253
|
+
if (timeoutMs === Infinity) {
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
try {
|
|
256
|
+
resolve(start());
|
|
257
|
+
} catch (error) {
|
|
258
|
+
reject(error);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return new Promise(
|
|
263
|
+
(resolve, reject) => {
|
|
264
|
+
const timer = setTimeout(() => {
|
|
265
|
+
reject(new SinkTimeoutError(message));
|
|
266
|
+
}, timeoutMs);
|
|
267
|
+
const settle = (done) => (...args) => {
|
|
268
|
+
clearTimeout(timer);
|
|
269
|
+
done(...args);
|
|
270
|
+
};
|
|
271
|
+
try {
|
|
272
|
+
Promise.resolve(start()).then(settle(resolve), settle(reject));
|
|
273
|
+
} catch (error) {
|
|
274
|
+
settle(reject)(error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
92
280
|
// src/adapters/webhook-error-adapter.ts
|
|
281
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
282
|
+
"localhost",
|
|
283
|
+
"127.0.0.1",
|
|
284
|
+
"[::1]"
|
|
285
|
+
]);
|
|
286
|
+
function parseWebhookUrl(url, allowInsecure) {
|
|
287
|
+
let parsed;
|
|
288
|
+
try {
|
|
289
|
+
parsed = new URL(url);
|
|
290
|
+
} catch {
|
|
291
|
+
throw new RangeError(
|
|
292
|
+
"WebhookErrorAdapter: the webhook URL is not a valid absolute URL (for example https://example.com/errors)."
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if (parsed.protocol === "https:") {
|
|
296
|
+
return parsed;
|
|
297
|
+
}
|
|
298
|
+
if (parsed.protocol === "http:" && (allowInsecure || LOOPBACK_HOSTS.has(parsed.hostname))) {
|
|
299
|
+
return parsed;
|
|
300
|
+
}
|
|
301
|
+
throw new RangeError(
|
|
302
|
+
`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.`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
93
305
|
var WebhookErrorAdapter = class {
|
|
306
|
+
/** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or `timeoutMs` is invalid. */
|
|
94
307
|
constructor(url, options = {}) {
|
|
95
308
|
this.url = url;
|
|
96
309
|
this.options = options;
|
|
97
|
-
this.prefix = options.prefix ??
|
|
310
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
311
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "WebhookErrorAdapter");
|
|
312
|
+
this.target = parseWebhookUrl(url, options.allowInsecure ?? false).origin;
|
|
98
313
|
}
|
|
99
314
|
async log(entry) {
|
|
315
|
+
const controller = new AbortController();
|
|
316
|
+
let timedOut = false;
|
|
317
|
+
const timer = this.timeoutMs === Infinity ? void 0 : setTimeout(() => {
|
|
318
|
+
timedOut = true;
|
|
319
|
+
controller.abort();
|
|
320
|
+
}, this.timeoutMs);
|
|
100
321
|
try {
|
|
101
322
|
const response = await fetch(this.url, {
|
|
102
323
|
method: "POST",
|
|
@@ -104,58 +325,112 @@ var WebhookErrorAdapter = class {
|
|
|
104
325
|
"Content-Type": "application/json",
|
|
105
326
|
...this.options.headers
|
|
106
327
|
},
|
|
107
|
-
|
|
328
|
+
// A caller-supplied id goes along so the receiver can drop a repeat.
|
|
329
|
+
body: JSON.stringify(
|
|
330
|
+
hasCallerId(entry) ? { id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry)
|
|
331
|
+
),
|
|
332
|
+
// A redirect would turn the POST into a GET without a body (302, 303)
|
|
333
|
+
// or re-send the entry and headers to another host (307, 308).
|
|
334
|
+
redirect: "manual",
|
|
335
|
+
signal: controller.signal
|
|
108
336
|
});
|
|
337
|
+
if (response.type === "opaqueredirect") {
|
|
338
|
+
return this.fail(this.redirectMessage("a redirect"));
|
|
339
|
+
}
|
|
109
340
|
if (!response.ok) {
|
|
341
|
+
if (response.status >= 300 && response.status < 400) {
|
|
342
|
+
return this.fail(this.redirectMessage(`HTTP ${response.status}`));
|
|
343
|
+
}
|
|
110
344
|
return this.fail(
|
|
111
|
-
`webhook ${this.
|
|
345
|
+
`webhook ${this.target} responded with HTTP ${response.status}`
|
|
112
346
|
);
|
|
113
347
|
}
|
|
114
348
|
return { ok: true };
|
|
115
349
|
} catch (error) {
|
|
116
350
|
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
351
|
+
if (timedOut) {
|
|
352
|
+
return this.fail(
|
|
353
|
+
timeoutMessage(`webhook ${this.target}`, this.timeoutMs)
|
|
354
|
+
);
|
|
355
|
+
}
|
|
117
356
|
return this.fail(
|
|
118
|
-
`could not reach webhook ${this.
|
|
357
|
+
`could not reach webhook ${this.target}: ${this.redact(describeError(error))}`
|
|
119
358
|
);
|
|
359
|
+
} finally {
|
|
360
|
+
clearTimeout(timer);
|
|
120
361
|
}
|
|
121
362
|
}
|
|
363
|
+
redirectMessage(what) {
|
|
364
|
+
return `webhook ${this.target} responded with ${what}, which is not followed because the entry would not arrive intact; use the final URL instead`;
|
|
365
|
+
}
|
|
366
|
+
/** Replaces the full URL in a message with its origin, in case the runtime put it there. */
|
|
367
|
+
redact(message) {
|
|
368
|
+
return message.split(this.url).join(this.target);
|
|
369
|
+
}
|
|
122
370
|
fail(reason) {
|
|
123
371
|
return { ok: false, error: `WebhookErrorAdapter failed: ${reason}` };
|
|
124
372
|
}
|
|
125
373
|
};
|
|
126
374
|
|
|
127
375
|
// src/adapters/mongodb-error-adapter.ts
|
|
376
|
+
function isDuplicateKeyError(error) {
|
|
377
|
+
return typeof error === "object" && error !== null && error.code === 11e3;
|
|
378
|
+
}
|
|
128
379
|
var MongoDbErrorAdapter = class {
|
|
380
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
129
381
|
constructor(collection, options = {}) {
|
|
130
382
|
this.collection = collection;
|
|
131
|
-
this.prefix = options.prefix ??
|
|
383
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
384
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "MongoDbErrorAdapter");
|
|
132
385
|
}
|
|
133
386
|
async log(entry) {
|
|
134
387
|
try {
|
|
135
|
-
|
|
388
|
+
const document = hasCallerId(entry) ? { _id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry);
|
|
389
|
+
await withTimeout(
|
|
390
|
+
() => this.collection.insertOne(document),
|
|
391
|
+
this.timeoutMs,
|
|
392
|
+
timeoutMessage("MongoDB", this.timeoutMs)
|
|
393
|
+
);
|
|
136
394
|
return { ok: true };
|
|
137
395
|
} catch (error) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
396
|
+
if (hasCallerId(entry) && isDuplicateKeyError(error)) {
|
|
397
|
+
return { ok: true };
|
|
398
|
+
}
|
|
399
|
+
return reportFailure({
|
|
400
|
+
adapterName: "MongoDbErrorAdapter",
|
|
401
|
+
prefix: this.prefix,
|
|
402
|
+
action: "to insert entry",
|
|
403
|
+
error
|
|
404
|
+
});
|
|
143
405
|
}
|
|
144
406
|
}
|
|
145
407
|
};
|
|
146
408
|
|
|
147
409
|
// src/adapters/composite-error-adapter.ts
|
|
410
|
+
var DEFAULT_COMPOSITE_TIMEOUT_MS = DEFAULT_TIMEOUT_MS * 3;
|
|
148
411
|
var CompositeErrorAdapter = class {
|
|
149
|
-
|
|
412
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
413
|
+
constructor(adapters, options = {}) {
|
|
150
414
|
this.adapters = adapters;
|
|
415
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
416
|
+
options.timeoutMs ?? DEFAULT_COMPOSITE_TIMEOUT_MS,
|
|
417
|
+
"CompositeErrorAdapter"
|
|
418
|
+
);
|
|
151
419
|
}
|
|
152
420
|
async log(entry) {
|
|
153
421
|
const results = await Promise.all(
|
|
154
422
|
this.adapters.map(
|
|
155
|
-
async (adapter) => {
|
|
423
|
+
async (adapter, index) => {
|
|
156
424
|
try {
|
|
157
|
-
return await
|
|
425
|
+
return await withTimeout(
|
|
426
|
+
() => adapter.log(entry),
|
|
427
|
+
this.timeoutMs,
|
|
428
|
+
`adapter ${index} (${adapter.constructor.name}) did not return within ${this.timeoutMs} ms, so the entry may or may not have been stored there`
|
|
429
|
+
);
|
|
158
430
|
} catch (error) {
|
|
431
|
+
if (error instanceof SinkTimeoutError) {
|
|
432
|
+
return { ok: false, error: error.message };
|
|
433
|
+
}
|
|
159
434
|
return {
|
|
160
435
|
ok: false,
|
|
161
436
|
error: `adapter threw instead of returning a result: ${String(error)}`
|
|
@@ -186,58 +461,101 @@ function createErrorLogStore(database, storeName = DEFAULT_STORE_NAME) {
|
|
|
186
461
|
database.createObjectStore(storeName, { keyPath: "id" }).createIndex("timestamp", "timestamp");
|
|
187
462
|
}
|
|
188
463
|
var IndexedDbErrorAdapter = class {
|
|
464
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
189
465
|
constructor(database, options = {}) {
|
|
190
466
|
this.database = database;
|
|
191
467
|
this.storeName = options.storeName ?? DEFAULT_STORE_NAME;
|
|
192
|
-
this.prefix = options.prefix ??
|
|
468
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
469
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
470
|
+
options.timeoutMs,
|
|
471
|
+
"IndexedDbErrorAdapter"
|
|
472
|
+
);
|
|
193
473
|
}
|
|
194
474
|
async log(entry) {
|
|
195
475
|
try {
|
|
196
476
|
const record = {
|
|
197
|
-
id:
|
|
477
|
+
id: resolveEntryId(entry),
|
|
198
478
|
...serializeEntry(entry)
|
|
199
479
|
};
|
|
200
|
-
await this.write(record);
|
|
480
|
+
await this.write(record, hasCallerId(entry));
|
|
201
481
|
return { ok: true };
|
|
202
482
|
} catch (error) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
483
|
+
return reportFailure({
|
|
484
|
+
adapterName: "IndexedDbErrorAdapter",
|
|
485
|
+
prefix: this.prefix,
|
|
486
|
+
action: `to write to object store "${this.storeName}"`,
|
|
487
|
+
error
|
|
488
|
+
});
|
|
208
489
|
}
|
|
209
490
|
}
|
|
210
|
-
/**
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
491
|
+
/**
|
|
492
|
+
* Resolves once the transaction commits; rejects if the request fails, the
|
|
493
|
+
* transaction aborts, or it does not finish within `timeoutMs`.
|
|
494
|
+
*
|
|
495
|
+
* Error events are handled here (`preventDefault` and `stopPropagation`), so a
|
|
496
|
+
* failed log write does not reach the `onerror` handler an app may have set on
|
|
497
|
+
* its own database object.
|
|
498
|
+
*/
|
|
499
|
+
write(record, replaceExisting) {
|
|
500
|
+
return withTimeout(
|
|
501
|
+
() => new Promise(
|
|
502
|
+
(resolve, reject) => {
|
|
503
|
+
const transaction = this.database.transaction(
|
|
504
|
+
this.storeName,
|
|
505
|
+
"readwrite"
|
|
506
|
+
);
|
|
507
|
+
const fail = (what, source) => (event) => {
|
|
508
|
+
event?.preventDefault?.();
|
|
509
|
+
event?.stopPropagation?.();
|
|
510
|
+
reject(new Error(`${what}: ${describeDomError(source.error)}`));
|
|
511
|
+
};
|
|
512
|
+
transaction.oncomplete = () => resolve();
|
|
513
|
+
transaction.onerror = fail("transaction failed", transaction);
|
|
514
|
+
transaction.onabort = fail("transaction aborted", transaction);
|
|
515
|
+
const store = transaction.objectStore(
|
|
516
|
+
this.storeName
|
|
517
|
+
);
|
|
518
|
+
const request = replaceExisting && typeof store.put === "function" ? store.put(record) : store.add(record);
|
|
519
|
+
if (typeof request === "object" && request !== null) {
|
|
520
|
+
request.onerror = fail(
|
|
521
|
+
"request failed",
|
|
522
|
+
request
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
),
|
|
527
|
+
this.timeoutMs,
|
|
528
|
+
timeoutMessage("IndexedDB", this.timeoutMs, "finish the transaction")
|
|
228
529
|
);
|
|
229
530
|
}
|
|
230
531
|
};
|
|
532
|
+
function describeDomError(error) {
|
|
533
|
+
if (error === null || error === void 0) {
|
|
534
|
+
return "no details provided";
|
|
535
|
+
}
|
|
536
|
+
const text = [error.name, error.message].filter((part) => typeof part === "string" && part !== "").join(": ");
|
|
537
|
+
return text === "" ? "no details provided" : text;
|
|
538
|
+
}
|
|
231
539
|
|
|
232
|
-
// src/
|
|
540
|
+
// src/sql-ident.ts
|
|
233
541
|
var DEFAULT_TABLE_NAME = "error_log";
|
|
234
542
|
var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
235
|
-
function
|
|
236
|
-
if (!IDENTIFIER_PATTERN.test(
|
|
543
|
+
function assertIdentifier(value, label, owner, maxLength) {
|
|
544
|
+
if (!IDENTIFIER_PATTERN.test(value)) {
|
|
237
545
|
throw new RangeError(
|
|
238
|
-
|
|
546
|
+
`${owner}: invalid ${label} "${value}". Use only letters, digits and underscores, and do not start with a digit.`
|
|
239
547
|
);
|
|
240
548
|
}
|
|
549
|
+
if (maxLength !== void 0 && value.length > maxLength) {
|
|
550
|
+
throw new RangeError(
|
|
551
|
+
`${owner}: ${label} "${value}" is ${value.length} characters long, but the limit is ${maxLength}.`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/adapters/sqlite-error-adapter.ts
|
|
557
|
+
function assertValidTableName(tableName) {
|
|
558
|
+
assertIdentifier(tableName, "table name", "SqliteErrorAdapter");
|
|
241
559
|
}
|
|
242
560
|
function buildCreateErrorLogTableSql(tableName = DEFAULT_TABLE_NAME) {
|
|
243
561
|
assertValidTableName(tableName);
|
|
@@ -247,23 +565,24 @@ function buildCreateErrorLogTableSql(tableName = DEFAULT_TABLE_NAME) {
|
|
|
247
565
|
];
|
|
248
566
|
}
|
|
249
567
|
var SqliteErrorAdapter = class {
|
|
250
|
-
/** @throws {RangeError} if `options.tableName` is not a plain identifier. */
|
|
568
|
+
/** @throws {RangeError} if `options.tableName` is not a plain identifier or `options.timeoutMs` is invalid. */
|
|
251
569
|
constructor(client, options = {}) {
|
|
252
570
|
this.client = client;
|
|
253
571
|
this.tableReady = null;
|
|
254
572
|
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
255
573
|
assertValidTableName(this.tableName);
|
|
256
574
|
this.createTable = options.createTable ?? true;
|
|
257
|
-
this.prefix = options.prefix ??
|
|
575
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
576
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "SqliteErrorAdapter");
|
|
258
577
|
}
|
|
259
578
|
async log(entry) {
|
|
260
579
|
try {
|
|
261
580
|
await this.ensureTable();
|
|
262
581
|
const serialized = serializeEntry(entry);
|
|
263
|
-
await this.
|
|
264
|
-
`INSERT INTO ${this.tableName} (id, code, message, cause, source, occurred_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
582
|
+
await this.run(
|
|
583
|
+
`INSERT INTO ${this.tableName} (id, code, message, cause, source, occurred_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO NOTHING`,
|
|
265
584
|
[
|
|
266
|
-
|
|
585
|
+
resolveEntryId(entry),
|
|
267
586
|
serialized.code,
|
|
268
587
|
serialized.message,
|
|
269
588
|
serialized.cause === void 0 ? null : JSON.stringify(serialized.cause),
|
|
@@ -273,11 +592,12 @@ var SqliteErrorAdapter = class {
|
|
|
273
592
|
);
|
|
274
593
|
return { ok: true };
|
|
275
594
|
} catch (error) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
595
|
+
return reportFailure({
|
|
596
|
+
adapterName: "SqliteErrorAdapter",
|
|
597
|
+
prefix: this.prefix,
|
|
598
|
+
action: `to write to table "${this.tableName}"`,
|
|
599
|
+
error
|
|
600
|
+
});
|
|
281
601
|
}
|
|
282
602
|
}
|
|
283
603
|
/** Creates the table once. A failed attempt is retried on the next log call. */
|
|
@@ -297,9 +617,17 @@ var SqliteErrorAdapter = class {
|
|
|
297
617
|
}
|
|
298
618
|
async runCreateStatements() {
|
|
299
619
|
for (const sql of buildCreateErrorLogTableSql(this.tableName)) {
|
|
300
|
-
await this.
|
|
620
|
+
await this.run(sql, []);
|
|
301
621
|
}
|
|
302
622
|
}
|
|
623
|
+
/** Runs one statement, giving up waiting after `timeoutMs`. */
|
|
624
|
+
run(sql, params) {
|
|
625
|
+
return withTimeout(
|
|
626
|
+
() => this.client.run(sql, params),
|
|
627
|
+
this.timeoutMs,
|
|
628
|
+
timeoutMessage("SQLite", this.timeoutMs)
|
|
629
|
+
);
|
|
630
|
+
}
|
|
303
631
|
};
|
|
304
632
|
|
|
305
633
|
// src/adapters/web-storage-error-adapter.ts
|
|
@@ -318,7 +646,8 @@ var WebStorageErrorAdapter = class {
|
|
|
318
646
|
this.storage = storage;
|
|
319
647
|
this.key = options.key ?? DEFAULT_STORAGE_KEY;
|
|
320
648
|
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
321
|
-
this.prefix = options.prefix ??
|
|
649
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
650
|
+
this.locks = options.locks === void 0 ? defaultLockManager() : options.locks;
|
|
322
651
|
if (!Number.isInteger(this.maxEntries) || this.maxEntries < 1) {
|
|
323
652
|
throw new RangeError(
|
|
324
653
|
`WebStorageErrorAdapter: maxEntries must be a positive integer, received ${String(options.maxEntries)}.`
|
|
@@ -328,23 +657,49 @@ var WebStorageErrorAdapter = class {
|
|
|
328
657
|
async log(entry) {
|
|
329
658
|
try {
|
|
330
659
|
const record = {
|
|
331
|
-
id:
|
|
660
|
+
id: resolveEntryId(entry),
|
|
332
661
|
...serializeEntry(entry)
|
|
333
662
|
};
|
|
334
|
-
this.
|
|
663
|
+
await this.appendLocked(record);
|
|
335
664
|
return { ok: true };
|
|
336
665
|
} catch (error) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
666
|
+
return reportFailure({
|
|
667
|
+
adapterName: "WebStorageErrorAdapter",
|
|
668
|
+
prefix: this.prefix,
|
|
669
|
+
action: `to write to storage key "${this.key}"`,
|
|
670
|
+
error
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Appends under a cross-tab lock when one is available. If the lock itself
|
|
676
|
+
* cannot be taken (for example a sandboxed frame), the entry is still written
|
|
677
|
+
* without it rather than lost.
|
|
678
|
+
*/
|
|
679
|
+
async appendLocked(record) {
|
|
680
|
+
if (this.locks === null) {
|
|
681
|
+
this.append(record);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
let appended = false;
|
|
685
|
+
try {
|
|
686
|
+
await this.locks.request(`error-log-adapter:${this.key}`, () => {
|
|
687
|
+
appended = true;
|
|
688
|
+
this.append(record);
|
|
689
|
+
});
|
|
690
|
+
} catch (error) {
|
|
691
|
+
if (appended) {
|
|
692
|
+
throw error;
|
|
693
|
+
}
|
|
694
|
+
this.append(record);
|
|
342
695
|
}
|
|
343
696
|
}
|
|
344
|
-
/** Reads, appends, trims, and writes back. Runs without awaiting, so calls never interleave. */
|
|
697
|
+
/** Reads, appends, trims, and writes back. Runs without awaiting, so calls in one tab never interleave. */
|
|
345
698
|
append(record) {
|
|
346
699
|
const records = [
|
|
347
|
-
...this.readRecords()
|
|
700
|
+
...this.readRecords().filter(
|
|
701
|
+
(existing) => existing?.id !== record.id
|
|
702
|
+
),
|
|
348
703
|
record
|
|
349
704
|
].slice(-this.maxEntries);
|
|
350
705
|
for (; ; ) {
|
|
@@ -370,16 +725,51 @@ var WebStorageErrorAdapter = class {
|
|
|
370
725
|
if (Array.isArray(parsed)) {
|
|
371
726
|
return parsed;
|
|
372
727
|
}
|
|
373
|
-
|
|
374
|
-
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" did not hold an array, so it was replaced.`
|
|
375
|
-
);
|
|
728
|
+
this.setAside(stored, "did not hold an array");
|
|
376
729
|
} catch (error) {
|
|
377
|
-
|
|
378
|
-
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" held invalid JSON (${describeError(error)}), so it was replaced.`
|
|
379
|
-
);
|
|
730
|
+
this.setAside(stored, `held invalid JSON (${describeError(error)})`);
|
|
380
731
|
}
|
|
381
732
|
return [];
|
|
382
733
|
}
|
|
734
|
+
/**
|
|
735
|
+
* Copies an unusable value to `<key>.corrupt` before it is replaced, so data
|
|
736
|
+
* the host app stored under the same key is not silently destroyed. Only the
|
|
737
|
+
* first one is kept; a later unusable value would not overwrite it.
|
|
738
|
+
*/
|
|
739
|
+
setAside(stored, reason) {
|
|
740
|
+
const backupKey = `${this.key}${CORRUPT_KEY_SUFFIX}`;
|
|
741
|
+
let savedTo = "but it could not be copied aside";
|
|
742
|
+
try {
|
|
743
|
+
if (this.storage.getItem(backupKey) === null) {
|
|
744
|
+
this.storage.setItem(backupKey, stored);
|
|
745
|
+
savedTo = `and a copy was kept under "${backupKey}"`;
|
|
746
|
+
} else {
|
|
747
|
+
savedTo = `and "${backupKey}" already holds an earlier copy`;
|
|
748
|
+
}
|
|
749
|
+
} catch {
|
|
750
|
+
}
|
|
751
|
+
console.error(
|
|
752
|
+
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" ${reason}, so it was replaced, ${savedTo}.`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
var CORRUPT_KEY_SUFFIX = ".corrupt";
|
|
757
|
+
function defaultLockManager() {
|
|
758
|
+
const locks = globalThis.navigator?.locks;
|
|
759
|
+
return typeof locks?.request === "function" ? locks : null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/queue.ts
|
|
763
|
+
var SerialQueue = class {
|
|
764
|
+
constructor() {
|
|
765
|
+
this.tail = Promise.resolve();
|
|
766
|
+
}
|
|
767
|
+
/** Starts `task` once every earlier task has finished, and resolves with its result. */
|
|
768
|
+
run(task) {
|
|
769
|
+
const result = this.tail.then(task);
|
|
770
|
+
this.tail = result.catch(() => void 0);
|
|
771
|
+
return result;
|
|
772
|
+
}
|
|
383
773
|
};
|
|
384
774
|
|
|
385
775
|
// src/file-system.ts
|
|
@@ -387,6 +777,7 @@ async function loadNodeFileSystem() {
|
|
|
387
777
|
const moduleName = "node:fs/promises";
|
|
388
778
|
return await import(moduleName);
|
|
389
779
|
}
|
|
780
|
+
var ROOT_PATTERN = /^(?:[A-Za-z]:)?[\\/]+$/;
|
|
390
781
|
function parentDirectory(filePath) {
|
|
391
782
|
const lastSeparator = Math.max(
|
|
392
783
|
filePath.lastIndexOf("/"),
|
|
@@ -395,7 +786,11 @@ function parentDirectory(filePath) {
|
|
|
395
786
|
if (lastSeparator < 0) {
|
|
396
787
|
return null;
|
|
397
788
|
}
|
|
398
|
-
|
|
789
|
+
const directory = filePath.slice(0, lastSeparator + 1);
|
|
790
|
+
if (ROOT_PATTERN.test(directory)) {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
return filePath.slice(0, lastSeparator);
|
|
399
794
|
}
|
|
400
795
|
function isMissingFileError(error) {
|
|
401
796
|
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
@@ -441,24 +836,20 @@ var FlatFileErrorAdapter = class {
|
|
|
441
836
|
constructor(filePath, options = {}) {
|
|
442
837
|
this.filePath = filePath;
|
|
443
838
|
this.directoryReady = false;
|
|
444
|
-
this.queue =
|
|
839
|
+
this.queue = new SerialQueue();
|
|
445
840
|
this.fs = options.fs;
|
|
446
841
|
this.formatLine = options.formatLine ?? formatErrorLogLine;
|
|
447
842
|
this.maxBytes = options.maxBytes;
|
|
448
843
|
this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
449
844
|
this.createDirectory = options.createDirectory ?? true;
|
|
450
|
-
this.prefix = options.prefix ??
|
|
845
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
451
846
|
if (this.maxBytes !== void 0) {
|
|
452
847
|
assertPositiveInteger(this.maxBytes, "maxBytes");
|
|
453
848
|
}
|
|
454
849
|
assertPositiveInteger(this.maxFiles, "maxFiles");
|
|
455
850
|
}
|
|
456
851
|
log(entry) {
|
|
457
|
-
|
|
458
|
-
() => this.write(entry)
|
|
459
|
-
);
|
|
460
|
-
this.queue = result.catch(() => void 0);
|
|
461
|
-
return result;
|
|
852
|
+
return this.queue.run(() => this.write(entry));
|
|
462
853
|
}
|
|
463
854
|
async write(entry) {
|
|
464
855
|
try {
|
|
@@ -471,11 +862,12 @@ var FlatFileErrorAdapter = class {
|
|
|
471
862
|
await fs.appendFile(this.filePath, line);
|
|
472
863
|
return { ok: true };
|
|
473
864
|
} catch (error) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
865
|
+
return reportFailure({
|
|
866
|
+
adapterName: "FlatFileErrorAdapter",
|
|
867
|
+
prefix: this.prefix,
|
|
868
|
+
action: `to append to "${this.filePath}"`,
|
|
869
|
+
error
|
|
870
|
+
});
|
|
479
871
|
}
|
|
480
872
|
}
|
|
481
873
|
/** Uses the injected file system, or loads `node:fs/promises` on first use. */
|
|
@@ -550,13 +942,13 @@ var JsonFileErrorAdapter = class {
|
|
|
550
942
|
constructor(filePath, options = {}) {
|
|
551
943
|
this.filePath = filePath;
|
|
552
944
|
this.directoryReady = false;
|
|
553
|
-
this.queue =
|
|
945
|
+
this.queue = new SerialQueue();
|
|
554
946
|
this.mode = options.mode ?? "ndjson";
|
|
555
947
|
this.maxEntries = options.maxEntries;
|
|
556
948
|
this.indent = options.indent ?? DEFAULT_INDENT;
|
|
557
949
|
this.fs = options.fs;
|
|
558
950
|
this.createDirectory = options.createDirectory ?? true;
|
|
559
|
-
this.prefix = options.prefix ??
|
|
951
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
560
952
|
if (this.maxEntries !== void 0) {
|
|
561
953
|
if (this.mode !== "array") {
|
|
562
954
|
throw new RangeError(
|
|
@@ -576,16 +968,12 @@ var JsonFileErrorAdapter = class {
|
|
|
576
968
|
}
|
|
577
969
|
}
|
|
578
970
|
log(entry) {
|
|
579
|
-
|
|
580
|
-
() => this.write(entry)
|
|
581
|
-
);
|
|
582
|
-
this.queue = result.catch(() => void 0);
|
|
583
|
-
return result;
|
|
971
|
+
return this.queue.run(() => this.write(entry));
|
|
584
972
|
}
|
|
585
973
|
async write(entry) {
|
|
586
974
|
try {
|
|
587
975
|
const record = {
|
|
588
|
-
id:
|
|
976
|
+
id: resolveEntryId(entry),
|
|
589
977
|
...serializeEntry(entry)
|
|
590
978
|
};
|
|
591
979
|
const fs = await this.getFileSystem();
|
|
@@ -598,11 +986,12 @@ var JsonFileErrorAdapter = class {
|
|
|
598
986
|
}
|
|
599
987
|
return { ok: true };
|
|
600
988
|
} catch (error) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
989
|
+
return reportFailure({
|
|
990
|
+
adapterName: "JsonFileErrorAdapter",
|
|
991
|
+
prefix: this.prefix,
|
|
992
|
+
action: `to write "${this.filePath}" in ${this.mode} mode`,
|
|
993
|
+
error
|
|
994
|
+
});
|
|
606
995
|
}
|
|
607
996
|
}
|
|
608
997
|
async getFileSystem() {
|
|
@@ -628,7 +1017,9 @@ var JsonFileErrorAdapter = class {
|
|
|
628
1017
|
*/
|
|
629
1018
|
async rewriteArray(fs, record) {
|
|
630
1019
|
const records = [
|
|
631
|
-
...await this.readRecords(fs)
|
|
1020
|
+
...(await this.readRecords(fs)).filter(
|
|
1021
|
+
(existing) => existing?.id !== record.id
|
|
1022
|
+
),
|
|
632
1023
|
record
|
|
633
1024
|
];
|
|
634
1025
|
const kept = this.maxEntries === void 0 ? records : records.slice(-this.maxEntries);
|
|
@@ -678,25 +1069,14 @@ var JsonFileErrorAdapter = class {
|
|
|
678
1069
|
};
|
|
679
1070
|
|
|
680
1071
|
// src/adapters/sql-error-adapter.ts
|
|
681
|
-
var DEFAULT_TABLE_NAME2 = "error_log";
|
|
682
1072
|
var MAX_TABLE_NAME_LENGTH = 47;
|
|
683
|
-
var IDENTIFIER_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
684
1073
|
function assertValidTableName2(tableName, adapterName) {
|
|
685
|
-
|
|
686
|
-
throw new RangeError(
|
|
687
|
-
`${adapterName}: invalid table name "${tableName}". Use only letters, digits and underscores, and do not start with a digit.`
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
if (tableName.length > MAX_TABLE_NAME_LENGTH) {
|
|
691
|
-
throw new RangeError(
|
|
692
|
-
`${adapterName}: table name "${tableName}" is ${tableName.length} characters long, but the limit is ${MAX_TABLE_NAME_LENGTH}.`
|
|
693
|
-
);
|
|
694
|
-
}
|
|
1074
|
+
assertIdentifier(tableName, "table name", adapterName, MAX_TABLE_NAME_LENGTH);
|
|
695
1075
|
}
|
|
696
1076
|
function quoteIdentifier(dialect, identifier) {
|
|
697
1077
|
return dialect === "postgres" ? `"${identifier}"` : `\`${identifier}\``;
|
|
698
1078
|
}
|
|
699
|
-
function buildErrorLogTableDdl(dialect, tableName =
|
|
1079
|
+
function buildErrorLogTableDdl(dialect, tableName = DEFAULT_TABLE_NAME) {
|
|
700
1080
|
assertValidTableName2(tableName, "buildErrorLogTableDdl");
|
|
701
1081
|
const table = quoteIdentifier(dialect, tableName);
|
|
702
1082
|
const index = quoteIdentifier(
|
|
@@ -710,7 +1090,13 @@ function buildErrorLogTableDdl(dialect, tableName = DEFAULT_TABLE_NAME2) {
|
|
|
710
1090
|
];
|
|
711
1091
|
}
|
|
712
1092
|
return [
|
|
713
|
-
`CREATE TABLE IF NOT EXISTS ${table} (id CHAR(36) NOT NULL PRIMARY KEY, code
|
|
1093
|
+
`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))`
|
|
1094
|
+
];
|
|
1095
|
+
}
|
|
1096
|
+
function buildMySqlWidenColumnsSql(tableName = DEFAULT_TABLE_NAME) {
|
|
1097
|
+
assertValidTableName2(tableName, "buildMySqlWidenColumnsSql");
|
|
1098
|
+
return [
|
|
1099
|
+
`ALTER TABLE ${quoteIdentifier("mysql", tableName)} MODIFY code TEXT NOT NULL, MODIFY source TEXT NULL`
|
|
714
1100
|
];
|
|
715
1101
|
}
|
|
716
1102
|
function toMysqlDateTime(date) {
|
|
@@ -722,10 +1108,11 @@ var SqlErrorAdapter = class {
|
|
|
722
1108
|
this.dialect = dialect;
|
|
723
1109
|
this.adapterName = adapterName;
|
|
724
1110
|
this.tableReady = null;
|
|
725
|
-
this.tableName = options.tableName ??
|
|
1111
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
726
1112
|
assertValidTableName2(this.tableName, adapterName);
|
|
727
1113
|
this.createTable = options.createTable ?? true;
|
|
728
|
-
this.prefix = options.prefix ??
|
|
1114
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1115
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, adapterName);
|
|
729
1116
|
}
|
|
730
1117
|
async log(entry) {
|
|
731
1118
|
try {
|
|
@@ -737,7 +1124,7 @@ var SqlErrorAdapter = class {
|
|
|
737
1124
|
);
|
|
738
1125
|
}
|
|
739
1126
|
const params = [
|
|
740
|
-
|
|
1127
|
+
resolveEntryId(entry),
|
|
741
1128
|
serialized.code,
|
|
742
1129
|
serialized.message,
|
|
743
1130
|
serialized.cause === void 0 ? null : JSON.stringify(serialized.cause),
|
|
@@ -745,19 +1132,21 @@ var SqlErrorAdapter = class {
|
|
|
745
1132
|
this.dialect === "postgres" ? occurredAt.toISOString() : toMysqlDateTime(occurredAt)
|
|
746
1133
|
];
|
|
747
1134
|
await this.ensureTable();
|
|
748
|
-
await this.
|
|
1135
|
+
await this.query(this.insertSql(), params);
|
|
749
1136
|
return { ok: true };
|
|
750
1137
|
} catch (error) {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1138
|
+
return reportFailure({
|
|
1139
|
+
adapterName: this.adapterName,
|
|
1140
|
+
prefix: this.prefix,
|
|
1141
|
+
action: `to write to table "${this.tableName}"`,
|
|
1142
|
+
error
|
|
1143
|
+
});
|
|
756
1144
|
}
|
|
757
1145
|
}
|
|
758
1146
|
insertSql() {
|
|
759
1147
|
const placeholders = this.dialect === "postgres" ? "$1, $2, $3, $4, $5, $6" : "?, ?, ?, ?, ?, ?";
|
|
760
|
-
|
|
1148
|
+
const onRepeat = this.dialect === "postgres" ? "ON CONFLICT (id) DO NOTHING" : "ON DUPLICATE KEY UPDATE id = id";
|
|
1149
|
+
return `INSERT INTO ${quoteIdentifier(this.dialect, this.tableName)} (id, code, message, cause, source, occurred_at) VALUES (${placeholders}) ${onRepeat}`;
|
|
761
1150
|
}
|
|
762
1151
|
/** Creates the table once. A failed attempt is retried on the next log call. */
|
|
763
1152
|
ensureTable() {
|
|
@@ -776,9 +1165,20 @@ var SqlErrorAdapter = class {
|
|
|
776
1165
|
}
|
|
777
1166
|
async runCreateStatements() {
|
|
778
1167
|
for (const sql of buildErrorLogTableDdl(this.dialect, this.tableName)) {
|
|
779
|
-
await this.
|
|
1168
|
+
await this.query(sql, []);
|
|
780
1169
|
}
|
|
781
1170
|
}
|
|
1171
|
+
/** Runs one statement, giving up waiting after `timeoutMs`. */
|
|
1172
|
+
query(sql, params) {
|
|
1173
|
+
return withTimeout(
|
|
1174
|
+
() => this.client.query(sql, params),
|
|
1175
|
+
this.timeoutMs,
|
|
1176
|
+
timeoutMessage(
|
|
1177
|
+
this.dialect === "postgres" ? "Postgres" : "MySQL",
|
|
1178
|
+
this.timeoutMs
|
|
1179
|
+
)
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
782
1182
|
};
|
|
783
1183
|
var PostgresErrorAdapter = class extends SqlErrorAdapter {
|
|
784
1184
|
/** @throws {RangeError} if `options.tableName` is not a plain identifier of 47 characters or fewer. */
|
|
@@ -803,7 +1203,8 @@ var RedisErrorAdapter = class {
|
|
|
803
1203
|
this.key = options.key ?? DEFAULT_REDIS_KEY;
|
|
804
1204
|
this.maxEntries = options.maxEntries;
|
|
805
1205
|
this.side = options.side ?? "right";
|
|
806
|
-
this.prefix = options.prefix ??
|
|
1206
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1207
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "RedisErrorAdapter");
|
|
807
1208
|
if (this.mode !== "list" && this.mode !== "stream") {
|
|
808
1209
|
throw new RangeError(
|
|
809
1210
|
`RedisErrorAdapter: mode must be "list" or "stream", received "${String(options.mode)}".`
|
|
@@ -825,11 +1226,11 @@ var RedisErrorAdapter = class {
|
|
|
825
1226
|
}
|
|
826
1227
|
async log(entry) {
|
|
827
1228
|
try {
|
|
828
|
-
const record = { id:
|
|
1229
|
+
const record = { id: resolveEntryId(entry), ...serializeEntry(entry) };
|
|
829
1230
|
if (this.mode === "stream") {
|
|
830
|
-
await this.
|
|
1231
|
+
await this.send(this.buildStreamCommand(record));
|
|
831
1232
|
} else {
|
|
832
|
-
await this.
|
|
1233
|
+
await this.send([
|
|
833
1234
|
this.side === "left" ? "LPUSH" : "RPUSH",
|
|
834
1235
|
this.key,
|
|
835
1236
|
JSON.stringify(record)
|
|
@@ -838,11 +1239,12 @@ var RedisErrorAdapter = class {
|
|
|
838
1239
|
}
|
|
839
1240
|
return { ok: true };
|
|
840
1241
|
} catch (error) {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1242
|
+
return reportFailure({
|
|
1243
|
+
adapterName: "RedisErrorAdapter",
|
|
1244
|
+
prefix: this.prefix,
|
|
1245
|
+
action: `to write to ${this.mode} "${this.key}"`,
|
|
1246
|
+
error
|
|
1247
|
+
});
|
|
846
1248
|
}
|
|
847
1249
|
}
|
|
848
1250
|
buildStreamCommand(record) {
|
|
@@ -874,6 +1276,14 @@ var RedisErrorAdapter = class {
|
|
|
874
1276
|
* stored by now, so a trim failure is reported but does not fail the call;
|
|
875
1277
|
* the next successful write trims the list again.
|
|
876
1278
|
*/
|
|
1279
|
+
/** Sends one command, giving up waiting after `timeoutMs`. */
|
|
1280
|
+
send(args) {
|
|
1281
|
+
return withTimeout(
|
|
1282
|
+
() => this.client.sendCommand(args),
|
|
1283
|
+
this.timeoutMs,
|
|
1284
|
+
timeoutMessage("Redis", this.timeoutMs)
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
877
1287
|
async trimList() {
|
|
878
1288
|
if (this.maxEntries === void 0) {
|
|
879
1289
|
return;
|
|
@@ -881,7 +1291,7 @@ var RedisErrorAdapter = class {
|
|
|
881
1291
|
const keep = this.maxEntries;
|
|
882
1292
|
const [start, stop] = this.side === "right" ? [String(-keep), "-1"] : ["0", String(keep - 1)];
|
|
883
1293
|
try {
|
|
884
|
-
await this.
|
|
1294
|
+
await this.send(["LTRIM", this.key, start, stop]);
|
|
885
1295
|
} catch (error) {
|
|
886
1296
|
console.error(
|
|
887
1297
|
`${this.prefix} RedisErrorAdapter: stored the entry but could not trim list "${this.key}" to ${keep} entries: ${describeError(error)}`
|
|
@@ -908,11 +1318,12 @@ var S3ErrorAdapter = class {
|
|
|
908
1318
|
this.bucket = bucket;
|
|
909
1319
|
this.buffer = [];
|
|
910
1320
|
this.timer = null;
|
|
911
|
-
this.flushQueue =
|
|
1321
|
+
this.flushQueue = new SerialQueue();
|
|
912
1322
|
this.keyPrefix = normalizePrefix(options.keyPrefix ?? DEFAULT_S3_PREFIX);
|
|
913
1323
|
this.batch = options.batch;
|
|
914
1324
|
this.contentType = options.contentType ?? (this.batch === void 0 ? "application/json" : "application/x-ndjson");
|
|
915
|
-
this.logPrefix = options.prefix ??
|
|
1325
|
+
this.logPrefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1326
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "S3ErrorAdapter");
|
|
916
1327
|
if (bucket === "") {
|
|
917
1328
|
throw new RangeError("S3ErrorAdapter: bucket must not be empty.");
|
|
918
1329
|
}
|
|
@@ -932,7 +1343,7 @@ var S3ErrorAdapter = class {
|
|
|
932
1343
|
async log(entry) {
|
|
933
1344
|
try {
|
|
934
1345
|
const record = {
|
|
935
|
-
id:
|
|
1346
|
+
id: resolveEntryId(entry),
|
|
936
1347
|
...serializeEntry(entry)
|
|
937
1348
|
};
|
|
938
1349
|
const occurredAt = new Date(record.timestamp);
|
|
@@ -957,11 +1368,9 @@ var S3ErrorAdapter = class {
|
|
|
957
1368
|
* Entries from a failed upload stay buffered and are retried by the next flush.
|
|
958
1369
|
*/
|
|
959
1370
|
flush() {
|
|
960
|
-
|
|
1371
|
+
return this.flushQueue.run(
|
|
961
1372
|
() => this.uploadBuffered()
|
|
962
1373
|
);
|
|
963
|
-
this.flushQueue = result.catch(() => void 0);
|
|
964
|
-
return result;
|
|
965
1374
|
}
|
|
966
1375
|
async addToBatch(record, batch) {
|
|
967
1376
|
this.buffer.push(record);
|
|
@@ -1000,7 +1409,7 @@ var S3ErrorAdapter = class {
|
|
|
1000
1409
|
return { ok: true };
|
|
1001
1410
|
}
|
|
1002
1411
|
try {
|
|
1003
|
-
const key = `${this.keyPrefix}${datePath(/* @__PURE__ */ new Date())}-${
|
|
1412
|
+
const key = `${this.keyPrefix}${datePath(/* @__PURE__ */ new Date())}-${generateId()}.ndjson`;
|
|
1004
1413
|
const body = pending.map((record) => JSON.stringify(record)).join("\n");
|
|
1005
1414
|
await this.upload(key, `${body}
|
|
1006
1415
|
`);
|
|
@@ -1017,64 +1426,41 @@ var S3ErrorAdapter = class {
|
|
|
1017
1426
|
}
|
|
1018
1427
|
}
|
|
1019
1428
|
upload(key, body) {
|
|
1020
|
-
return
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1429
|
+
return withTimeout(
|
|
1430
|
+
() => this.client.putObject({
|
|
1431
|
+
Bucket: this.bucket,
|
|
1432
|
+
Key: key,
|
|
1433
|
+
Body: body,
|
|
1434
|
+
ContentType: this.contentType
|
|
1435
|
+
}),
|
|
1436
|
+
this.timeoutMs,
|
|
1437
|
+
timeoutMessage("S3", this.timeoutMs)
|
|
1438
|
+
);
|
|
1026
1439
|
}
|
|
1027
1440
|
fail(error, bufferedCount) {
|
|
1028
|
-
console.error(`${this.logPrefix} S3ErrorAdapter:`, error);
|
|
1029
1441
|
const retry = bufferedCount === void 0 ? "" : ` (${bufferedCount} buffered ${bufferedCount === 1 ? "entry was" : "entries were"} kept for the next flush)`;
|
|
1030
|
-
return {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1442
|
+
return reportFailure({
|
|
1443
|
+
adapterName: "S3ErrorAdapter",
|
|
1444
|
+
prefix: this.logPrefix,
|
|
1445
|
+
action: `to upload to bucket "${this.bucket}"`,
|
|
1446
|
+
error,
|
|
1447
|
+
reason: `${describeError(error)}${retry}`
|
|
1448
|
+
});
|
|
1034
1449
|
}
|
|
1035
1450
|
};
|
|
1036
1451
|
|
|
1037
1452
|
// src/adapters/firebase-error-adapter.ts
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
`${adapter}: timeoutMs must be a positive number or Infinity, received ${String(options.timeoutMs)}.`
|
|
1044
|
-
);
|
|
1045
|
-
}
|
|
1046
|
-
return timeoutMs;
|
|
1047
|
-
}
|
|
1048
|
-
function waitForWrite(write, timeoutMs) {
|
|
1049
|
-
if (timeoutMs === Infinity) {
|
|
1050
|
-
return Promise.resolve(write);
|
|
1051
|
-
}
|
|
1052
|
-
return new Promise(
|
|
1053
|
-
(resolve, reject) => {
|
|
1054
|
-
const timer = setTimeout(() => {
|
|
1055
|
-
reject(
|
|
1056
|
-
new Error(
|
|
1057
|
-
`the write was not saved within ${timeoutMs} ms (the connection may be down; the SDK keeps the write queued and may still save it later)`
|
|
1058
|
-
)
|
|
1059
|
-
);
|
|
1060
|
-
}, timeoutMs);
|
|
1061
|
-
write.then(
|
|
1062
|
-
(value) => {
|
|
1063
|
-
clearTimeout(timer);
|
|
1064
|
-
resolve(value);
|
|
1065
|
-
},
|
|
1066
|
-
(reason) => {
|
|
1067
|
-
clearTimeout(timer);
|
|
1068
|
-
reject(reason);
|
|
1069
|
-
}
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1453
|
+
function waitForWrite(start, timeoutMs) {
|
|
1454
|
+
return withTimeout(
|
|
1455
|
+
start,
|
|
1456
|
+
timeoutMs,
|
|
1457
|
+
`the write was not saved within ${timeoutMs} ms (the connection may be down; the SDK keeps the write queued and may still save it later, so logging the entry again could store it twice unless it carries an id)`
|
|
1072
1458
|
);
|
|
1073
1459
|
}
|
|
1074
1460
|
function buildRecord(entry) {
|
|
1075
1461
|
const serialized = serializeEntry(entry);
|
|
1076
1462
|
const record = {
|
|
1077
|
-
id:
|
|
1463
|
+
id: resolveEntryId(entry),
|
|
1078
1464
|
code: serialized.code,
|
|
1079
1465
|
message: serialized.message,
|
|
1080
1466
|
timestamp: serialized.timestamp
|
|
@@ -1091,23 +1477,27 @@ var FirestoreErrorAdapter = class {
|
|
|
1091
1477
|
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
|
|
1092
1478
|
constructor(collection, options = {}) {
|
|
1093
1479
|
this.collection = collection;
|
|
1094
|
-
this.prefix = options.prefix ??
|
|
1095
|
-
this.timeoutMs =
|
|
1480
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1481
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1482
|
+
options.timeoutMs,
|
|
1483
|
+
"FirestoreErrorAdapter"
|
|
1484
|
+
);
|
|
1096
1485
|
}
|
|
1097
1486
|
async log(entry) {
|
|
1098
1487
|
try {
|
|
1099
1488
|
const record = buildRecord(entry);
|
|
1100
1489
|
await waitForWrite(
|
|
1101
|
-
this.collection.doc(record.id).set(record),
|
|
1490
|
+
() => this.collection.doc(record.id).set(record),
|
|
1102
1491
|
this.timeoutMs
|
|
1103
1492
|
);
|
|
1104
1493
|
return { ok: true };
|
|
1105
1494
|
} catch (error) {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1495
|
+
return reportFailure({
|
|
1496
|
+
adapterName: "FirestoreErrorAdapter",
|
|
1497
|
+
prefix: this.prefix,
|
|
1498
|
+
action: `to write the document`,
|
|
1499
|
+
error
|
|
1500
|
+
});
|
|
1111
1501
|
}
|
|
1112
1502
|
}
|
|
1113
1503
|
};
|
|
@@ -1115,34 +1505,51 @@ var RealtimeDatabaseErrorAdapter = class {
|
|
|
1115
1505
|
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
|
|
1116
1506
|
constructor(reference, options = {}) {
|
|
1117
1507
|
this.reference = reference;
|
|
1118
|
-
this.prefix = options.prefix ??
|
|
1119
|
-
this.timeoutMs =
|
|
1508
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1509
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1510
|
+
options.timeoutMs,
|
|
1511
|
+
"RealtimeDatabaseErrorAdapter"
|
|
1512
|
+
);
|
|
1120
1513
|
}
|
|
1121
1514
|
async log(entry) {
|
|
1122
1515
|
try {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1516
|
+
const record = buildRecord(entry);
|
|
1517
|
+
let write;
|
|
1518
|
+
if (hasCallerId(entry)) {
|
|
1519
|
+
if (this.reference.child === void 0) {
|
|
1520
|
+
throw new TypeError(
|
|
1521
|
+
"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"
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
write = this.reference.child(record.id).set(record);
|
|
1525
|
+
} else {
|
|
1526
|
+
write = this.reference.push(record);
|
|
1527
|
+
}
|
|
1528
|
+
await waitForWrite(() => write, this.timeoutMs);
|
|
1127
1529
|
return { ok: true };
|
|
1128
1530
|
} catch (error) {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1531
|
+
return reportFailure({
|
|
1532
|
+
adapterName: "RealtimeDatabaseErrorAdapter",
|
|
1533
|
+
prefix: this.prefix,
|
|
1534
|
+
action: `to write the entry`,
|
|
1535
|
+
error
|
|
1536
|
+
});
|
|
1134
1537
|
}
|
|
1135
1538
|
}
|
|
1136
1539
|
};
|
|
1137
1540
|
|
|
1138
1541
|
// src/adapters/supabase-error-adapter.ts
|
|
1139
|
-
var
|
|
1140
|
-
|
|
1542
|
+
var POLICY_MAX_CODE_LENGTH = 255;
|
|
1543
|
+
var POLICY_MAX_MESSAGE_LENGTH = 4096;
|
|
1544
|
+
var POLICY_MAX_CAUSE_BYTES = 8192;
|
|
1545
|
+
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})`;
|
|
1546
|
+
function buildSupabaseSetupSql(tableName = DEFAULT_TABLE_NAME, options = {}) {
|
|
1141
1547
|
const insertRoles = options.insertRoles ?? [];
|
|
1142
1548
|
for (const role of insertRoles) {
|
|
1143
|
-
|
|
1549
|
+
assertIdentifier(role, "role name", "buildSupabaseSetupSql");
|
|
1550
|
+
if ((role.toLowerCase() === "anon" || role.toLowerCase() === "public") && options.allowPublicInsert !== true) {
|
|
1144
1551
|
throw new RangeError(
|
|
1145
|
-
`buildSupabaseSetupSql:
|
|
1552
|
+
`buildSupabaseSetupSql: role "${role}" would let anyone who loads your page write to the log, because the anon key is public. Use "authenticated" instead, or pass allowPublicInsert: true to accept the risk of forged entries and quota use.`
|
|
1146
1553
|
);
|
|
1147
1554
|
}
|
|
1148
1555
|
}
|
|
@@ -1154,7 +1561,7 @@ function buildSupabaseSetupSql(tableName = DEFAULT_TABLE_NAME2, options = {}) {
|
|
|
1154
1561
|
const policy = `${tableName}_insert_only`;
|
|
1155
1562
|
statements.push(
|
|
1156
1563
|
`DROP POLICY IF EXISTS "${policy}" ON "${tableName}"`,
|
|
1157
|
-
`CREATE POLICY "${policy}" ON "${tableName}" FOR INSERT TO ${insertRoles.map((role) => `"${role}"`).join(", ")} WITH CHECK (
|
|
1564
|
+
`CREATE POLICY "${policy}" ON "${tableName}" FOR INSERT TO ${insertRoles.map((role) => `"${role}"`).join(", ")} WITH CHECK (${INSERT_POLICY_CHECK})`
|
|
1158
1565
|
);
|
|
1159
1566
|
}
|
|
1160
1567
|
return statements;
|
|
@@ -1184,44 +1591,52 @@ function describeSupabaseError(result) {
|
|
|
1184
1591
|
return parts.join("; ");
|
|
1185
1592
|
}
|
|
1186
1593
|
var SupabaseErrorAdapter = class {
|
|
1187
|
-
/** @throws {RangeError} if `options.tableName` is
|
|
1594
|
+
/** @throws {RangeError} if `options.tableName` is not a plain identifier or `options.timeoutMs` is invalid. */
|
|
1188
1595
|
constructor(client, options = {}) {
|
|
1189
1596
|
this.client = client;
|
|
1190
|
-
this.tableName = options.tableName ??
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1597
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
1598
|
+
assertIdentifier(this.tableName, "table name", "SupabaseErrorAdapter");
|
|
1599
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1600
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1601
|
+
options.timeoutMs,
|
|
1602
|
+
"SupabaseErrorAdapter"
|
|
1603
|
+
);
|
|
1197
1604
|
}
|
|
1198
1605
|
async log(entry) {
|
|
1199
1606
|
try {
|
|
1200
1607
|
const serialized = serializeEntry(entry);
|
|
1201
1608
|
const row = {
|
|
1202
|
-
id:
|
|
1609
|
+
id: resolveEntryId(entry),
|
|
1203
1610
|
code: serialized.code,
|
|
1204
1611
|
message: serialized.message,
|
|
1205
1612
|
cause: serialized.cause === void 0 ? null : serialized.cause,
|
|
1206
1613
|
source: serialized.source ?? null,
|
|
1207
1614
|
occurred_at: serialized.timestamp
|
|
1208
1615
|
};
|
|
1209
|
-
const result = await
|
|
1616
|
+
const result = await withTimeout(
|
|
1617
|
+
() => this.client.from(this.tableName).insert(row),
|
|
1618
|
+
this.timeoutMs,
|
|
1619
|
+
timeoutMessage("Supabase", this.timeoutMs)
|
|
1620
|
+
);
|
|
1210
1621
|
if (result.error) {
|
|
1211
|
-
|
|
1212
|
-
|
|
1622
|
+
if (hasCallerId(entry) && result.error.code === "23505") {
|
|
1623
|
+
return { ok: true };
|
|
1624
|
+
}
|
|
1625
|
+
return this.fail(result, describeSupabaseError(result));
|
|
1213
1626
|
}
|
|
1214
1627
|
return { ok: true };
|
|
1215
1628
|
} catch (error) {
|
|
1216
|
-
|
|
1217
|
-
return this.fail(describeError(error));
|
|
1629
|
+
return this.fail(error, describeError(error));
|
|
1218
1630
|
}
|
|
1219
1631
|
}
|
|
1220
|
-
fail(reason) {
|
|
1221
|
-
return {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1632
|
+
fail(logged, reason) {
|
|
1633
|
+
return reportFailure({
|
|
1634
|
+
adapterName: "SupabaseErrorAdapter",
|
|
1635
|
+
prefix: this.prefix,
|
|
1636
|
+
action: `to insert into table "${this.tableName}"`,
|
|
1637
|
+
error: logged,
|
|
1638
|
+
reason
|
|
1639
|
+
});
|
|
1225
1640
|
}
|
|
1226
1641
|
};
|
|
1227
1642
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -1244,6 +1659,7 @@ var SupabaseErrorAdapter = class {
|
|
|
1244
1659
|
WebhookErrorAdapter,
|
|
1245
1660
|
buildCreateErrorLogTableSql,
|
|
1246
1661
|
buildErrorLogTableDdl,
|
|
1662
|
+
buildMySqlWidenColumnsSql,
|
|
1247
1663
|
buildSupabaseSetupSql,
|
|
1248
1664
|
createErrorLogStore,
|
|
1249
1665
|
formatErrorLogLine,
|