triva 0.0.2 → 0.3.1

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.
@@ -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 };