@richardmcquiston01/error-log-adapter 0.3.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 +771 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +383 -13
- package/dist/index.d.ts +383 -13
- package/dist/index.mjs +766 -156
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/dist/index.cjs
CHANGED
|
@@ -22,19 +22,24 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
CompositeErrorAdapter: () => CompositeErrorAdapter,
|
|
24
24
|
ConsoleErrorAdapter: () => ConsoleErrorAdapter,
|
|
25
|
+
FirestoreErrorAdapter: () => FirestoreErrorAdapter,
|
|
25
26
|
FlatFileErrorAdapter: () => FlatFileErrorAdapter,
|
|
26
27
|
IndexedDbErrorAdapter: () => IndexedDbErrorAdapter,
|
|
27
28
|
JsonFileErrorAdapter: () => JsonFileErrorAdapter,
|
|
28
29
|
MongoDbErrorAdapter: () => MongoDbErrorAdapter,
|
|
29
30
|
MySqlErrorAdapter: () => MySqlErrorAdapter,
|
|
30
31
|
PostgresErrorAdapter: () => PostgresErrorAdapter,
|
|
32
|
+
RealtimeDatabaseErrorAdapter: () => RealtimeDatabaseErrorAdapter,
|
|
31
33
|
RedisErrorAdapter: () => RedisErrorAdapter,
|
|
32
34
|
S3ErrorAdapter: () => S3ErrorAdapter,
|
|
33
35
|
SqliteErrorAdapter: () => SqliteErrorAdapter,
|
|
36
|
+
SupabaseErrorAdapter: () => SupabaseErrorAdapter,
|
|
34
37
|
WebStorageErrorAdapter: () => WebStorageErrorAdapter,
|
|
35
38
|
WebhookErrorAdapter: () => WebhookErrorAdapter,
|
|
36
39
|
buildCreateErrorLogTableSql: () => buildCreateErrorLogTableSql,
|
|
37
40
|
buildErrorLogTableDdl: () => buildErrorLogTableDdl,
|
|
41
|
+
buildMySqlWidenColumnsSql: () => buildMySqlWidenColumnsSql,
|
|
42
|
+
buildSupabaseSetupSql: () => buildSupabaseSetupSql,
|
|
38
43
|
createErrorLogStore: () => createErrorLogStore,
|
|
39
44
|
formatErrorLogLine: () => formatErrorLogLine,
|
|
40
45
|
serializeCause: () => serializeCause,
|
|
@@ -43,11 +48,100 @@ __export(index_exports, {
|
|
|
43
48
|
module.exports = __toCommonJS(index_exports);
|
|
44
49
|
|
|
45
50
|
// src/serialize.ts
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
var MAX_CAUSE_DEPTH = 6;
|
|
52
|
+
function isErrorLike(value) {
|
|
53
|
+
if (value instanceof Error) {
|
|
54
|
+
return true;
|
|
49
55
|
}
|
|
50
|
-
|
|
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);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function serializeCause(cause) {
|
|
144
|
+
return toJsonSafe(cause, 0, /* @__PURE__ */ new WeakSet());
|
|
51
145
|
}
|
|
52
146
|
function serializeEntry(entry) {
|
|
53
147
|
const serialized = {
|
|
@@ -67,10 +161,20 @@ function describeError(error) {
|
|
|
67
161
|
return error instanceof Error ? error.message : String(error);
|
|
68
162
|
}
|
|
69
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
|
+
|
|
70
174
|
// src/adapters/console-error-adapter.ts
|
|
71
175
|
var ConsoleErrorAdapter = class {
|
|
72
176
|
constructor(options = {}) {
|
|
73
|
-
this.prefix = options.prefix ??
|
|
177
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
74
178
|
}
|
|
75
179
|
async log(entry) {
|
|
76
180
|
try {
|
|
@@ -85,14 +189,135 @@ var ConsoleErrorAdapter = class {
|
|
|
85
189
|
}
|
|
86
190
|
};
|
|
87
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
|
+
|
|
88
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
|
+
}
|
|
89
305
|
var WebhookErrorAdapter = class {
|
|
306
|
+
/** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or `timeoutMs` is invalid. */
|
|
90
307
|
constructor(url, options = {}) {
|
|
91
308
|
this.url = url;
|
|
92
309
|
this.options = options;
|
|
93
|
-
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;
|
|
94
313
|
}
|
|
95
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);
|
|
96
321
|
try {
|
|
97
322
|
const response = await fetch(this.url, {
|
|
98
323
|
method: "POST",
|
|
@@ -100,58 +325,112 @@ var WebhookErrorAdapter = class {
|
|
|
100
325
|
"Content-Type": "application/json",
|
|
101
326
|
...this.options.headers
|
|
102
327
|
},
|
|
103
|
-
|
|
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
|
|
104
336
|
});
|
|
337
|
+
if (response.type === "opaqueredirect") {
|
|
338
|
+
return this.fail(this.redirectMessage("a redirect"));
|
|
339
|
+
}
|
|
105
340
|
if (!response.ok) {
|
|
341
|
+
if (response.status >= 300 && response.status < 400) {
|
|
342
|
+
return this.fail(this.redirectMessage(`HTTP ${response.status}`));
|
|
343
|
+
}
|
|
106
344
|
return this.fail(
|
|
107
|
-
`webhook ${this.
|
|
345
|
+
`webhook ${this.target} responded with HTTP ${response.status}`
|
|
108
346
|
);
|
|
109
347
|
}
|
|
110
348
|
return { ok: true };
|
|
111
349
|
} catch (error) {
|
|
112
350
|
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
351
|
+
if (timedOut) {
|
|
352
|
+
return this.fail(
|
|
353
|
+
timeoutMessage(`webhook ${this.target}`, this.timeoutMs)
|
|
354
|
+
);
|
|
355
|
+
}
|
|
113
356
|
return this.fail(
|
|
114
|
-
`could not reach webhook ${this.
|
|
357
|
+
`could not reach webhook ${this.target}: ${this.redact(describeError(error))}`
|
|
115
358
|
);
|
|
359
|
+
} finally {
|
|
360
|
+
clearTimeout(timer);
|
|
116
361
|
}
|
|
117
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
|
+
}
|
|
118
370
|
fail(reason) {
|
|
119
371
|
return { ok: false, error: `WebhookErrorAdapter failed: ${reason}` };
|
|
120
372
|
}
|
|
121
373
|
};
|
|
122
374
|
|
|
123
375
|
// src/adapters/mongodb-error-adapter.ts
|
|
376
|
+
function isDuplicateKeyError(error) {
|
|
377
|
+
return typeof error === "object" && error !== null && error.code === 11e3;
|
|
378
|
+
}
|
|
124
379
|
var MongoDbErrorAdapter = class {
|
|
380
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
125
381
|
constructor(collection, options = {}) {
|
|
126
382
|
this.collection = collection;
|
|
127
|
-
this.prefix = options.prefix ??
|
|
383
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
384
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "MongoDbErrorAdapter");
|
|
128
385
|
}
|
|
129
386
|
async log(entry) {
|
|
130
387
|
try {
|
|
131
|
-
|
|
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
|
+
);
|
|
132
394
|
return { ok: true };
|
|
133
395
|
} catch (error) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
});
|
|
139
405
|
}
|
|
140
406
|
}
|
|
141
407
|
};
|
|
142
408
|
|
|
143
409
|
// src/adapters/composite-error-adapter.ts
|
|
410
|
+
var DEFAULT_COMPOSITE_TIMEOUT_MS = DEFAULT_TIMEOUT_MS * 3;
|
|
144
411
|
var CompositeErrorAdapter = class {
|
|
145
|
-
|
|
412
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
413
|
+
constructor(adapters, options = {}) {
|
|
146
414
|
this.adapters = adapters;
|
|
415
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
416
|
+
options.timeoutMs ?? DEFAULT_COMPOSITE_TIMEOUT_MS,
|
|
417
|
+
"CompositeErrorAdapter"
|
|
418
|
+
);
|
|
147
419
|
}
|
|
148
420
|
async log(entry) {
|
|
149
421
|
const results = await Promise.all(
|
|
150
422
|
this.adapters.map(
|
|
151
|
-
async (adapter) => {
|
|
423
|
+
async (adapter, index) => {
|
|
152
424
|
try {
|
|
153
|
-
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
|
+
);
|
|
154
430
|
} catch (error) {
|
|
431
|
+
if (error instanceof SinkTimeoutError) {
|
|
432
|
+
return { ok: false, error: error.message };
|
|
433
|
+
}
|
|
155
434
|
return {
|
|
156
435
|
ok: false,
|
|
157
436
|
error: `adapter threw instead of returning a result: ${String(error)}`
|
|
@@ -182,59 +461,102 @@ function createErrorLogStore(database, storeName = DEFAULT_STORE_NAME) {
|
|
|
182
461
|
database.createObjectStore(storeName, { keyPath: "id" }).createIndex("timestamp", "timestamp");
|
|
183
462
|
}
|
|
184
463
|
var IndexedDbErrorAdapter = class {
|
|
464
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
|
|
185
465
|
constructor(database, options = {}) {
|
|
186
466
|
this.database = database;
|
|
187
467
|
this.storeName = options.storeName ?? DEFAULT_STORE_NAME;
|
|
188
|
-
this.prefix = options.prefix ??
|
|
468
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
469
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
470
|
+
options.timeoutMs,
|
|
471
|
+
"IndexedDbErrorAdapter"
|
|
472
|
+
);
|
|
189
473
|
}
|
|
190
474
|
async log(entry) {
|
|
191
475
|
try {
|
|
192
476
|
const record = {
|
|
193
|
-
id:
|
|
477
|
+
id: resolveEntryId(entry),
|
|
194
478
|
...serializeEntry(entry)
|
|
195
479
|
};
|
|
196
|
-
await this.write(record);
|
|
480
|
+
await this.write(record, hasCallerId(entry));
|
|
197
481
|
return { ok: true };
|
|
198
482
|
} catch (error) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
483
|
+
return reportFailure({
|
|
484
|
+
adapterName: "IndexedDbErrorAdapter",
|
|
485
|
+
prefix: this.prefix,
|
|
486
|
+
action: `to write to object store "${this.storeName}"`,
|
|
487
|
+
error
|
|
488
|
+
});
|
|
204
489
|
}
|
|
205
490
|
}
|
|
206
|
-
/**
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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")
|
|
224
529
|
);
|
|
225
530
|
}
|
|
226
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
|
+
}
|
|
227
539
|
|
|
228
|
-
// src/
|
|
540
|
+
// src/sql-ident.ts
|
|
229
541
|
var DEFAULT_TABLE_NAME = "error_log";
|
|
230
542
|
var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
231
|
-
function
|
|
232
|
-
if (!IDENTIFIER_PATTERN.test(
|
|
543
|
+
function assertIdentifier(value, label, owner, maxLength) {
|
|
544
|
+
if (!IDENTIFIER_PATTERN.test(value)) {
|
|
233
545
|
throw new RangeError(
|
|
234
|
-
|
|
546
|
+
`${owner}: invalid ${label} "${value}". Use only letters, digits and underscores, and do not start with a digit.`
|
|
547
|
+
);
|
|
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}.`
|
|
235
552
|
);
|
|
236
553
|
}
|
|
237
554
|
}
|
|
555
|
+
|
|
556
|
+
// src/adapters/sqlite-error-adapter.ts
|
|
557
|
+
function assertValidTableName(tableName) {
|
|
558
|
+
assertIdentifier(tableName, "table name", "SqliteErrorAdapter");
|
|
559
|
+
}
|
|
238
560
|
function buildCreateErrorLogTableSql(tableName = DEFAULT_TABLE_NAME) {
|
|
239
561
|
assertValidTableName(tableName);
|
|
240
562
|
return [
|
|
@@ -243,23 +565,24 @@ function buildCreateErrorLogTableSql(tableName = DEFAULT_TABLE_NAME) {
|
|
|
243
565
|
];
|
|
244
566
|
}
|
|
245
567
|
var SqliteErrorAdapter = class {
|
|
246
|
-
/** @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. */
|
|
247
569
|
constructor(client, options = {}) {
|
|
248
570
|
this.client = client;
|
|
249
571
|
this.tableReady = null;
|
|
250
572
|
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
251
573
|
assertValidTableName(this.tableName);
|
|
252
574
|
this.createTable = options.createTable ?? true;
|
|
253
|
-
this.prefix = options.prefix ??
|
|
575
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
576
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "SqliteErrorAdapter");
|
|
254
577
|
}
|
|
255
578
|
async log(entry) {
|
|
256
579
|
try {
|
|
257
580
|
await this.ensureTable();
|
|
258
581
|
const serialized = serializeEntry(entry);
|
|
259
|
-
await this.
|
|
260
|
-
`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`,
|
|
261
584
|
[
|
|
262
|
-
|
|
585
|
+
resolveEntryId(entry),
|
|
263
586
|
serialized.code,
|
|
264
587
|
serialized.message,
|
|
265
588
|
serialized.cause === void 0 ? null : JSON.stringify(serialized.cause),
|
|
@@ -269,11 +592,12 @@ var SqliteErrorAdapter = class {
|
|
|
269
592
|
);
|
|
270
593
|
return { ok: true };
|
|
271
594
|
} catch (error) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
595
|
+
return reportFailure({
|
|
596
|
+
adapterName: "SqliteErrorAdapter",
|
|
597
|
+
prefix: this.prefix,
|
|
598
|
+
action: `to write to table "${this.tableName}"`,
|
|
599
|
+
error
|
|
600
|
+
});
|
|
277
601
|
}
|
|
278
602
|
}
|
|
279
603
|
/** Creates the table once. A failed attempt is retried on the next log call. */
|
|
@@ -293,9 +617,17 @@ var SqliteErrorAdapter = class {
|
|
|
293
617
|
}
|
|
294
618
|
async runCreateStatements() {
|
|
295
619
|
for (const sql of buildCreateErrorLogTableSql(this.tableName)) {
|
|
296
|
-
await this.
|
|
620
|
+
await this.run(sql, []);
|
|
297
621
|
}
|
|
298
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
|
+
}
|
|
299
631
|
};
|
|
300
632
|
|
|
301
633
|
// src/adapters/web-storage-error-adapter.ts
|
|
@@ -314,7 +646,8 @@ var WebStorageErrorAdapter = class {
|
|
|
314
646
|
this.storage = storage;
|
|
315
647
|
this.key = options.key ?? DEFAULT_STORAGE_KEY;
|
|
316
648
|
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
317
|
-
this.prefix = options.prefix ??
|
|
649
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
650
|
+
this.locks = options.locks === void 0 ? defaultLockManager() : options.locks;
|
|
318
651
|
if (!Number.isInteger(this.maxEntries) || this.maxEntries < 1) {
|
|
319
652
|
throw new RangeError(
|
|
320
653
|
`WebStorageErrorAdapter: maxEntries must be a positive integer, received ${String(options.maxEntries)}.`
|
|
@@ -324,23 +657,49 @@ var WebStorageErrorAdapter = class {
|
|
|
324
657
|
async log(entry) {
|
|
325
658
|
try {
|
|
326
659
|
const record = {
|
|
327
|
-
id:
|
|
660
|
+
id: resolveEntryId(entry),
|
|
328
661
|
...serializeEntry(entry)
|
|
329
662
|
};
|
|
330
|
-
this.
|
|
663
|
+
await this.appendLocked(record);
|
|
331
664
|
return { ok: true };
|
|
332
665
|
} catch (error) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
666
|
+
return reportFailure({
|
|
667
|
+
adapterName: "WebStorageErrorAdapter",
|
|
668
|
+
prefix: this.prefix,
|
|
669
|
+
action: `to write to storage key "${this.key}"`,
|
|
670
|
+
error
|
|
671
|
+
});
|
|
338
672
|
}
|
|
339
673
|
}
|
|
340
|
-
/**
|
|
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);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/** Reads, appends, trims, and writes back. Runs without awaiting, so calls in one tab never interleave. */
|
|
341
698
|
append(record) {
|
|
342
699
|
const records = [
|
|
343
|
-
...this.readRecords()
|
|
700
|
+
...this.readRecords().filter(
|
|
701
|
+
(existing) => existing?.id !== record.id
|
|
702
|
+
),
|
|
344
703
|
record
|
|
345
704
|
].slice(-this.maxEntries);
|
|
346
705
|
for (; ; ) {
|
|
@@ -366,16 +725,51 @@ var WebStorageErrorAdapter = class {
|
|
|
366
725
|
if (Array.isArray(parsed)) {
|
|
367
726
|
return parsed;
|
|
368
727
|
}
|
|
369
|
-
|
|
370
|
-
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" did not hold an array, so it was replaced.`
|
|
371
|
-
);
|
|
728
|
+
this.setAside(stored, "did not hold an array");
|
|
372
729
|
} catch (error) {
|
|
373
|
-
|
|
374
|
-
`${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" held invalid JSON (${describeError(error)}), so it was replaced.`
|
|
375
|
-
);
|
|
730
|
+
this.setAside(stored, `held invalid JSON (${describeError(error)})`);
|
|
376
731
|
}
|
|
377
732
|
return [];
|
|
378
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
|
+
}
|
|
379
773
|
};
|
|
380
774
|
|
|
381
775
|
// src/file-system.ts
|
|
@@ -383,6 +777,7 @@ async function loadNodeFileSystem() {
|
|
|
383
777
|
const moduleName = "node:fs/promises";
|
|
384
778
|
return await import(moduleName);
|
|
385
779
|
}
|
|
780
|
+
var ROOT_PATTERN = /^(?:[A-Za-z]:)?[\\/]+$/;
|
|
386
781
|
function parentDirectory(filePath) {
|
|
387
782
|
const lastSeparator = Math.max(
|
|
388
783
|
filePath.lastIndexOf("/"),
|
|
@@ -391,7 +786,11 @@ function parentDirectory(filePath) {
|
|
|
391
786
|
if (lastSeparator < 0) {
|
|
392
787
|
return null;
|
|
393
788
|
}
|
|
394
|
-
|
|
789
|
+
const directory = filePath.slice(0, lastSeparator + 1);
|
|
790
|
+
if (ROOT_PATTERN.test(directory)) {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
return filePath.slice(0, lastSeparator);
|
|
395
794
|
}
|
|
396
795
|
function isMissingFileError(error) {
|
|
397
796
|
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
@@ -437,24 +836,20 @@ var FlatFileErrorAdapter = class {
|
|
|
437
836
|
constructor(filePath, options = {}) {
|
|
438
837
|
this.filePath = filePath;
|
|
439
838
|
this.directoryReady = false;
|
|
440
|
-
this.queue =
|
|
839
|
+
this.queue = new SerialQueue();
|
|
441
840
|
this.fs = options.fs;
|
|
442
841
|
this.formatLine = options.formatLine ?? formatErrorLogLine;
|
|
443
842
|
this.maxBytes = options.maxBytes;
|
|
444
843
|
this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
445
844
|
this.createDirectory = options.createDirectory ?? true;
|
|
446
|
-
this.prefix = options.prefix ??
|
|
845
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
447
846
|
if (this.maxBytes !== void 0) {
|
|
448
847
|
assertPositiveInteger(this.maxBytes, "maxBytes");
|
|
449
848
|
}
|
|
450
849
|
assertPositiveInteger(this.maxFiles, "maxFiles");
|
|
451
850
|
}
|
|
452
851
|
log(entry) {
|
|
453
|
-
|
|
454
|
-
() => this.write(entry)
|
|
455
|
-
);
|
|
456
|
-
this.queue = result.catch(() => void 0);
|
|
457
|
-
return result;
|
|
852
|
+
return this.queue.run(() => this.write(entry));
|
|
458
853
|
}
|
|
459
854
|
async write(entry) {
|
|
460
855
|
try {
|
|
@@ -467,11 +862,12 @@ var FlatFileErrorAdapter = class {
|
|
|
467
862
|
await fs.appendFile(this.filePath, line);
|
|
468
863
|
return { ok: true };
|
|
469
864
|
} catch (error) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
865
|
+
return reportFailure({
|
|
866
|
+
adapterName: "FlatFileErrorAdapter",
|
|
867
|
+
prefix: this.prefix,
|
|
868
|
+
action: `to append to "${this.filePath}"`,
|
|
869
|
+
error
|
|
870
|
+
});
|
|
475
871
|
}
|
|
476
872
|
}
|
|
477
873
|
/** Uses the injected file system, or loads `node:fs/promises` on first use. */
|
|
@@ -546,13 +942,13 @@ var JsonFileErrorAdapter = class {
|
|
|
546
942
|
constructor(filePath, options = {}) {
|
|
547
943
|
this.filePath = filePath;
|
|
548
944
|
this.directoryReady = false;
|
|
549
|
-
this.queue =
|
|
945
|
+
this.queue = new SerialQueue();
|
|
550
946
|
this.mode = options.mode ?? "ndjson";
|
|
551
947
|
this.maxEntries = options.maxEntries;
|
|
552
948
|
this.indent = options.indent ?? DEFAULT_INDENT;
|
|
553
949
|
this.fs = options.fs;
|
|
554
950
|
this.createDirectory = options.createDirectory ?? true;
|
|
555
|
-
this.prefix = options.prefix ??
|
|
951
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
556
952
|
if (this.maxEntries !== void 0) {
|
|
557
953
|
if (this.mode !== "array") {
|
|
558
954
|
throw new RangeError(
|
|
@@ -572,16 +968,12 @@ var JsonFileErrorAdapter = class {
|
|
|
572
968
|
}
|
|
573
969
|
}
|
|
574
970
|
log(entry) {
|
|
575
|
-
|
|
576
|
-
() => this.write(entry)
|
|
577
|
-
);
|
|
578
|
-
this.queue = result.catch(() => void 0);
|
|
579
|
-
return result;
|
|
971
|
+
return this.queue.run(() => this.write(entry));
|
|
580
972
|
}
|
|
581
973
|
async write(entry) {
|
|
582
974
|
try {
|
|
583
975
|
const record = {
|
|
584
|
-
id:
|
|
976
|
+
id: resolveEntryId(entry),
|
|
585
977
|
...serializeEntry(entry)
|
|
586
978
|
};
|
|
587
979
|
const fs = await this.getFileSystem();
|
|
@@ -594,11 +986,12 @@ var JsonFileErrorAdapter = class {
|
|
|
594
986
|
}
|
|
595
987
|
return { ok: true };
|
|
596
988
|
} catch (error) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
989
|
+
return reportFailure({
|
|
990
|
+
adapterName: "JsonFileErrorAdapter",
|
|
991
|
+
prefix: this.prefix,
|
|
992
|
+
action: `to write "${this.filePath}" in ${this.mode} mode`,
|
|
993
|
+
error
|
|
994
|
+
});
|
|
602
995
|
}
|
|
603
996
|
}
|
|
604
997
|
async getFileSystem() {
|
|
@@ -624,7 +1017,9 @@ var JsonFileErrorAdapter = class {
|
|
|
624
1017
|
*/
|
|
625
1018
|
async rewriteArray(fs, record) {
|
|
626
1019
|
const records = [
|
|
627
|
-
...await this.readRecords(fs)
|
|
1020
|
+
...(await this.readRecords(fs)).filter(
|
|
1021
|
+
(existing) => existing?.id !== record.id
|
|
1022
|
+
),
|
|
628
1023
|
record
|
|
629
1024
|
];
|
|
630
1025
|
const kept = this.maxEntries === void 0 ? records : records.slice(-this.maxEntries);
|
|
@@ -674,25 +1069,14 @@ var JsonFileErrorAdapter = class {
|
|
|
674
1069
|
};
|
|
675
1070
|
|
|
676
1071
|
// src/adapters/sql-error-adapter.ts
|
|
677
|
-
var DEFAULT_TABLE_NAME2 = "error_log";
|
|
678
1072
|
var MAX_TABLE_NAME_LENGTH = 47;
|
|
679
|
-
var IDENTIFIER_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
680
1073
|
function assertValidTableName2(tableName, adapterName) {
|
|
681
|
-
|
|
682
|
-
throw new RangeError(
|
|
683
|
-
`${adapterName}: invalid table name "${tableName}". Use only letters, digits and underscores, and do not start with a digit.`
|
|
684
|
-
);
|
|
685
|
-
}
|
|
686
|
-
if (tableName.length > MAX_TABLE_NAME_LENGTH) {
|
|
687
|
-
throw new RangeError(
|
|
688
|
-
`${adapterName}: table name "${tableName}" is ${tableName.length} characters long, but the limit is ${MAX_TABLE_NAME_LENGTH}.`
|
|
689
|
-
);
|
|
690
|
-
}
|
|
1074
|
+
assertIdentifier(tableName, "table name", adapterName, MAX_TABLE_NAME_LENGTH);
|
|
691
1075
|
}
|
|
692
1076
|
function quoteIdentifier(dialect, identifier) {
|
|
693
1077
|
return dialect === "postgres" ? `"${identifier}"` : `\`${identifier}\``;
|
|
694
1078
|
}
|
|
695
|
-
function buildErrorLogTableDdl(dialect, tableName =
|
|
1079
|
+
function buildErrorLogTableDdl(dialect, tableName = DEFAULT_TABLE_NAME) {
|
|
696
1080
|
assertValidTableName2(tableName, "buildErrorLogTableDdl");
|
|
697
1081
|
const table = quoteIdentifier(dialect, tableName);
|
|
698
1082
|
const index = quoteIdentifier(
|
|
@@ -706,7 +1090,13 @@ function buildErrorLogTableDdl(dialect, tableName = DEFAULT_TABLE_NAME2) {
|
|
|
706
1090
|
];
|
|
707
1091
|
}
|
|
708
1092
|
return [
|
|
709
|
-
`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`
|
|
710
1100
|
];
|
|
711
1101
|
}
|
|
712
1102
|
function toMysqlDateTime(date) {
|
|
@@ -718,10 +1108,11 @@ var SqlErrorAdapter = class {
|
|
|
718
1108
|
this.dialect = dialect;
|
|
719
1109
|
this.adapterName = adapterName;
|
|
720
1110
|
this.tableReady = null;
|
|
721
|
-
this.tableName = options.tableName ??
|
|
1111
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
722
1112
|
assertValidTableName2(this.tableName, adapterName);
|
|
723
1113
|
this.createTable = options.createTable ?? true;
|
|
724
|
-
this.prefix = options.prefix ??
|
|
1114
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1115
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, adapterName);
|
|
725
1116
|
}
|
|
726
1117
|
async log(entry) {
|
|
727
1118
|
try {
|
|
@@ -733,7 +1124,7 @@ var SqlErrorAdapter = class {
|
|
|
733
1124
|
);
|
|
734
1125
|
}
|
|
735
1126
|
const params = [
|
|
736
|
-
|
|
1127
|
+
resolveEntryId(entry),
|
|
737
1128
|
serialized.code,
|
|
738
1129
|
serialized.message,
|
|
739
1130
|
serialized.cause === void 0 ? null : JSON.stringify(serialized.cause),
|
|
@@ -741,19 +1132,21 @@ var SqlErrorAdapter = class {
|
|
|
741
1132
|
this.dialect === "postgres" ? occurredAt.toISOString() : toMysqlDateTime(occurredAt)
|
|
742
1133
|
];
|
|
743
1134
|
await this.ensureTable();
|
|
744
|
-
await this.
|
|
1135
|
+
await this.query(this.insertSql(), params);
|
|
745
1136
|
return { ok: true };
|
|
746
1137
|
} catch (error) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1138
|
+
return reportFailure({
|
|
1139
|
+
adapterName: this.adapterName,
|
|
1140
|
+
prefix: this.prefix,
|
|
1141
|
+
action: `to write to table "${this.tableName}"`,
|
|
1142
|
+
error
|
|
1143
|
+
});
|
|
752
1144
|
}
|
|
753
1145
|
}
|
|
754
1146
|
insertSql() {
|
|
755
1147
|
const placeholders = this.dialect === "postgres" ? "$1, $2, $3, $4, $5, $6" : "?, ?, ?, ?, ?, ?";
|
|
756
|
-
|
|
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}`;
|
|
757
1150
|
}
|
|
758
1151
|
/** Creates the table once. A failed attempt is retried on the next log call. */
|
|
759
1152
|
ensureTable() {
|
|
@@ -772,9 +1165,20 @@ var SqlErrorAdapter = class {
|
|
|
772
1165
|
}
|
|
773
1166
|
async runCreateStatements() {
|
|
774
1167
|
for (const sql of buildErrorLogTableDdl(this.dialect, this.tableName)) {
|
|
775
|
-
await this.
|
|
1168
|
+
await this.query(sql, []);
|
|
776
1169
|
}
|
|
777
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
|
+
}
|
|
778
1182
|
};
|
|
779
1183
|
var PostgresErrorAdapter = class extends SqlErrorAdapter {
|
|
780
1184
|
/** @throws {RangeError} if `options.tableName` is not a plain identifier of 47 characters or fewer. */
|
|
@@ -799,7 +1203,8 @@ var RedisErrorAdapter = class {
|
|
|
799
1203
|
this.key = options.key ?? DEFAULT_REDIS_KEY;
|
|
800
1204
|
this.maxEntries = options.maxEntries;
|
|
801
1205
|
this.side = options.side ?? "right";
|
|
802
|
-
this.prefix = options.prefix ??
|
|
1206
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1207
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "RedisErrorAdapter");
|
|
803
1208
|
if (this.mode !== "list" && this.mode !== "stream") {
|
|
804
1209
|
throw new RangeError(
|
|
805
1210
|
`RedisErrorAdapter: mode must be "list" or "stream", received "${String(options.mode)}".`
|
|
@@ -821,11 +1226,11 @@ var RedisErrorAdapter = class {
|
|
|
821
1226
|
}
|
|
822
1227
|
async log(entry) {
|
|
823
1228
|
try {
|
|
824
|
-
const record = { id:
|
|
1229
|
+
const record = { id: resolveEntryId(entry), ...serializeEntry(entry) };
|
|
825
1230
|
if (this.mode === "stream") {
|
|
826
|
-
await this.
|
|
1231
|
+
await this.send(this.buildStreamCommand(record));
|
|
827
1232
|
} else {
|
|
828
|
-
await this.
|
|
1233
|
+
await this.send([
|
|
829
1234
|
this.side === "left" ? "LPUSH" : "RPUSH",
|
|
830
1235
|
this.key,
|
|
831
1236
|
JSON.stringify(record)
|
|
@@ -834,11 +1239,12 @@ var RedisErrorAdapter = class {
|
|
|
834
1239
|
}
|
|
835
1240
|
return { ok: true };
|
|
836
1241
|
} catch (error) {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1242
|
+
return reportFailure({
|
|
1243
|
+
adapterName: "RedisErrorAdapter",
|
|
1244
|
+
prefix: this.prefix,
|
|
1245
|
+
action: `to write to ${this.mode} "${this.key}"`,
|
|
1246
|
+
error
|
|
1247
|
+
});
|
|
842
1248
|
}
|
|
843
1249
|
}
|
|
844
1250
|
buildStreamCommand(record) {
|
|
@@ -870,6 +1276,14 @@ var RedisErrorAdapter = class {
|
|
|
870
1276
|
* stored by now, so a trim failure is reported but does not fail the call;
|
|
871
1277
|
* the next successful write trims the list again.
|
|
872
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
|
+
}
|
|
873
1287
|
async trimList() {
|
|
874
1288
|
if (this.maxEntries === void 0) {
|
|
875
1289
|
return;
|
|
@@ -877,7 +1291,7 @@ var RedisErrorAdapter = class {
|
|
|
877
1291
|
const keep = this.maxEntries;
|
|
878
1292
|
const [start, stop] = this.side === "right" ? [String(-keep), "-1"] : ["0", String(keep - 1)];
|
|
879
1293
|
try {
|
|
880
|
-
await this.
|
|
1294
|
+
await this.send(["LTRIM", this.key, start, stop]);
|
|
881
1295
|
} catch (error) {
|
|
882
1296
|
console.error(
|
|
883
1297
|
`${this.prefix} RedisErrorAdapter: stored the entry but could not trim list "${this.key}" to ${keep} entries: ${describeError(error)}`
|
|
@@ -904,11 +1318,12 @@ var S3ErrorAdapter = class {
|
|
|
904
1318
|
this.bucket = bucket;
|
|
905
1319
|
this.buffer = [];
|
|
906
1320
|
this.timer = null;
|
|
907
|
-
this.flushQueue =
|
|
1321
|
+
this.flushQueue = new SerialQueue();
|
|
908
1322
|
this.keyPrefix = normalizePrefix(options.keyPrefix ?? DEFAULT_S3_PREFIX);
|
|
909
1323
|
this.batch = options.batch;
|
|
910
1324
|
this.contentType = options.contentType ?? (this.batch === void 0 ? "application/json" : "application/x-ndjson");
|
|
911
|
-
this.logPrefix = options.prefix ??
|
|
1325
|
+
this.logPrefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1326
|
+
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "S3ErrorAdapter");
|
|
912
1327
|
if (bucket === "") {
|
|
913
1328
|
throw new RangeError("S3ErrorAdapter: bucket must not be empty.");
|
|
914
1329
|
}
|
|
@@ -928,7 +1343,7 @@ var S3ErrorAdapter = class {
|
|
|
928
1343
|
async log(entry) {
|
|
929
1344
|
try {
|
|
930
1345
|
const record = {
|
|
931
|
-
id:
|
|
1346
|
+
id: resolveEntryId(entry),
|
|
932
1347
|
...serializeEntry(entry)
|
|
933
1348
|
};
|
|
934
1349
|
const occurredAt = new Date(record.timestamp);
|
|
@@ -953,11 +1368,9 @@ var S3ErrorAdapter = class {
|
|
|
953
1368
|
* Entries from a failed upload stay buffered and are retried by the next flush.
|
|
954
1369
|
*/
|
|
955
1370
|
flush() {
|
|
956
|
-
|
|
1371
|
+
return this.flushQueue.run(
|
|
957
1372
|
() => this.uploadBuffered()
|
|
958
1373
|
);
|
|
959
|
-
this.flushQueue = result.catch(() => void 0);
|
|
960
|
-
return result;
|
|
961
1374
|
}
|
|
962
1375
|
async addToBatch(record, batch) {
|
|
963
1376
|
this.buffer.push(record);
|
|
@@ -996,7 +1409,7 @@ var S3ErrorAdapter = class {
|
|
|
996
1409
|
return { ok: true };
|
|
997
1410
|
}
|
|
998
1411
|
try {
|
|
999
|
-
const key = `${this.keyPrefix}${datePath(/* @__PURE__ */ new Date())}-${
|
|
1412
|
+
const key = `${this.keyPrefix}${datePath(/* @__PURE__ */ new Date())}-${generateId()}.ndjson`;
|
|
1000
1413
|
const body = pending.map((record) => JSON.stringify(record)).join("\n");
|
|
1001
1414
|
await this.upload(key, `${body}
|
|
1002
1415
|
`);
|
|
@@ -1013,39 +1426,241 @@ var S3ErrorAdapter = class {
|
|
|
1013
1426
|
}
|
|
1014
1427
|
}
|
|
1015
1428
|
upload(key, body) {
|
|
1016
|
-
return
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
+
);
|
|
1022
1439
|
}
|
|
1023
1440
|
fail(error, bufferedCount) {
|
|
1024
|
-
console.error(`${this.logPrefix} S3ErrorAdapter:`, error);
|
|
1025
1441
|
const retry = bufferedCount === void 0 ? "" : ` (${bufferedCount} buffered ${bufferedCount === 1 ? "entry was" : "entries were"} kept for the next flush)`;
|
|
1026
|
-
return {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
// src/adapters/firebase-error-adapter.ts
|
|
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)`
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
function buildRecord(entry) {
|
|
1461
|
+
const serialized = serializeEntry(entry);
|
|
1462
|
+
const record = {
|
|
1463
|
+
id: resolveEntryId(entry),
|
|
1464
|
+
code: serialized.code,
|
|
1465
|
+
message: serialized.message,
|
|
1466
|
+
timestamp: serialized.timestamp
|
|
1467
|
+
};
|
|
1468
|
+
if (serialized.source !== void 0) {
|
|
1469
|
+
record.source = serialized.source;
|
|
1470
|
+
}
|
|
1471
|
+
if (serialized.cause !== void 0) {
|
|
1472
|
+
record.cause = JSON.stringify(serialized.cause);
|
|
1473
|
+
}
|
|
1474
|
+
return record;
|
|
1475
|
+
}
|
|
1476
|
+
var FirestoreErrorAdapter = class {
|
|
1477
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
|
|
1478
|
+
constructor(collection, options = {}) {
|
|
1479
|
+
this.collection = collection;
|
|
1480
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1481
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1482
|
+
options.timeoutMs,
|
|
1483
|
+
"FirestoreErrorAdapter"
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
async log(entry) {
|
|
1487
|
+
try {
|
|
1488
|
+
const record = buildRecord(entry);
|
|
1489
|
+
await waitForWrite(
|
|
1490
|
+
() => this.collection.doc(record.id).set(record),
|
|
1491
|
+
this.timeoutMs
|
|
1492
|
+
);
|
|
1493
|
+
return { ok: true };
|
|
1494
|
+
} catch (error) {
|
|
1495
|
+
return reportFailure({
|
|
1496
|
+
adapterName: "FirestoreErrorAdapter",
|
|
1497
|
+
prefix: this.prefix,
|
|
1498
|
+
action: `to write the document`,
|
|
1499
|
+
error
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
var RealtimeDatabaseErrorAdapter = class {
|
|
1505
|
+
/** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
|
|
1506
|
+
constructor(reference, options = {}) {
|
|
1507
|
+
this.reference = reference;
|
|
1508
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1509
|
+
this.timeoutMs = resolveTimeoutMs(
|
|
1510
|
+
options.timeoutMs,
|
|
1511
|
+
"RealtimeDatabaseErrorAdapter"
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
async log(entry) {
|
|
1515
|
+
try {
|
|
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);
|
|
1529
|
+
return { ok: true };
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
return reportFailure({
|
|
1532
|
+
adapterName: "RealtimeDatabaseErrorAdapter",
|
|
1533
|
+
prefix: this.prefix,
|
|
1534
|
+
action: `to write the entry`,
|
|
1535
|
+
error
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
// src/adapters/supabase-error-adapter.ts
|
|
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 = {}) {
|
|
1547
|
+
const insertRoles = options.insertRoles ?? [];
|
|
1548
|
+
for (const role of insertRoles) {
|
|
1549
|
+
assertIdentifier(role, "role name", "buildSupabaseSetupSql");
|
|
1550
|
+
if ((role.toLowerCase() === "anon" || role.toLowerCase() === "public") && options.allowPublicInsert !== true) {
|
|
1551
|
+
throw new RangeError(
|
|
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.`
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
const statements = [
|
|
1557
|
+
...buildErrorLogTableDdl("postgres", tableName),
|
|
1558
|
+
`ALTER TABLE "${tableName}" ENABLE ROW LEVEL SECURITY`
|
|
1559
|
+
];
|
|
1560
|
+
if (insertRoles.length > 0) {
|
|
1561
|
+
const policy = `${tableName}_insert_only`;
|
|
1562
|
+
statements.push(
|
|
1563
|
+
`DROP POLICY IF EXISTS "${policy}" ON "${tableName}"`,
|
|
1564
|
+
`CREATE POLICY "${policy}" ON "${tableName}" FOR INSERT TO ${insertRoles.map((role) => `"${role}"`).join(", ")} WITH CHECK (${INSERT_POLICY_CHECK})`
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
return statements;
|
|
1568
|
+
}
|
|
1569
|
+
function describeSupabaseError(result) {
|
|
1570
|
+
const error = result.error ?? { message: "" };
|
|
1571
|
+
const parts = [];
|
|
1572
|
+
if (error.message) {
|
|
1573
|
+
parts.push(error.message);
|
|
1574
|
+
} else if (result.status) {
|
|
1575
|
+
parts.push(
|
|
1576
|
+
`HTTP ${result.status}${result.statusText ? ` ${result.statusText}` : ""} with no error message (a 404 usually means the table does not exist or is not exposed by the API)`
|
|
1577
|
+
);
|
|
1578
|
+
} else {
|
|
1579
|
+
parts.push("unknown error with no message");
|
|
1580
|
+
}
|
|
1581
|
+
if (error.code) {
|
|
1582
|
+
parts.push(`code ${error.code}`);
|
|
1583
|
+
}
|
|
1584
|
+
const detailsLine = (error.details ?? "").split("\n")[0].trim();
|
|
1585
|
+
if (detailsLine && detailsLine !== error.message) {
|
|
1586
|
+
parts.push(`details: ${detailsLine}`);
|
|
1587
|
+
}
|
|
1588
|
+
if (error.hint) {
|
|
1589
|
+
parts.push(`hint: ${error.hint}`);
|
|
1590
|
+
}
|
|
1591
|
+
return parts.join("; ");
|
|
1592
|
+
}
|
|
1593
|
+
var SupabaseErrorAdapter = class {
|
|
1594
|
+
/** @throws {RangeError} if `options.tableName` is not a plain identifier or `options.timeoutMs` is invalid. */
|
|
1595
|
+
constructor(client, options = {}) {
|
|
1596
|
+
this.client = client;
|
|
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
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
async log(entry) {
|
|
1606
|
+
try {
|
|
1607
|
+
const serialized = serializeEntry(entry);
|
|
1608
|
+
const row = {
|
|
1609
|
+
id: resolveEntryId(entry),
|
|
1610
|
+
code: serialized.code,
|
|
1611
|
+
message: serialized.message,
|
|
1612
|
+
cause: serialized.cause === void 0 ? null : serialized.cause,
|
|
1613
|
+
source: serialized.source ?? null,
|
|
1614
|
+
occurred_at: serialized.timestamp
|
|
1615
|
+
};
|
|
1616
|
+
const result = await withTimeout(
|
|
1617
|
+
() => this.client.from(this.tableName).insert(row),
|
|
1618
|
+
this.timeoutMs,
|
|
1619
|
+
timeoutMessage("Supabase", this.timeoutMs)
|
|
1620
|
+
);
|
|
1621
|
+
if (result.error) {
|
|
1622
|
+
if (hasCallerId(entry) && result.error.code === "23505") {
|
|
1623
|
+
return { ok: true };
|
|
1624
|
+
}
|
|
1625
|
+
return this.fail(result, describeSupabaseError(result));
|
|
1626
|
+
}
|
|
1627
|
+
return { ok: true };
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
return this.fail(error, describeError(error));
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
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
|
+
});
|
|
1030
1640
|
}
|
|
1031
1641
|
};
|
|
1032
1642
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1033
1643
|
0 && (module.exports = {
|
|
1034
1644
|
CompositeErrorAdapter,
|
|
1035
1645
|
ConsoleErrorAdapter,
|
|
1646
|
+
FirestoreErrorAdapter,
|
|
1036
1647
|
FlatFileErrorAdapter,
|
|
1037
1648
|
IndexedDbErrorAdapter,
|
|
1038
1649
|
JsonFileErrorAdapter,
|
|
1039
1650
|
MongoDbErrorAdapter,
|
|
1040
1651
|
MySqlErrorAdapter,
|
|
1041
1652
|
PostgresErrorAdapter,
|
|
1653
|
+
RealtimeDatabaseErrorAdapter,
|
|
1042
1654
|
RedisErrorAdapter,
|
|
1043
1655
|
S3ErrorAdapter,
|
|
1044
1656
|
SqliteErrorAdapter,
|
|
1657
|
+
SupabaseErrorAdapter,
|
|
1045
1658
|
WebStorageErrorAdapter,
|
|
1046
1659
|
WebhookErrorAdapter,
|
|
1047
1660
|
buildCreateErrorLogTableSql,
|
|
1048
1661
|
buildErrorLogTableDdl,
|
|
1662
|
+
buildMySqlWidenColumnsSql,
|
|
1663
|
+
buildSupabaseSetupSql,
|
|
1049
1664
|
createErrorLogStore,
|
|
1050
1665
|
formatErrorLogLine,
|
|
1051
1666
|
serializeCause,
|