triva 0.0.2 → 0.3.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/LICENSE +21 -0
- package/README.md +416 -0
- package/index.d.ts +91 -0
- package/lib/cache.js +112 -0
- package/lib/cookie-parser.js +114 -0
- package/lib/db-adapters.js +580 -0
- package/lib/error-tracker.js +353 -0
- package/lib/index.js +655 -0
- package/lib/log.js +261 -0
- package/lib/middleware.js +237 -0
- package/lib/ua-parser.js +130 -0
- package/package.json +29 -8
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Triva - Error Tracking System
|
|
3
|
+
* Copyright (c) 2026 Kris Powers
|
|
4
|
+
* License MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import { parseUA } from './ua-parser.js';
|
|
10
|
+
|
|
11
|
+
/* ---------------- Error Entry Structure ---------------- */
|
|
12
|
+
class ErrorEntry {
|
|
13
|
+
constructor(error, context = {}) {
|
|
14
|
+
this.id = this._generateId();
|
|
15
|
+
this.timestamp = Date.now();
|
|
16
|
+
this.datetime = new Date().toISOString();
|
|
17
|
+
|
|
18
|
+
// Error details
|
|
19
|
+
this.error = {
|
|
20
|
+
name: error.name || 'Error',
|
|
21
|
+
message: error.message || 'Unknown error',
|
|
22
|
+
stack: error.stack || null,
|
|
23
|
+
code: error.code || null,
|
|
24
|
+
type: this._classifyError(error)
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Request context (if available)
|
|
28
|
+
this.request = {
|
|
29
|
+
method: context.req?.method || null,
|
|
30
|
+
url: context.req?.url || null,
|
|
31
|
+
pathname: context.pathname || null,
|
|
32
|
+
ip: context.req?.socket?.remoteAddress || context.req?.connection?.remoteAddress || null,
|
|
33
|
+
userAgent: context.req?.headers?['user-agent'] || null,
|
|
34
|
+
headers: context.req ? this._sanitizeHeaders(context.req.headers) : null
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// User agent data (if available)
|
|
38
|
+
this.uaData = context.uaData || null;
|
|
39
|
+
|
|
40
|
+
// Additional context
|
|
41
|
+
this.context = {
|
|
42
|
+
route: context.route || null,
|
|
43
|
+
handler: context.handler || null,
|
|
44
|
+
phase: context.phase || null, // 'middleware', 'route', 'error-handler', etc.
|
|
45
|
+
custom: context.custom || {}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// System info
|
|
49
|
+
this.system = {
|
|
50
|
+
nodeVersion: process.version,
|
|
51
|
+
platform: process.platform,
|
|
52
|
+
memory: process.memoryUsage(),
|
|
53
|
+
uptime: process.uptime()
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Error severity
|
|
57
|
+
this.severity = this._determineSeverity(error, context);
|
|
58
|
+
|
|
59
|
+
// Resolution status
|
|
60
|
+
this.status = 'unresolved';
|
|
61
|
+
this.resolved = false;
|
|
62
|
+
this.resolvedAt = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_generateId() {
|
|
66
|
+
return `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_sanitizeHeaders(headers) {
|
|
70
|
+
const sanitized = { ...headers };
|
|
71
|
+
// Remove sensitive headers
|
|
72
|
+
const sensitive = ['authorization', 'cookie', 'x-api-key', 'x-auth-token'];
|
|
73
|
+
sensitive.forEach(key => {
|
|
74
|
+
if (sanitized[key]) {
|
|
75
|
+
sanitized[key] = '[REDACTED]';
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return sanitized;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_classifyError(error) {
|
|
82
|
+
if (error.name === 'TypeError') return 'type-error';
|
|
83
|
+
if (error.name === 'ReferenceError') return 'reference-error';
|
|
84
|
+
if (error.name === 'SyntaxError') return 'syntax-error';
|
|
85
|
+
if (error.code === 'ENOENT') return 'file-not-found';
|
|
86
|
+
if (error.code === 'ECONNREFUSED') return 'connection-refused';
|
|
87
|
+
if (error.code === 'ETIMEDOUT') return 'timeout';
|
|
88
|
+
if (error.message?.includes('JSON')) return 'json-parse-error';
|
|
89
|
+
if (error.message?.includes('permission')) return 'permission-error';
|
|
90
|
+
return 'general';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_determineSeverity(error, context) {
|
|
94
|
+
// Critical: System errors, crashes
|
|
95
|
+
if (error.name === 'Error' && error.message.includes('FATAL')) return 'critical';
|
|
96
|
+
if (error.code === 'ERR_OUT_OF_MEMORY') return 'critical';
|
|
97
|
+
|
|
98
|
+
// High: Unhandled errors, type errors in handlers
|
|
99
|
+
if (context.phase === 'uncaught') return 'high';
|
|
100
|
+
if (error.name === 'TypeError' || error.name === 'ReferenceError') return 'high';
|
|
101
|
+
|
|
102
|
+
// Medium: Route handler errors, middleware errors
|
|
103
|
+
if (context.phase === 'route' || context.phase === 'middleware') return 'medium';
|
|
104
|
+
|
|
105
|
+
// Low: Validation errors, expected errors
|
|
106
|
+
if (error.name === 'ValidationError') return 'low';
|
|
107
|
+
if (error.message?.includes('Invalid')) return 'low';
|
|
108
|
+
|
|
109
|
+
return 'medium';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
markResolved() {
|
|
113
|
+
this.resolved = true;
|
|
114
|
+
this.status = 'resolved';
|
|
115
|
+
this.resolvedAt = new Date().toISOString();
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ---------------- Error Storage & Analytics ---------------- */
|
|
121
|
+
class ErrorTracker {
|
|
122
|
+
constructor() {
|
|
123
|
+
this.errors = [];
|
|
124
|
+
this.config = {
|
|
125
|
+
enabled: true,
|
|
126
|
+
maxEntries: 10000,
|
|
127
|
+
captureStackTrace: true,
|
|
128
|
+
captureContext: true,
|
|
129
|
+
captureSystemInfo: true
|
|
130
|
+
};
|
|
131
|
+
this.stats = {
|
|
132
|
+
total: 0,
|
|
133
|
+
bySeverity: {
|
|
134
|
+
critical: 0,
|
|
135
|
+
high: 0,
|
|
136
|
+
medium: 0,
|
|
137
|
+
low: 0
|
|
138
|
+
},
|
|
139
|
+
byType: {},
|
|
140
|
+
byPhase: {},
|
|
141
|
+
resolved: 0,
|
|
142
|
+
unresolved: 0
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Hook into process-level error handlers
|
|
146
|
+
this._setupGlobalHandlers();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
configure(options = {}) {
|
|
150
|
+
this.config = {
|
|
151
|
+
...this.config,
|
|
152
|
+
enabled: options.enabled !== false,
|
|
153
|
+
maxEntries: options.maxEntries || this.config.maxEntries,
|
|
154
|
+
captureStackTrace: options.captureStackTrace !== false,
|
|
155
|
+
captureContext: options.captureContext !== false,
|
|
156
|
+
captureSystemInfo: options.captureSystemInfo !== false
|
|
157
|
+
};
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async capture(error, context = {}) {
|
|
162
|
+
if (!this.config.enabled) return null;
|
|
163
|
+
|
|
164
|
+
// Parse UA if available and not already parsed
|
|
165
|
+
if (context.req && !context.uaData) {
|
|
166
|
+
const ua = context.req.headers?.['user-agent'];
|
|
167
|
+
if (ua) {
|
|
168
|
+
try {
|
|
169
|
+
context.uaData = await parseUA(ua);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
// Silently fail UA parsing
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const entry = new ErrorEntry(error, context);
|
|
177
|
+
|
|
178
|
+
// Update stats
|
|
179
|
+
this.stats.total++;
|
|
180
|
+
this.stats.bySeverity[entry.severity]++;
|
|
181
|
+
this.stats.byType[entry.error.type] = (this.stats.byType[entry.error.type] || 0) + 1;
|
|
182
|
+
if (entry.context.phase) {
|
|
183
|
+
this.stats.byPhase[entry.context.phase] = (this.stats.byPhase[entry.context.phase] || 0) + 1;
|
|
184
|
+
}
|
|
185
|
+
this.stats.unresolved++;
|
|
186
|
+
|
|
187
|
+
// Add to storage
|
|
188
|
+
this.errors.push(entry);
|
|
189
|
+
this._enforceRetention();
|
|
190
|
+
|
|
191
|
+
// Log to console in development
|
|
192
|
+
if (process.env.NODE_ENV === 'development') {
|
|
193
|
+
console.error(`[Error Tracker] ${entry.severity.toUpperCase()}: ${entry.error.message}`);
|
|
194
|
+
if (entry.request.url) {
|
|
195
|
+
console.error(` Request: ${entry.request.method} ${entry.request.url}`);
|
|
196
|
+
}
|
|
197
|
+
if (this.config.captureStackTrace && entry.error.stack) {
|
|
198
|
+
console.error(` Stack: ${entry.error.stack.split('\n')[1]?.trim()}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return entry;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_enforceRetention() {
|
|
206
|
+
if (this.errors.length > this.config.maxEntries) {
|
|
207
|
+
const toRemove = this.errors.length - this.config.maxEntries;
|
|
208
|
+
this.errors.splice(0, toRemove);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async get(filter = {}) {
|
|
213
|
+
if (filter === 'all') {
|
|
214
|
+
return this.errors;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let results = [...this.errors];
|
|
218
|
+
|
|
219
|
+
// Filter by severity
|
|
220
|
+
if (filter.severity) {
|
|
221
|
+
const severities = Array.isArray(filter.severity) ? filter.severity : [filter.severity];
|
|
222
|
+
results = results.filter(e => severities.includes(e.severity));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Filter by type
|
|
226
|
+
if (filter.type) {
|
|
227
|
+
const types = Array.isArray(filter.type) ? filter.type : [filter.type];
|
|
228
|
+
results = results.filter(e => types.includes(e.error.type));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Filter by phase
|
|
232
|
+
if (filter.phase) {
|
|
233
|
+
results = results.filter(e => e.context.phase === filter.phase);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Filter by resolved status
|
|
237
|
+
if (filter.resolved !== undefined) {
|
|
238
|
+
results = results.filter(e => e.resolved === filter.resolved);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Filter by time range
|
|
242
|
+
if (filter.from) {
|
|
243
|
+
const fromTime = typeof filter.from === 'number' ? filter.from : new Date(filter.from).getTime();
|
|
244
|
+
results = results.filter(e => e.timestamp >= fromTime);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (filter.to) {
|
|
248
|
+
const toTime = typeof filter.to === 'number' ? filter.to : new Date(filter.to).getTime();
|
|
249
|
+
results = results.filter(e => e.timestamp <= toTime);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Search in error messages
|
|
253
|
+
if (filter.search) {
|
|
254
|
+
const search = filter.search.toLowerCase();
|
|
255
|
+
results = results.filter(e =>
|
|
256
|
+
e.error.message.toLowerCase().includes(search) ||
|
|
257
|
+
e.error.name.toLowerCase().includes(search) ||
|
|
258
|
+
e.request.url?.toLowerCase().includes(search)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Limit results
|
|
263
|
+
if (filter.limit) {
|
|
264
|
+
results = results.slice(-filter.limit);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return results;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async getById(id) {
|
|
271
|
+
return this.errors.find(e => e.id === id);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async resolve(id) {
|
|
275
|
+
const error = await this.getById(id);
|
|
276
|
+
if (error && !error.resolved) {
|
|
277
|
+
error.markResolved();
|
|
278
|
+
this.stats.resolved++;
|
|
279
|
+
this.stats.unresolved--;
|
|
280
|
+
return error;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async getStats() {
|
|
286
|
+
const recent = this.errors.slice(-1000);
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
total: this.stats.total,
|
|
290
|
+
stored: this.errors.length,
|
|
291
|
+
severity: this.stats.bySeverity,
|
|
292
|
+
types: this.stats.byType,
|
|
293
|
+
phases: this.stats.byPhase,
|
|
294
|
+
resolved: this.stats.resolved,
|
|
295
|
+
unresolved: this.stats.unresolved,
|
|
296
|
+
recent: {
|
|
297
|
+
count: recent.length,
|
|
298
|
+
critical: recent.filter(e => e.severity === 'critical').length,
|
|
299
|
+
high: recent.filter(e => e.severity === 'high').length,
|
|
300
|
+
medium: recent.filter(e => e.severity === 'medium').length,
|
|
301
|
+
low: recent.filter(e => e.severity === 'low').length
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async clear() {
|
|
307
|
+
const count = this.errors.length;
|
|
308
|
+
this.errors = [];
|
|
309
|
+
this.stats = {
|
|
310
|
+
total: this.stats.total, // Keep total count
|
|
311
|
+
bySeverity: { critical: 0, high: 0, medium: 0, low: 0 },
|
|
312
|
+
byType: {},
|
|
313
|
+
byPhase: {},
|
|
314
|
+
resolved: 0,
|
|
315
|
+
unresolved: 0
|
|
316
|
+
};
|
|
317
|
+
return { cleared: count };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
_setupGlobalHandlers() {
|
|
321
|
+
// Capture uncaught exceptions
|
|
322
|
+
process.on('uncaughtException', (error, origin) => {
|
|
323
|
+
this.capture(error, {
|
|
324
|
+
phase: 'uncaught',
|
|
325
|
+
custom: { origin }
|
|
326
|
+
}).catch(() => {}); // Silently fail to prevent recursion
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Capture unhandled promise rejections
|
|
330
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
331
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
332
|
+
this.capture(error, {
|
|
333
|
+
phase: 'unhandled-rejection',
|
|
334
|
+
custom: { promise: String(promise) }
|
|
335
|
+
}).catch(() => {});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Capture warnings
|
|
339
|
+
process.on('warning', (warning) => {
|
|
340
|
+
if (warning.name !== 'DeprecationWarning') { // Skip deprecation warnings
|
|
341
|
+
this.capture(warning, {
|
|
342
|
+
phase: 'warning',
|
|
343
|
+
custom: { warningType: warning.name }
|
|
344
|
+
}).catch(() => {});
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* ---------------- Export Singleton ---------------- */
|
|
351
|
+
const errorTracker = new ErrorTracker();
|
|
352
|
+
|
|
353
|
+
export { errorTracker, ErrorEntry };
|