@usequark/quark-core 2.5.4
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 +63 -0
- package/package.json +80 -0
- package/src/admin-auth.js +121 -0
- package/src/admin.js +352 -0
- package/src/admin.test.js +257 -0
- package/src/auth/index.js +160 -0
- package/src/auth/middleware.js +106 -0
- package/src/auth/middleware.test.js +116 -0
- package/src/auth/password.js +9 -0
- package/src/auth.test.js +235 -0
- package/src/authorization.js +235 -0
- package/src/authorization.test.js +314 -0
- package/src/cache.js +137 -0
- package/src/cache.test.js +217 -0
- package/src/core.js +32 -0
- package/src/csrf.js +122 -0
- package/src/csrf.test.js +169 -0
- package/src/db-instrumentation.js +281 -0
- package/src/db-instrumentation.test.js +284 -0
- package/src/db-metrics.js +26 -0
- package/src/db.js +218 -0
- package/src/db.test.js +365 -0
- package/src/email-templates.js +134 -0
- package/src/email-templates.test.js +124 -0
- package/src/email.js +314 -0
- package/src/email.test.js +498 -0
- package/src/error-reporter.js +266 -0
- package/src/error-reporter.test.js +236 -0
- package/src/errors.js +192 -0
- package/src/errors.test.js +128 -0
- package/src/exports.test.js +154 -0
- package/src/file-validation.js +249 -0
- package/src/file-validation.test.js +207 -0
- package/src/index.js +58 -0
- package/src/locale.js +21 -0
- package/src/logger.js +182 -0
- package/src/logger.test.js +287 -0
- package/src/mail.js +50 -0
- package/src/metrics.js +379 -0
- package/src/metrics.test.js +380 -0
- package/src/multipart.js +144 -0
- package/src/multipart.test.js +188 -0
- package/src/pagination.js +139 -0
- package/src/pagination.test.js +210 -0
- package/src/query-builder.js +183 -0
- package/src/query-builder.test.js +234 -0
- package/src/queue/index.js +446 -0
- package/src/queue/queue.test.js +365 -0
- package/src/rate-limiter.js +253 -0
- package/src/rate-limiter.test.js +130 -0
- package/src/redis.js +154 -0
- package/src/redis.test.js +150 -0
- package/src/request-logger.js +211 -0
- package/src/request-logger.test.js +286 -0
- package/src/sms.js +208 -0
- package/src/storage-s3.js +236 -0
- package/src/storage.js +214 -0
- package/src/storage.test.js +457 -0
- package/src/stripe.js +89 -0
- package/src/testing/factories.js +89 -0
- package/src/testing/helpers.js +266 -0
- package/src/testing/index.js +46 -0
- package/src/testing/mocks.js +480 -0
- package/src/testing/testing.test.js +543 -0
- package/src/types.js +74 -0
- package/src/utils.js +219 -0
- package/src/utils.test.js +193 -0
- package/src/validation.js +26 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @techstream/quark-core - Error Reporter Module
|
|
3
|
+
* Adapter/hook system for pluggable error reporting
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { normalizeError } from "./errors.js";
|
|
7
|
+
import { logger } from "./logger.js";
|
|
8
|
+
|
|
9
|
+
const MAX_BREADCRUMBS = 50;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} ErrorAdapter
|
|
13
|
+
* @property {string} name - Unique adapter identifier
|
|
14
|
+
* @property {(error: Error, context: Object) => void} report - Called when an error is reported
|
|
15
|
+
* @property {(message: string, level: string, context: Object) => void} [captureMessage] - For non-error events
|
|
16
|
+
* @property {(user: Object) => void} [setUser] - Sets user context
|
|
17
|
+
* @property {(message: string, category: string, data: Object) => void} [addBreadcrumb] - Adds a breadcrumb
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} Breadcrumb
|
|
22
|
+
* @property {string} message - Breadcrumb message
|
|
23
|
+
* @property {string} category - Breadcrumb category
|
|
24
|
+
* @property {Object} data - Associated data
|
|
25
|
+
* @property {string} timestamp - ISO timestamp
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Built-in adapter that logs errors via the Quark logger.
|
|
30
|
+
* @type {ErrorAdapter}
|
|
31
|
+
*/
|
|
32
|
+
export const consoleAdapter = {
|
|
33
|
+
name: "console",
|
|
34
|
+
|
|
35
|
+
report(error, context) {
|
|
36
|
+
const appError = normalizeError(error);
|
|
37
|
+
logger.error(appError.message, {
|
|
38
|
+
error: appError.toJSON(),
|
|
39
|
+
context,
|
|
40
|
+
stack: error.stack,
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
captureMessage(message, level = "info", context = {}) {
|
|
45
|
+
const logFn = logger[level] || logger.info;
|
|
46
|
+
logFn.call(logger, message, context);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
setUser(_user) {
|
|
50
|
+
// Console adapter does not persist user context
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
addBreadcrumb(_message, _category, _data) {
|
|
54
|
+
// Console adapter does not store breadcrumbs
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Error reporter with adapter registration.
|
|
60
|
+
* Distributes error reports to all registered adapters.
|
|
61
|
+
*/
|
|
62
|
+
export class ErrorReporter {
|
|
63
|
+
/** @type {ErrorAdapter[]} */
|
|
64
|
+
#adapters = [];
|
|
65
|
+
|
|
66
|
+
/** @type {Breadcrumb[]} */
|
|
67
|
+
#breadcrumbs = [];
|
|
68
|
+
|
|
69
|
+
/** @type {Object|null} */
|
|
70
|
+
#user = null;
|
|
71
|
+
|
|
72
|
+
constructor() {
|
|
73
|
+
this.#adapters = [consoleAdapter];
|
|
74
|
+
this.#breadcrumbs = [];
|
|
75
|
+
this.#user = null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Register an adapter
|
|
80
|
+
* @param {ErrorAdapter} adapter - The adapter to register
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
use(adapter) {
|
|
84
|
+
if (
|
|
85
|
+
!adapter ||
|
|
86
|
+
typeof adapter.name !== "string" ||
|
|
87
|
+
typeof adapter.report !== "function"
|
|
88
|
+
) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"Adapter must have a name (string) and a report (function)",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
this.#adapters.push(adapter);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Report an error to all registered adapters
|
|
98
|
+
* @param {Error} error - The error to report
|
|
99
|
+
* @param {Object} [context={}] - Additional context
|
|
100
|
+
* @returns {void}
|
|
101
|
+
*/
|
|
102
|
+
report(error, context = {}) {
|
|
103
|
+
const enrichedContext = {
|
|
104
|
+
...context,
|
|
105
|
+
...(this.#user ? { user: this.#user } : {}),
|
|
106
|
+
breadcrumbs: [...this.#breadcrumbs],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
for (const adapter of this.#adapters) {
|
|
110
|
+
try {
|
|
111
|
+
adapter.report(error, enrichedContext);
|
|
112
|
+
} catch (_adapterError) {
|
|
113
|
+
// Swallow adapter errors to avoid cascading failures
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Capture a non-error message event
|
|
120
|
+
* @param {string} message - The message to capture
|
|
121
|
+
* @param {string} [level="info"] - Log level
|
|
122
|
+
* @param {Object} [context={}] - Additional context
|
|
123
|
+
* @returns {void}
|
|
124
|
+
*/
|
|
125
|
+
captureMessage(message, level = "info", context = {}) {
|
|
126
|
+
const enrichedContext = {
|
|
127
|
+
...context,
|
|
128
|
+
...(this.#user ? { user: this.#user } : {}),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
for (const adapter of this.#adapters) {
|
|
132
|
+
try {
|
|
133
|
+
if (typeof adapter.captureMessage === "function") {
|
|
134
|
+
adapter.captureMessage(message, level, enrichedContext);
|
|
135
|
+
}
|
|
136
|
+
} catch (_adapterError) {
|
|
137
|
+
// Swallow adapter errors
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Set user context for subsequent reports
|
|
144
|
+
* @param {Object} user - User information
|
|
145
|
+
* @returns {void}
|
|
146
|
+
*/
|
|
147
|
+
setUser(user) {
|
|
148
|
+
this.#user = user;
|
|
149
|
+
|
|
150
|
+
for (const adapter of this.#adapters) {
|
|
151
|
+
try {
|
|
152
|
+
if (typeof adapter.setUser === "function") {
|
|
153
|
+
adapter.setUser(user);
|
|
154
|
+
}
|
|
155
|
+
} catch (_adapterError) {
|
|
156
|
+
// Swallow adapter errors
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Add a breadcrumb to the trail (circular buffer, max 50)
|
|
163
|
+
* @param {string} message - Breadcrumb message
|
|
164
|
+
* @param {string} [category="default"] - Breadcrumb category
|
|
165
|
+
* @param {Object} [data={}] - Associated data
|
|
166
|
+
* @returns {void}
|
|
167
|
+
*/
|
|
168
|
+
addBreadcrumb(message, category = "default", data = {}) {
|
|
169
|
+
/** @type {Breadcrumb} */
|
|
170
|
+
const breadcrumb = {
|
|
171
|
+
message,
|
|
172
|
+
category,
|
|
173
|
+
data,
|
|
174
|
+
timestamp: new Date().toISOString(),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (this.#breadcrumbs.length >= MAX_BREADCRUMBS) {
|
|
178
|
+
this.#breadcrumbs.shift();
|
|
179
|
+
}
|
|
180
|
+
this.#breadcrumbs.push(breadcrumb);
|
|
181
|
+
|
|
182
|
+
for (const adapter of this.#adapters) {
|
|
183
|
+
try {
|
|
184
|
+
if (typeof adapter.addBreadcrumb === "function") {
|
|
185
|
+
adapter.addBreadcrumb(message, category, data);
|
|
186
|
+
}
|
|
187
|
+
} catch (_adapterError) {
|
|
188
|
+
// Swallow adapter errors
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Returns the list of registered adapters (for inspection/testing)
|
|
195
|
+
* @returns {ErrorAdapter[]}
|
|
196
|
+
*/
|
|
197
|
+
get adapters() {
|
|
198
|
+
return [...this.#adapters];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Returns the current breadcrumbs (for inspection/testing)
|
|
203
|
+
* @returns {Breadcrumb[]}
|
|
204
|
+
*/
|
|
205
|
+
get breadcrumbs() {
|
|
206
|
+
return [...this.#breadcrumbs];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Creates a Sentry adapter factory (stub/example).
|
|
212
|
+
* Downstream projects can use this as a template to integrate Sentry.
|
|
213
|
+
*
|
|
214
|
+
* @param {Object} _options - Sentry configuration options
|
|
215
|
+
* @returns {ErrorAdapter} A Sentry adapter (stubbed)
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```js
|
|
219
|
+
* import * as Sentry from "@sentry/node";
|
|
220
|
+
* import { errorReporter, createSentryAdapter } from "@techstream/quark-core";
|
|
221
|
+
*
|
|
222
|
+
* Sentry.init({ dsn: "https://example@sentry.io/123" });
|
|
223
|
+
* errorReporter.use(createSentryAdapter({ Sentry }));
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export const createSentryAdapter = (_options = {}) => {
|
|
227
|
+
return {
|
|
228
|
+
name: "sentry",
|
|
229
|
+
|
|
230
|
+
report(_error, _context) {
|
|
231
|
+
// Example Sentry integration:
|
|
232
|
+
// const { Sentry } = options;
|
|
233
|
+
// Sentry.withScope((scope) => {
|
|
234
|
+
// if (context.user) scope.setUser(context.user);
|
|
235
|
+
// if (context.breadcrumbs) {
|
|
236
|
+
// for (const b of context.breadcrumbs) {
|
|
237
|
+
// Sentry.addBreadcrumb({ message: b.message, category: b.category, data: b.data });
|
|
238
|
+
// }
|
|
239
|
+
// }
|
|
240
|
+
// scope.setExtras(context);
|
|
241
|
+
// Sentry.captureException(error);
|
|
242
|
+
// });
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
captureMessage(_message, _level, _context) {
|
|
246
|
+
// Example:
|
|
247
|
+
// const { Sentry } = options;
|
|
248
|
+
// Sentry.captureMessage(message, level);
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
setUser(_user) {
|
|
252
|
+
// Example:
|
|
253
|
+
// const { Sentry } = options;
|
|
254
|
+
// Sentry.setUser(user);
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
addBreadcrumb(_message, _category, _data) {
|
|
258
|
+
// Example:
|
|
259
|
+
// const { Sentry } = options;
|
|
260
|
+
// Sentry.addBreadcrumb({ message, category, data });
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/** Global singleton error reporter instance */
|
|
266
|
+
export const errorReporter = new ErrorReporter();
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { beforeEach, describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
createSentryAdapter,
|
|
5
|
+
ErrorReporter,
|
|
6
|
+
errorReporter,
|
|
7
|
+
} from "./error-reporter.js";
|
|
8
|
+
|
|
9
|
+
describe("ErrorReporter", () => {
|
|
10
|
+
/** @type {ErrorReporter} */
|
|
11
|
+
let reporter;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
reporter = new ErrorReporter();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("default instance", () => {
|
|
18
|
+
it("has console adapter registered by default", () => {
|
|
19
|
+
assert.equal(reporter.adapters.length, 1);
|
|
20
|
+
assert.equal(reporter.adapters[0].name, "console");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("global singleton has console adapter", () => {
|
|
24
|
+
const names = errorReporter.adapters.map((a) => a.name);
|
|
25
|
+
assert.ok(names.includes("console"));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("use()", () => {
|
|
30
|
+
it("adds an adapter", () => {
|
|
31
|
+
const adapter = { name: "test", report: () => {} };
|
|
32
|
+
reporter.use(adapter);
|
|
33
|
+
assert.equal(reporter.adapters.length, 2);
|
|
34
|
+
assert.equal(reporter.adapters[1].name, "test");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("throws if adapter has no name", () => {
|
|
38
|
+
assert.throws(() => reporter.use({ report: () => {} }), /name/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("throws if adapter has no report function", () => {
|
|
42
|
+
assert.throws(() => reporter.use({ name: "bad" }), /report/);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("report()", () => {
|
|
47
|
+
it("calls all registered adapters", () => {
|
|
48
|
+
const calls = [];
|
|
49
|
+
const adapter1 = {
|
|
50
|
+
name: "a1",
|
|
51
|
+
report: (err, ctx) => calls.push({ adapter: "a1", err, ctx }),
|
|
52
|
+
};
|
|
53
|
+
const adapter2 = {
|
|
54
|
+
name: "a2",
|
|
55
|
+
report: (err, ctx) => calls.push({ adapter: "a2", err, ctx }),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
reporter.use(adapter1);
|
|
59
|
+
reporter.use(adapter2);
|
|
60
|
+
|
|
61
|
+
const error = new Error("test error");
|
|
62
|
+
reporter.report(error, { requestId: "123" });
|
|
63
|
+
|
|
64
|
+
// console adapter + 2 custom = 3 calls
|
|
65
|
+
assert.equal(calls.length, 2);
|
|
66
|
+
assert.equal(calls[0].adapter, "a1");
|
|
67
|
+
assert.equal(calls[1].adapter, "a2");
|
|
68
|
+
assert.equal(calls[0].err, error);
|
|
69
|
+
assert.equal(calls[0].ctx.requestId, "123");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("swallows adapter errors without throwing", () => {
|
|
73
|
+
const failAdapter = {
|
|
74
|
+
name: "fail",
|
|
75
|
+
report: () => {
|
|
76
|
+
throw new Error("adapter crash");
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
reporter.use(failAdapter);
|
|
80
|
+
|
|
81
|
+
assert.doesNotThrow(() => reporter.report(new Error("test")));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("captureMessage()", () => {
|
|
86
|
+
it("calls adapters that support captureMessage", () => {
|
|
87
|
+
const messages = [];
|
|
88
|
+
const adapter = {
|
|
89
|
+
name: "msg",
|
|
90
|
+
report: () => {},
|
|
91
|
+
captureMessage: (msg, level, ctx) => messages.push({ msg, level, ctx }),
|
|
92
|
+
};
|
|
93
|
+
reporter.use(adapter);
|
|
94
|
+
|
|
95
|
+
reporter.captureMessage("hello", "warn", { extra: true });
|
|
96
|
+
|
|
97
|
+
assert.equal(messages.length, 1);
|
|
98
|
+
assert.equal(messages[0].msg, "hello");
|
|
99
|
+
assert.equal(messages[0].level, "warn");
|
|
100
|
+
assert.equal(messages[0].ctx.extra, true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("skips adapters without captureMessage", () => {
|
|
104
|
+
const adapter = { name: "no-msg", report: () => {} };
|
|
105
|
+
reporter.use(adapter);
|
|
106
|
+
|
|
107
|
+
assert.doesNotThrow(() => reporter.captureMessage("hello"));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("setUser()", () => {
|
|
112
|
+
it("includes user in subsequent reports", () => {
|
|
113
|
+
const reported = [];
|
|
114
|
+
const adapter = {
|
|
115
|
+
name: "user-test",
|
|
116
|
+
report: (_err, ctx) => reported.push(ctx),
|
|
117
|
+
};
|
|
118
|
+
reporter.use(adapter);
|
|
119
|
+
|
|
120
|
+
reporter.setUser({ id: "u1", email: "a@b.com" });
|
|
121
|
+
reporter.report(new Error("test"));
|
|
122
|
+
|
|
123
|
+
assert.equal(reported.length, 1);
|
|
124
|
+
assert.deepEqual(reported[0].user, { id: "u1", email: "a@b.com" });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("forwards user to adapters with setUser", () => {
|
|
128
|
+
let receivedUser = null;
|
|
129
|
+
const adapter = {
|
|
130
|
+
name: "user-fwd",
|
|
131
|
+
report: () => {},
|
|
132
|
+
setUser: (u) => {
|
|
133
|
+
receivedUser = u;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
reporter.use(adapter);
|
|
137
|
+
|
|
138
|
+
reporter.setUser({ id: "u2" });
|
|
139
|
+
assert.deepEqual(receivedUser, { id: "u2" });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("addBreadcrumb()", () => {
|
|
144
|
+
it("stores breadcrumbs and attaches them to report context", () => {
|
|
145
|
+
const reported = [];
|
|
146
|
+
const adapter = {
|
|
147
|
+
name: "bc-test",
|
|
148
|
+
report: (_err, ctx) => reported.push(ctx),
|
|
149
|
+
};
|
|
150
|
+
reporter.use(adapter);
|
|
151
|
+
|
|
152
|
+
reporter.addBreadcrumb("clicked button", "ui", { buttonId: "submit" });
|
|
153
|
+
reporter.addBreadcrumb("navigated", "navigation");
|
|
154
|
+
reporter.report(new Error("test"));
|
|
155
|
+
|
|
156
|
+
assert.equal(reported.length, 1);
|
|
157
|
+
assert.equal(reported[0].breadcrumbs.length, 2);
|
|
158
|
+
assert.equal(reported[0].breadcrumbs[0].message, "clicked button");
|
|
159
|
+
assert.equal(reported[0].breadcrumbs[0].category, "ui");
|
|
160
|
+
assert.deepEqual(reported[0].breadcrumbs[0].data, { buttonId: "submit" });
|
|
161
|
+
assert.equal(reported[0].breadcrumbs[1].message, "navigated");
|
|
162
|
+
assert.equal(reported[0].breadcrumbs[1].category, "navigation");
|
|
163
|
+
assert.ok(reported[0].breadcrumbs[0].timestamp);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("circular buffer keeps max 50 breadcrumbs", () => {
|
|
167
|
+
for (let i = 0; i < 60; i++) {
|
|
168
|
+
reporter.addBreadcrumb(`crumb-${i}`, "test");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const crumbs = reporter.breadcrumbs;
|
|
172
|
+
assert.equal(crumbs.length, 50);
|
|
173
|
+
// First 10 should have been evicted, first remaining is crumb-10
|
|
174
|
+
assert.equal(crumbs[0].message, "crumb-10");
|
|
175
|
+
assert.equal(crumbs[49].message, "crumb-59");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("forwards breadcrumbs to adapters that support it", () => {
|
|
179
|
+
const received = [];
|
|
180
|
+
const adapter = {
|
|
181
|
+
name: "bc-fwd",
|
|
182
|
+
report: () => {},
|
|
183
|
+
addBreadcrumb: (msg, cat, data) => received.push({ msg, cat, data }),
|
|
184
|
+
};
|
|
185
|
+
reporter.use(adapter);
|
|
186
|
+
|
|
187
|
+
reporter.addBreadcrumb("test", "cat", { key: "val" });
|
|
188
|
+
|
|
189
|
+
assert.equal(received.length, 1);
|
|
190
|
+
assert.equal(received[0].msg, "test");
|
|
191
|
+
assert.equal(received[0].cat, "cat");
|
|
192
|
+
assert.deepEqual(received[0].data, { key: "val" });
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("custom adapter receives correct data", () => {
|
|
197
|
+
it("passes error and enriched context", () => {
|
|
198
|
+
const reported = [];
|
|
199
|
+
const adapter = {
|
|
200
|
+
name: "custom",
|
|
201
|
+
report: (err, ctx) => reported.push({ err, ctx }),
|
|
202
|
+
};
|
|
203
|
+
reporter.use(adapter);
|
|
204
|
+
|
|
205
|
+
reporter.setUser({ id: "u1" });
|
|
206
|
+
reporter.addBreadcrumb("init", "app");
|
|
207
|
+
|
|
208
|
+
const error = new Error("custom test");
|
|
209
|
+
reporter.report(error, { route: "/api/test" });
|
|
210
|
+
|
|
211
|
+
assert.equal(reported.length, 1);
|
|
212
|
+
assert.equal(reported[0].err, error);
|
|
213
|
+
assert.equal(reported[0].ctx.route, "/api/test");
|
|
214
|
+
assert.deepEqual(reported[0].ctx.user, { id: "u1" });
|
|
215
|
+
assert.equal(reported[0].ctx.breadcrumbs.length, 1);
|
|
216
|
+
assert.equal(reported[0].ctx.breadcrumbs[0].message, "init");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("createSentryAdapter()", () => {
|
|
221
|
+
it("returns a valid adapter shape", () => {
|
|
222
|
+
const adapter = createSentryAdapter();
|
|
223
|
+
assert.equal(adapter.name, "sentry");
|
|
224
|
+
assert.equal(typeof adapter.report, "function");
|
|
225
|
+
assert.equal(typeof adapter.captureMessage, "function");
|
|
226
|
+
assert.equal(typeof adapter.setUser, "function");
|
|
227
|
+
assert.equal(typeof adapter.addBreadcrumb, "function");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("can be registered without errors", () => {
|
|
231
|
+
const adapter = createSentryAdapter();
|
|
232
|
+
assert.doesNotThrow(() => reporter.use(adapter));
|
|
233
|
+
assert.doesNotThrow(() => reporter.report(new Error("test")));
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @techstream/quark-core - Error Handling Module
|
|
3
|
+
* Standardized error types and utilities for consistent error handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { errorReporter } from "./error-reporter.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base application error class
|
|
10
|
+
*/
|
|
11
|
+
export class AppError extends Error {
|
|
12
|
+
constructor(message, statusCode = 500, code = "INTERNAL_ERROR") {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = this.constructor.name;
|
|
15
|
+
this.statusCode = statusCode;
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.timestamp = new Date().toISOString();
|
|
18
|
+
Error.captureStackTrace(this, this.constructor);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toJSON() {
|
|
22
|
+
return {
|
|
23
|
+
name: this.name,
|
|
24
|
+
message: this.message,
|
|
25
|
+
code: this.code,
|
|
26
|
+
statusCode: this.statusCode,
|
|
27
|
+
timestamp: this.timestamp,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 400 Bad Request Error
|
|
34
|
+
*/
|
|
35
|
+
export class ValidationError extends AppError {
|
|
36
|
+
constructor(message, details = null) {
|
|
37
|
+
super(message, 400, "VALIDATION_ERROR");
|
|
38
|
+
this.details = details;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
toJSON() {
|
|
42
|
+
return {
|
|
43
|
+
...super.toJSON(),
|
|
44
|
+
details: this.details,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 401 Unauthorized Error
|
|
51
|
+
*/
|
|
52
|
+
export class UnauthorizedError extends AppError {
|
|
53
|
+
constructor(message = "Authentication required") {
|
|
54
|
+
super(message, 401, "UNAUTHORIZED");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 403 Forbidden Error
|
|
60
|
+
*/
|
|
61
|
+
export class ForbiddenError extends AppError {
|
|
62
|
+
constructor(message = "Access denied") {
|
|
63
|
+
super(message, 403, "FORBIDDEN");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 404 Not Found Error
|
|
69
|
+
*/
|
|
70
|
+
export class NotFoundError extends AppError {
|
|
71
|
+
constructor(message = "Resource not found") {
|
|
72
|
+
super(message, 404, "NOT_FOUND");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 409 Conflict Error
|
|
78
|
+
*/
|
|
79
|
+
export class ConflictError extends AppError {
|
|
80
|
+
constructor(message = "Resource conflict") {
|
|
81
|
+
super(message, 409, "CONFLICT");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 429 Rate Limit Error
|
|
87
|
+
*/
|
|
88
|
+
export class RateLimitError extends AppError {
|
|
89
|
+
constructor(message = "Too many requests", retryAfter = 60) {
|
|
90
|
+
super(message, 429, "RATE_LIMIT");
|
|
91
|
+
this.retryAfter = retryAfter;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
toJSON() {
|
|
95
|
+
return {
|
|
96
|
+
...super.toJSON(),
|
|
97
|
+
retryAfter: this.retryAfter,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Database Error
|
|
104
|
+
*/
|
|
105
|
+
export class DatabaseError extends AppError {
|
|
106
|
+
constructor(message = "Database error", originalError = null) {
|
|
107
|
+
super(message, 500, "DATABASE_ERROR");
|
|
108
|
+
this.originalError = originalError;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* External Service Error
|
|
114
|
+
*/
|
|
115
|
+
export class ServiceError extends AppError {
|
|
116
|
+
constructor(serviceName, message = "Service error", statusCode = 502) {
|
|
117
|
+
super(message, statusCode, "SERVICE_ERROR");
|
|
118
|
+
this.serviceName = serviceName;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
toJSON() {
|
|
122
|
+
return {
|
|
123
|
+
...super.toJSON(),
|
|
124
|
+
serviceName: this.serviceName,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Safely extracts error message from various error types
|
|
131
|
+
* @param {Error|string|Object} error - The error to extract from
|
|
132
|
+
* @returns {string} Extracted error message
|
|
133
|
+
*/
|
|
134
|
+
export const getErrorMessage = (error) => {
|
|
135
|
+
if (typeof error === "string") return error;
|
|
136
|
+
if (error instanceof Error) return error.message;
|
|
137
|
+
if (error && typeof error === "object" && error.message) return error.message;
|
|
138
|
+
return "An unknown error occurred";
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Safely extracts status code from error
|
|
143
|
+
* @param {Error|AppError} error - The error to extract from
|
|
144
|
+
* @returns {number} HTTP status code
|
|
145
|
+
*/
|
|
146
|
+
export const getStatusCode = (error) => {
|
|
147
|
+
if (error instanceof AppError) return error.statusCode;
|
|
148
|
+
if (error.statusCode) return error.statusCode;
|
|
149
|
+
return 500;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Converts any error to an AppError instance
|
|
154
|
+
* @param {Error|AppError|any} error - The error to normalize
|
|
155
|
+
* @returns {AppError} Normalized error
|
|
156
|
+
*/
|
|
157
|
+
export const normalizeError = (error) => {
|
|
158
|
+
if (error instanceof AppError) return error;
|
|
159
|
+
if (error instanceof Error) {
|
|
160
|
+
return new AppError(error.message, 500, "INTERNAL_ERROR");
|
|
161
|
+
}
|
|
162
|
+
return new AppError(
|
|
163
|
+
typeof error === "string" ? error : "An unknown error occurred",
|
|
164
|
+
500,
|
|
165
|
+
"INTERNAL_ERROR",
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Logs error with context
|
|
171
|
+
* @param {Error} error - The error to log
|
|
172
|
+
* @param {Object} context - Additional context
|
|
173
|
+
*/
|
|
174
|
+
export const logError = (error, context = {}) => {
|
|
175
|
+
errorReporter.report(error, context);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Wraps async function and handles errors consistently
|
|
180
|
+
* @param {Function} fn - Async function to wrap
|
|
181
|
+
* @returns {Function} Wrapped function
|
|
182
|
+
*/
|
|
183
|
+
export const withErrorHandling = (fn) => {
|
|
184
|
+
return async (...args) => {
|
|
185
|
+
try {
|
|
186
|
+
return await fn(...args);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
errorReporter.report(error);
|
|
189
|
+
throw normalizeError(error);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
};
|