intellerror 1.0.0 → 1.1.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 +73 -71
- package/examples/advanced-usage.ts +28 -0
- package/package.json +3 -2
- package/src/config.ts +11 -5
- package/src/formatter/browser.ts +10 -3
- package/src/formatter/index.ts +84 -9
- package/src/index.ts +2 -2
- package/src/parser/index.ts +68 -4
- package/src/register.ts +61 -19
- package/src/suggestions/index.ts +14 -4
- package/src/suggestions/rules.ts +60 -3
- package/tests/formatter.test.ts +44 -0
- package/tests/parser.test.ts +51 -0
package/README.md
CHANGED
|
@@ -11,74 +11,91 @@
|
|
|
11
11
|
Standard error logs are noisy, cluttered with `node_modules`, and lack context. **IntellError** gives you the "Why" and "How to Fix" instantly.
|
|
12
12
|
|
|
13
13
|
- ❌ **Clean Stack Traces**: Auto-hides Node.js internals and collapses `node_modules` noise.
|
|
14
|
-
- 📍 **
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- 🌐 **Universal
|
|
20
|
-
- 📢 **Team Integration**: Push critical dev errors directly to **Slack** or **Discord** webhooks.
|
|
14
|
+
- 📍 **Source Map Support**: Auto-translates minified/compiled code back to original TypeScript/Source locations.
|
|
15
|
+
- 🔗 **Async Stack Chaining**: Recursively traverses `Error.cause` to show the full async story.
|
|
16
|
+
- 💡 **Actionable Suggestions 2.0**: 100+ context-aware rules with confidence scoring.
|
|
17
|
+
- 📸 **Code Snapshots**: See the exact line of code directly in your terminal (Node.js only).
|
|
18
|
+
- 📊 **Observability Ready**: Includes **JSON Mode**, **Error Fingerprinting**, and **Severity Levels**.
|
|
19
|
+
- 🌐 **Universal Runtime Support**: Native support for **Node.js**, **Bun**, **Deno**, and **Edge/Serverless**.
|
|
21
20
|
|
|
22
21
|
---
|
|
23
22
|
|
|
24
23
|
## 🚀 Quick Start (Zero-Config)
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
Add this at the very top of your entry file to catch all unhandled errors automatically.
|
|
27
26
|
|
|
28
27
|
### For Browser (React, Vite, Next.js)
|
|
29
|
-
Add this at the very top of your entry file (e.g., `main.tsx` or `index.tsx`):
|
|
30
|
-
|
|
31
28
|
```typescript
|
|
32
29
|
import 'intellerror/register';
|
|
33
30
|
```
|
|
34
31
|
|
|
35
|
-
### For Node.js
|
|
36
|
-
Run your application with the register hook:
|
|
37
|
-
|
|
32
|
+
### For Node.js / Bun / Deno
|
|
38
33
|
```bash
|
|
39
|
-
#
|
|
34
|
+
# Node.js 20+
|
|
40
35
|
node --import intellerror/register index.js
|
|
41
36
|
|
|
42
|
-
#
|
|
43
|
-
|
|
37
|
+
# Bun
|
|
38
|
+
bun index.ts # (Auto-detected if you import it)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🛠️ Advanced Features
|
|
44
|
+
|
|
45
|
+
### 🤖 Machine-Readable JSON Mode
|
|
46
|
+
Enable production logging that flows perfectly into Datadog, Splunk, or Loki.
|
|
47
|
+
```typescript
|
|
48
|
+
import { formatErrorJSON } from 'intellerror';
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// your code
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.log(formatErrorJSON(err));
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 🔗 Error Chaining (`Error.cause`)
|
|
58
|
+
Never lose context again. IntellError recursively handles nested errors.
|
|
59
|
+
```text
|
|
60
|
+
ERROR Failed to fetch user from DB
|
|
61
|
+
2026-04-06T06:07:49.123Z | uptime: 12.5s | ID: a1b2c3d4
|
|
62
|
+
|
|
63
|
+
🔗 Causes:
|
|
64
|
+
caused by: Error: Connection refused (timeout after 5000ms)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 📍 Source Map Integration
|
|
68
|
+
If a `.map` file exists alongside your compiled script, IntellError will automatically show the original source location.
|
|
69
|
+
```text
|
|
70
|
+
📍 Location:
|
|
71
|
+
src/users/service.ts:45:10 [mapped] ← YOUR CODE
|
|
44
72
|
```
|
|
45
73
|
|
|
46
74
|
---
|
|
47
75
|
|
|
48
|
-
## ⚙️ Configuration
|
|
76
|
+
## ⚙️ Configuration
|
|
49
77
|
|
|
50
78
|
Fine-tune the output or add your own project-specific intelligence.
|
|
51
79
|
|
|
52
80
|
```typescript
|
|
53
|
-
import { setupConfig
|
|
81
|
+
import { setupConfig } from 'intellerror';
|
|
54
82
|
|
|
55
83
|
setupConfig({
|
|
56
84
|
showNodeModules: false, // Hide noise?
|
|
57
85
|
showNodeInternals: false, // Hide node internals?
|
|
58
86
|
suggestionsEnabled: true, // Show the "💡 Suggestions"?
|
|
59
|
-
|
|
60
|
-
showSearchLinks: true, // Show troubleshooting links?
|
|
87
|
+
sourceMapsEnabled: true, // Enable original source mapping?
|
|
61
88
|
webhookUrl: 'https://...' // Push to Slack/Discord?
|
|
62
89
|
});
|
|
63
|
-
|
|
64
|
-
// Add your own "Thinking" to the engine
|
|
65
|
-
registerRule({
|
|
66
|
-
match: (err) => err.message.includes("AUTH_FAILED"),
|
|
67
|
-
message: "Authentication failure detected.",
|
|
68
|
-
fix: "Ensure your .env contains a valid API_KEY.",
|
|
69
|
-
description: "The request was rejected by the server due to invalid credentials."
|
|
70
|
-
});
|
|
71
90
|
```
|
|
72
91
|
|
|
73
92
|
---
|
|
74
93
|
|
|
75
94
|
## 📖 Manual Usage
|
|
76
95
|
|
|
77
|
-
If you prefer to format specific errors manually in your `try-catch` blocks:
|
|
78
|
-
|
|
79
96
|
### Terminal / Node.js
|
|
80
97
|
```typescript
|
|
81
|
-
import { formatError
|
|
98
|
+
import { formatError } from 'intellerror';
|
|
82
99
|
|
|
83
100
|
try {
|
|
84
101
|
// your code...
|
|
@@ -92,9 +109,9 @@ try {
|
|
|
92
109
|
import { formatErrorBrowser } from 'intellerror';
|
|
93
110
|
|
|
94
111
|
try {
|
|
95
|
-
//
|
|
112
|
+
// ...
|
|
96
113
|
} catch (err) {
|
|
97
|
-
//
|
|
114
|
+
// MUST use the spread operator (...) for CSS styling
|
|
98
115
|
console.log(...formatErrorBrowser(err));
|
|
99
116
|
}
|
|
100
117
|
```
|
|
@@ -109,56 +126,41 @@ import express from 'express';
|
|
|
109
126
|
import { errorFormatter } from 'intellerror';
|
|
110
127
|
|
|
111
128
|
const app = express();
|
|
112
|
-
// ... routes ...
|
|
113
|
-
|
|
114
|
-
// Add at the very end of your middleware stack
|
|
115
129
|
app.use(errorFormatter());
|
|
116
130
|
```
|
|
117
131
|
|
|
118
132
|
---
|
|
119
133
|
|
|
120
|
-
## 🎮 Demos & Examples
|
|
121
|
-
|
|
122
|
-
We've included several ready-to-run examples in the `examples/` directory:
|
|
123
|
-
|
|
124
|
-
- **Basic Error**: `npm run example:basic`
|
|
125
|
-
- **Async Errors**: `npm run example:async`
|
|
126
|
-
- **Custom Errors**: `npm run example:custom`
|
|
127
|
-
- **Run All**: `npm run examples:all`
|
|
128
|
-
|
|
129
|
-
### Run Unit Tests
|
|
130
|
-
```bash
|
|
131
|
-
npm run test:unit
|
|
132
|
-
```
|
|
133
|
-
|
|
134
134
|
---
|
|
135
135
|
|
|
136
|
-
##
|
|
136
|
+
## 🌍 Environment Variables (Zero-Config)
|
|
137
|
+
You can configure IntellError without changing a single line of code using environment variables.
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
| Variable | Default | Description |
|
|
140
|
+
| :--- | :--- | :--- |
|
|
141
|
+
| `INTELLERROR_JSON` | `false` | Enable structured JSON output for all unhandled errors. |
|
|
142
|
+
| `INTELLERROR_SOURCE_MAPS` | `true` | Enable/disable original source mapping via `.map` files. |
|
|
143
|
+
| `INTELLERROR_SUGGESTIONS` | `true` | Show the "💡 Suggestions" section. |
|
|
144
|
+
| `INTELLERROR_SHOW_MODULES` | `false` | Show `node_modules` frames in the stack trace. |
|
|
145
|
+
| `INTELLERROR_SHOW_INTERNALS` | `false` | Show Node.js internal frames in the stack trace. |
|
|
146
|
+
| `INTELLERROR_SEARCH_LINKS` | `true` | Show troubleshooting links. |
|
|
140
147
|
|
|
141
|
-
|
|
142
|
-
src/index.ts:12:15 ← YOUR CODE
|
|
148
|
+
---
|
|
143
149
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
## 🎮 Examples
|
|
151
|
+
Explore the `examples/` directory for ready-to-run demos:
|
|
152
|
+
- `npm run example:basic`
|
|
153
|
+
- `npm run example:async`
|
|
154
|
+
- `npm run example:advanced` (New: Chaining & JSON mode)
|
|
147
155
|
|
|
148
|
-
|
|
149
|
-
• Accessing property on undefined object.
|
|
150
|
-
Fix: Use optional chaining like 'obj?.prop' or ensure the object is initialized.
|
|
156
|
+
---
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
• GitHub: https://github.com/...
|
|
158
|
+
## 📄 License
|
|
159
|
+
MIT © [darshan1005](https://github.com/darshan1005)
|
|
155
160
|
|
|
156
|
-
|
|
157
|
-
→ src/index.ts:12:15
|
|
158
|
-
→ src/server.ts:45:10
|
|
159
|
-
(4 node internals and 2 node_modules hidden)
|
|
160
|
-
```
|
|
161
|
+
---
|
|
161
162
|
|
|
162
|
-
##
|
|
163
|
+
## 🤝 Contributing
|
|
164
|
+
IntellError is open for contributions! Whether it's adding new suggestion rules, improving the parser, or adding more framework integrations, feel free to open a PR or Issue on [GitHub](https://github.com/darshan1005/IntellError).
|
|
163
165
|
|
|
164
|
-
|
|
166
|
+
*Built with humans in mind.*
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { formatError, formatErrorJSON, setupConfig } from '../src/index.js';
|
|
2
|
+
|
|
3
|
+
// Example 1: Async Chaining with Error.cause
|
|
4
|
+
async function databaseCall() {
|
|
5
|
+
throw new Error('Connection refused (timeout after 5000ms)');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function fetchUser() {
|
|
9
|
+
try {
|
|
10
|
+
await databaseCall();
|
|
11
|
+
} catch (err) {
|
|
12
|
+
throw new Error('Failed to fetch user from DB', { cause: err });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
console.log('--- 🔗 Async Chaining Example ---');
|
|
18
|
+
try {
|
|
19
|
+
await fetchUser();
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.log(formatError(err));
|
|
22
|
+
|
|
23
|
+
console.log('\n--- 🤖 Machine-Readable JSON Mode ---');
|
|
24
|
+
console.log(formatErrorJSON(err));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "intellerror",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Intelligent error formatting for Node.js and Browser. Transforms ugly stack traces into clean, readable, and actionable output with smart code suggestions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"example:basic": "tsx examples/basic-usage.ts",
|
|
25
25
|
"example:async": "tsx examples/async-error.ts",
|
|
26
26
|
"example:custom": "tsx examples/custom-error.ts",
|
|
27
|
-
"
|
|
27
|
+
"example:advanced": "tsx examples/advanced-usage.ts",
|
|
28
|
+
"examples:all": "npm run example:basic && npm run example:async && npm run example:custom && npm run example:advanced"
|
|
28
29
|
},
|
|
29
30
|
"repository": {
|
|
30
31
|
"type": "git",
|
package/src/config.ts
CHANGED
|
@@ -4,15 +4,21 @@ export interface IntellErrorConfig {
|
|
|
4
4
|
suggestionsEnabled: boolean;
|
|
5
5
|
interceptWarnings: boolean;
|
|
6
6
|
showSearchLinks: boolean;
|
|
7
|
+
sourceMapsEnabled: boolean;
|
|
8
|
+
useJsonMode: boolean; // NEW: Should registration use JSON formatting by default?
|
|
7
9
|
webhookUrl?: string;
|
|
8
10
|
}
|
|
9
11
|
|
|
12
|
+
const env = typeof process !== 'undefined' ? process.env : {};
|
|
13
|
+
|
|
10
14
|
let config: IntellErrorConfig = {
|
|
11
|
-
showNodeModules:
|
|
12
|
-
showNodeInternals:
|
|
13
|
-
suggestionsEnabled:
|
|
14
|
-
interceptWarnings:
|
|
15
|
-
showSearchLinks:
|
|
15
|
+
showNodeModules: env.INTELLERROR_SHOW_MODULES === 'true',
|
|
16
|
+
showNodeInternals: env.INTELLERROR_SHOW_INTERNALS === 'true',
|
|
17
|
+
suggestionsEnabled: env.INTELLERROR_SUGGESTIONS !== 'false',
|
|
18
|
+
interceptWarnings: env.INTELLERROR_INTERCEPT_WARNINGS === 'true',
|
|
19
|
+
showSearchLinks: env.INTELLERROR_SEARCH_LINKS !== 'false',
|
|
20
|
+
sourceMapsEnabled: env.INTELLERROR_SOURCE_MAPS !== 'false',
|
|
21
|
+
useJsonMode: env.INTELLERROR_JSON === 'true',
|
|
16
22
|
};
|
|
17
23
|
|
|
18
24
|
/**
|
package/src/formatter/browser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseStack } from '../parser/index.js';
|
|
1
|
+
import { parseStack, getErrorChain } from '../parser/index.js';
|
|
2
2
|
import { getSuggestions } from '../suggestions/index.js';
|
|
3
3
|
import { getConfig } from '../config.js';
|
|
4
4
|
import { getSearchLinks } from '../suggestions/links.js';
|
|
@@ -13,7 +13,9 @@ export function formatErrorBrowser(error: Error | unknown): any[] {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const config = getConfig();
|
|
16
|
-
const
|
|
16
|
+
const errorChain = getErrorChain(error);
|
|
17
|
+
const topError = errorChain[0];
|
|
18
|
+
const stackFrames = topError.frames;
|
|
17
19
|
const userFrame = stackFrames.find(frame => !frame.isNodeInternal && !frame.isNodeModule);
|
|
18
20
|
|
|
19
21
|
const errorType = error.constructor.name || 'Error';
|
|
@@ -28,7 +30,12 @@ export function formatErrorBrowser(error: Error | unknown): any[] {
|
|
|
28
30
|
text += `%c📍 Location:%c\n`;
|
|
29
31
|
styles.push('font-weight: bold;', '');
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
const fileName = userFrame.originalFile || userFrame.file;
|
|
34
|
+
const line = userFrame.originalLineNumber || userFrame.lineNumber;
|
|
35
|
+
const col = userFrame.originalColumn || userFrame.column;
|
|
36
|
+
const isMapped = userFrame.originalFile ? ' [mapped]' : '';
|
|
37
|
+
|
|
38
|
+
text += `%c${fileName}:${line}:${col}%c${isMapped} (YOUR CODE)\n\n`;
|
|
32
39
|
styles.push('color: #1890ff; text-decoration: underline;', 'color: grey; font-style: italic;');
|
|
33
40
|
}
|
|
34
41
|
|
package/src/formatter/index.ts
CHANGED
|
@@ -1,32 +1,59 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
import { parseStack, getErrorChain, type ParsedStackFrame, type ErrorChain } from '../parser/index.js';
|
|
3
4
|
import { getSuggestions } from '../suggestions/index.js';
|
|
4
5
|
import { getConfig } from '../config.js';
|
|
5
6
|
import { getCodeSnapshot } from './snapshot.js';
|
|
6
7
|
import { getSearchLinks } from '../suggestions/links.js';
|
|
7
8
|
|
|
9
|
+
function getFingerprint(type: string, frame: ParsedStackFrame | undefined): string {
|
|
10
|
+
const frameId = frame ? `${frame.file}:${frame.lineNumber}:${frame.column}` : 'no-frame';
|
|
11
|
+
return crypto.createHash('sha1').update(`${type}:${frameId}`).digest('hex').substring(0, 8);
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
export function formatError(error: Error | unknown): string {
|
|
9
15
|
if (!(error instanceof Error)) {
|
|
10
16
|
return String(error);
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
const config = getConfig();
|
|
14
|
-
const
|
|
20
|
+
const errorChain = getErrorChain(error);
|
|
21
|
+
const topError = errorChain[0];
|
|
22
|
+
const stackFrames = topError.frames;
|
|
15
23
|
|
|
16
24
|
// Find first user frame that is not node internal or node_modules
|
|
17
25
|
const userFrame = stackFrames.find(frame => !frame.isNodeInternal && !frame.isNodeModule);
|
|
18
26
|
|
|
19
27
|
const errorType = error.constructor.name || 'Error';
|
|
20
|
-
const
|
|
21
|
-
|
|
28
|
+
const severity = (error as any).severity || 'ERROR';
|
|
29
|
+
|
|
30
|
+
const severityColors: Record<string, any> = {
|
|
31
|
+
'FATAL': chalk.bgRed.white.bold,
|
|
32
|
+
'ERROR': chalk.bgRed.white,
|
|
33
|
+
'WARN': chalk.bgYellow.black,
|
|
34
|
+
'INFO': chalk.bgBlue.white
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const typeLabel = (severityColors[severity] || severityColors['ERROR'])(` ${severity} `);
|
|
38
|
+
const timestamp = new Date().toISOString();
|
|
39
|
+
const uptime = process.uptime().toFixed(1);
|
|
40
|
+
const fingerprint = getFingerprint(errorType, userFrame);
|
|
41
|
+
|
|
42
|
+
let output = `\n${typeLabel} ${chalk.red(`${errorType}: ${error.message}`)}\n`;
|
|
43
|
+
output += chalk.dim(`${timestamp} | uptime: ${uptime}s | ID: ${fingerprint}\n\n`);
|
|
22
44
|
|
|
23
45
|
if (userFrame && userFrame.file) {
|
|
24
46
|
output += `${chalk.bold('📍 Location:')}\n`;
|
|
25
|
-
|
|
47
|
+
const fileName = userFrame.originalFile || userFrame.file;
|
|
48
|
+
const line = userFrame.originalLineNumber || userFrame.lineNumber;
|
|
49
|
+
const col = userFrame.originalColumn || userFrame.column;
|
|
50
|
+
const isMapped = userFrame.originalFile ? chalk.magenta(' [mapped]') : '';
|
|
51
|
+
|
|
52
|
+
output += `${chalk.cyan(fileName)}:${chalk.yellow(line)}:${chalk.yellow(col)}${isMapped} ${chalk.gray('← YOUR CODE')}\n\n`;
|
|
26
53
|
|
|
27
54
|
// Add code snapshot (Node only)
|
|
28
|
-
if (
|
|
29
|
-
output += getCodeSnapshot(
|
|
55
|
+
if (line && fileName && !userFrame.originalFile) { // Snapshot only for actual files on disk
|
|
56
|
+
output += getCodeSnapshot(fileName, line);
|
|
30
57
|
}
|
|
31
58
|
}
|
|
32
59
|
|
|
@@ -53,6 +80,15 @@ export function formatError(error: Error | unknown): string {
|
|
|
53
80
|
output += `• ${chalk.dim('GitHub:')} ${chalk.blue(searchLinks[2])}\n\n`;
|
|
54
81
|
}
|
|
55
82
|
|
|
83
|
+
// Handle causes
|
|
84
|
+
if (errorChain.length > 1) {
|
|
85
|
+
output += `${chalk.bold('🔗 Causes:')}\n`;
|
|
86
|
+
for (let i = 1; i < errorChain.length; i++) {
|
|
87
|
+
output += `${' '.repeat(i)}caused by: ${chalk.red(errorChain[i].error.constructor.name)}: ${errorChain[i].error.message}\n`;
|
|
88
|
+
}
|
|
89
|
+
output += '\n';
|
|
90
|
+
}
|
|
91
|
+
|
|
56
92
|
output += `${chalk.bold('📦 Stack:')}\n`;
|
|
57
93
|
|
|
58
94
|
let hiddenInternalsCount = 0;
|
|
@@ -69,8 +105,13 @@ export function formatError(error: Error | unknown): string {
|
|
|
69
105
|
continue;
|
|
70
106
|
}
|
|
71
107
|
|
|
72
|
-
const
|
|
73
|
-
|
|
108
|
+
const fileName = frame.originalFile || frame.file;
|
|
109
|
+
const line = frame.originalLineNumber || frame.lineNumber;
|
|
110
|
+
const col = frame.originalColumn || frame.column;
|
|
111
|
+
const isMapped = frame.originalFile ? chalk.magenta('*') : '';
|
|
112
|
+
|
|
113
|
+
const fileStr = fileName
|
|
114
|
+
? `${chalk.cyan(fileName)}:${chalk.yellow(line)}:${chalk.yellow(col)}${isMapped}`
|
|
74
115
|
: chalk.gray('<unknown>');
|
|
75
116
|
|
|
76
117
|
output += `→ ${fileStr}\n`;
|
|
@@ -86,6 +127,40 @@ export function formatError(error: Error | unknown): string {
|
|
|
86
127
|
return output;
|
|
87
128
|
}
|
|
88
129
|
|
|
130
|
+
export function formatErrorJSON(error: Error | unknown): string {
|
|
131
|
+
if (!(error instanceof Error)) {
|
|
132
|
+
return JSON.stringify({ error: String(error) });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const config = getConfig();
|
|
136
|
+
const errorChain = getErrorChain(error);
|
|
137
|
+
const topError = errorChain[0];
|
|
138
|
+
const userFrame = topError.frames.find(frame => !frame.isNodeInternal && !frame.isNodeModule);
|
|
139
|
+
|
|
140
|
+
return JSON.stringify({
|
|
141
|
+
timestamp: new Date().toISOString(),
|
|
142
|
+
uptime: process.uptime(),
|
|
143
|
+
severity: (error as any).severity || 'ERROR',
|
|
144
|
+
fingerprint: getFingerprint(error.constructor.name, userFrame),
|
|
145
|
+
error: {
|
|
146
|
+
type: error.constructor.name,
|
|
147
|
+
message: error.message,
|
|
148
|
+
stack: topError.frames,
|
|
149
|
+
causes: errorChain.slice(1).map(c => ({
|
|
150
|
+
type: c.error.constructor.name,
|
|
151
|
+
message: c.error.message
|
|
152
|
+
}))
|
|
153
|
+
},
|
|
154
|
+
suggestions: config.suggestionsEnabled ? getSuggestions(error) : [],
|
|
155
|
+
location: userFrame ? {
|
|
156
|
+
file: userFrame.originalFile || userFrame.file,
|
|
157
|
+
line: userFrame.originalLineNumber || userFrame.lineNumber,
|
|
158
|
+
column: userFrame.originalColumn || userFrame.column,
|
|
159
|
+
isMapped: !!userFrame.originalFile
|
|
160
|
+
} : null
|
|
161
|
+
}, null, 2);
|
|
162
|
+
}
|
|
163
|
+
|
|
89
164
|
export function formatWarning(message: string, error?: Error): string {
|
|
90
165
|
const config = getConfig();
|
|
91
166
|
const actualError = error || new Error(message);
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { formatError, formatWarning } from './formatter/index.js';
|
|
1
|
+
export { formatError, formatWarning, formatErrorJSON } from './formatter/index.js';
|
|
2
2
|
export { formatErrorBrowser, formatWarningBrowser } from './formatter/browser.js';
|
|
3
3
|
export { setupConfig } from './config.js';
|
|
4
4
|
export { registerRule } from './suggestions/index.js';
|
|
5
|
-
export { parseStack } from './parser/index.js';
|
|
5
|
+
export { parseStack, getErrorChain, type ParsedStackFrame, type ErrorChain } from './parser/index.js';
|
|
6
6
|
export { errorFormatter } from './middleware/index.js';
|
package/src/parser/index.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import * as stackTraceParser from 'stacktrace-parser';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { SourceMapConsumer } from 'source-map-js';
|
|
5
|
+
import { getConfig } from '../config.js';
|
|
2
6
|
|
|
3
7
|
export interface ParsedStackFrame {
|
|
4
8
|
file: string | null;
|
|
@@ -7,8 +11,27 @@ export interface ParsedStackFrame {
|
|
|
7
11
|
column: number | null;
|
|
8
12
|
isNodeInternal: boolean;
|
|
9
13
|
isNodeModule: boolean;
|
|
14
|
+
originalFile?: string | null;
|
|
15
|
+
originalLineNumber?: number | null;
|
|
16
|
+
originalColumn?: number | null;
|
|
10
17
|
}
|
|
11
18
|
|
|
19
|
+
export interface ErrorChain {
|
|
20
|
+
error: Error;
|
|
21
|
+
frames: ParsedStackFrame[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Last updated: Node.js v22. Source:
|
|
25
|
+
// https://nodejs.org/api/module.html#modulebuiltinmodules
|
|
26
|
+
const NODE_BUILTINS = new Set([
|
|
27
|
+
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console',
|
|
28
|
+
'constants', 'crypto', 'dgram', 'diagnostics_channel', 'dns', 'domain',
|
|
29
|
+
'events', 'fs', 'http', 'http2', 'https', 'inspector', 'loader', 'module', 'net',
|
|
30
|
+
'os', 'path', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline',
|
|
31
|
+
'repl', 'stream', 'string_decoder', 'sys', 'timers', 'tls', 'trace_events',
|
|
32
|
+
'tty', 'url', 'util', 'v8', 'vm', 'worker_threads', 'zlib'
|
|
33
|
+
]);
|
|
34
|
+
|
|
12
35
|
export function parseStack(error: Error): ParsedStackFrame[] {
|
|
13
36
|
if (!error.stack) return [];
|
|
14
37
|
|
|
@@ -17,16 +40,14 @@ export function parseStack(error: Error): ParsedStackFrame[] {
|
|
|
17
40
|
return parsed.map(frame => {
|
|
18
41
|
const file = frame.file || '';
|
|
19
42
|
|
|
20
|
-
// Check if it's a built-in node module or internal
|
|
21
|
-
// e.g. node:internal/... or just internal/... or events.js
|
|
22
43
|
const isNodeInternal =
|
|
23
44
|
file.startsWith('node:') ||
|
|
24
45
|
file.startsWith('internal/') ||
|
|
25
|
-
|
|
46
|
+
NODE_BUILTINS.has(file.replace(/\.js$/, '').replace(/^node:/, ''));
|
|
26
47
|
|
|
27
48
|
const isNodeModule = file.includes('node_modules');
|
|
28
49
|
|
|
29
|
-
|
|
50
|
+
const result: ParsedStackFrame = {
|
|
30
51
|
file: frame.file,
|
|
31
52
|
methodName: frame.methodName || '<unknown>',
|
|
32
53
|
lineNumber: frame.lineNumber,
|
|
@@ -34,5 +55,48 @@ export function parseStack(error: Error): ParsedStackFrame[] {
|
|
|
34
55
|
isNodeInternal: Boolean(isNodeInternal),
|
|
35
56
|
isNodeModule: Boolean(isNodeModule)
|
|
36
57
|
};
|
|
58
|
+
|
|
59
|
+
// Try to enrich with source maps (Node only, synchronous for now)
|
|
60
|
+
const config = getConfig();
|
|
61
|
+
if (config.sourceMapsEnabled && !isNodeInternal && !isNodeModule && frame.file && frame.lineNumber && frame.column) {
|
|
62
|
+
try {
|
|
63
|
+
const mapPath = `${frame.file}.map`;
|
|
64
|
+
if (fs.existsSync(mapPath)) {
|
|
65
|
+
const mapContent = fs.readFileSync(mapPath, 'utf-8');
|
|
66
|
+
const consumer = new SourceMapConsumer(JSON.parse(mapContent));
|
|
67
|
+
const original = consumer.originalPositionFor({
|
|
68
|
+
line: frame.lineNumber,
|
|
69
|
+
column: frame.column
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (original.source) {
|
|
73
|
+
result.originalFile = original.source;
|
|
74
|
+
result.originalLineNumber = original.line;
|
|
75
|
+
result.originalColumn = original.column;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// Silently ignore source map errors
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result;
|
|
37
84
|
});
|
|
38
85
|
}
|
|
86
|
+
|
|
87
|
+
export function getErrorChain(error: Error): ErrorChain[] {
|
|
88
|
+
const chain: ErrorChain[] = [];
|
|
89
|
+
let currentError: Error | undefined = error;
|
|
90
|
+
|
|
91
|
+
while (currentError) {
|
|
92
|
+
chain.push({
|
|
93
|
+
error: currentError,
|
|
94
|
+
frames: parseStack(currentError)
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Handle Error.cause (Node 16.9+, modern browsers)
|
|
98
|
+
currentError = (currentError as any).cause instanceof Error ? (currentError as any).cause : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return chain;
|
|
102
|
+
}
|
package/src/register.ts
CHANGED
|
@@ -1,48 +1,90 @@
|
|
|
1
|
-
import { formatError, formatWarning } from './formatter/index.js';
|
|
1
|
+
import { formatError, formatWarning, formatErrorJSON } from './formatter/index.js';
|
|
2
2
|
import { formatErrorBrowser, formatWarningBrowser } from './formatter/browser.js';
|
|
3
3
|
import { getConfig } from './config.js';
|
|
4
4
|
import { sendWebhookNotification } from './integrations/webhook.js';
|
|
5
5
|
|
|
6
|
+
const isDeno = typeof (globalThis as any).Deno !== 'undefined';
|
|
7
|
+
const isBun = typeof (globalThis as any).Bun !== 'undefined';
|
|
8
|
+
const isNode = typeof process !== 'undefined' && process.release?.name === 'node';
|
|
9
|
+
const isEdge = typeof process !== 'undefined' && (process.env as any).NEXT_RUNTIME === 'edge';
|
|
6
10
|
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
7
11
|
|
|
8
|
-
if (isBrowser) {
|
|
9
|
-
// --- BROWSER REGISTRATION ---
|
|
10
|
-
window
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
if (isBrowser || isDeno) {
|
|
13
|
+
// --- BROWSER / DENO / WEB-STD REGISTRATION ---
|
|
14
|
+
const target = isBrowser ? window : globalThis;
|
|
15
|
+
|
|
16
|
+
(target as any).addEventListener('error', (event: any) => {
|
|
17
|
+
const error = event.error || event.message;
|
|
18
|
+
if (error) {
|
|
19
|
+
if (isBrowser) {
|
|
20
|
+
console.error('%c 🛑 UNHANDLED RUNTIME ERROR:', 'background: #ff4d4f; color: white; font-weight: bold; padding: 2px 5px;');
|
|
21
|
+
console.log(...formatErrorBrowser(error));
|
|
22
|
+
} else {
|
|
23
|
+
const config = getConfig();
|
|
24
|
+
console.error('\n 🔥 UNHANDLED RUNTIME ERROR:');
|
|
25
|
+
if (config.useJsonMode) {
|
|
26
|
+
console.error(formatErrorJSON(error));
|
|
27
|
+
} else {
|
|
28
|
+
console.error(formatError(error));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
sendWebhookNotification(error);
|
|
16
32
|
}
|
|
17
33
|
});
|
|
18
34
|
|
|
19
|
-
|
|
20
|
-
console.error('%c 🛑 UNHANDLED PROMISE REJECTION:', 'background: #ff4d4f; color: white; font-weight: bold; padding: 2px 5px;');
|
|
35
|
+
(target as any).addEventListener('unhandledrejection', (event: any) => {
|
|
21
36
|
const reason = event.reason;
|
|
22
37
|
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
23
|
-
|
|
38
|
+
if (isBrowser) {
|
|
39
|
+
console.error('%c 🛑 UNHANDLED PROMISE REJECTION:', 'background: #ff4d4f; color: white; font-weight: bold; padding: 2px 5px;');
|
|
40
|
+
console.log(...formatErrorBrowser(error));
|
|
41
|
+
} else {
|
|
42
|
+
const config = getConfig();
|
|
43
|
+
console.error('\n 🔥 UNHANDLED PROMISE REJECTION:');
|
|
44
|
+
if (config.useJsonMode) {
|
|
45
|
+
console.error(formatErrorJSON(error));
|
|
46
|
+
} else {
|
|
47
|
+
console.error(formatError(error));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
24
50
|
sendWebhookNotification(error);
|
|
25
51
|
});
|
|
26
52
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
53
|
+
const runtimeName = isBrowser ? 'Browser' : 'Deno';
|
|
54
|
+
if (isBrowser) {
|
|
55
|
+
console.log(`%c ✅ IntellError ${runtimeName} listener active.`, 'color: #52c41a; font-weight: bold;');
|
|
56
|
+
} else {
|
|
57
|
+
console.log(`✅ IntellError ${runtimeName} listener active.`);
|
|
58
|
+
}
|
|
59
|
+
} else if (isNode || isBun || isEdge) {
|
|
60
|
+
// --- NODE.JS / BUN / EDGE REGISTRATION ---
|
|
30
61
|
process.on('uncaughtException', (err) => {
|
|
62
|
+
const config = getConfig();
|
|
31
63
|
console.error('\n 🔥 UNCAUGHT EXCEPTION DETECTED:');
|
|
32
|
-
|
|
64
|
+
if (config.useJsonMode) {
|
|
65
|
+
console.error(formatErrorJSON(err));
|
|
66
|
+
} else {
|
|
67
|
+
console.error(formatError(err));
|
|
68
|
+
}
|
|
33
69
|
sendWebhookNotification(err).finally(() => {
|
|
34
|
-
process.exit(1);
|
|
70
|
+
if (!isEdge) process.exit(1); // Don't exit in Edge/Serverless environments
|
|
35
71
|
});
|
|
36
72
|
});
|
|
37
73
|
|
|
38
74
|
process.on('unhandledRejection', (reason) => {
|
|
75
|
+
const config = getConfig();
|
|
39
76
|
console.error('\n 🔥 UNHANDLED REJECTION DETECTED:');
|
|
40
77
|
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
41
|
-
|
|
78
|
+
if (config.useJsonMode) {
|
|
79
|
+
console.error(formatErrorJSON(error));
|
|
80
|
+
} else {
|
|
81
|
+
console.error(formatError(error));
|
|
82
|
+
}
|
|
42
83
|
sendWebhookNotification(error);
|
|
43
84
|
});
|
|
44
85
|
|
|
45
|
-
|
|
86
|
+
const runtimeName = isBun ? 'Bun' : (isEdge ? 'Edge' : 'Node.js');
|
|
87
|
+
console.log(`✅ IntellError ${runtimeName} listener active.`);
|
|
46
88
|
}
|
|
47
89
|
|
|
48
90
|
// --- OPTIONAL WARNING INTERCEPTION ---
|
package/src/suggestions/index.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
import { rules,
|
|
1
|
+
import { rules, SuggestionRule } from './rules.js';
|
|
2
|
+
import { parseStack, ParsedStackFrame } from '../parser/index.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Gets intelligent suggestions for a given error.
|
|
6
|
+
* Now supports context-aware matching and confidence-based sorting.
|
|
7
|
+
*/
|
|
8
|
+
export function getSuggestions(error: Error): SuggestionRule[] {
|
|
9
|
+
const stack = parseStack(error);
|
|
10
|
+
|
|
11
|
+
return rules
|
|
12
|
+
.filter(rule => rule.match(error, stack))
|
|
13
|
+
// Sort by confidence descending
|
|
14
|
+
.sort((a, b) => b.confidence - a.confidence);
|
|
5
15
|
}
|
|
6
16
|
|
|
7
17
|
/**
|
|
8
18
|
* Registers a new custom suggestion rule.
|
|
9
19
|
*/
|
|
10
|
-
export function registerRule(rule:
|
|
20
|
+
export function registerRule(rule: SuggestionRule) {
|
|
11
21
|
rules.push(rule);
|
|
12
22
|
}
|
package/src/suggestions/rules.ts
CHANGED
|
@@ -1,33 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { type ParsedStackFrame } from '../parser/index.js';
|
|
2
|
+
|
|
3
|
+
export interface SuggestionRule {
|
|
4
|
+
match: (error: Error, stack: ParsedStackFrame[]) => boolean;
|
|
3
5
|
message: string;
|
|
6
|
+
confidence: number; // 0-1
|
|
4
7
|
fix?: string;
|
|
5
8
|
description?: string;
|
|
6
9
|
}
|
|
7
10
|
|
|
8
|
-
export const rules:
|
|
11
|
+
export const rules: SuggestionRule[] = [
|
|
9
12
|
// --- TYPE ERRORS & NULL/UNDEFINED ---
|
|
10
13
|
{
|
|
11
14
|
match: (err) => err.message.includes("Cannot read properties of undefined (reading '"),
|
|
12
15
|
message: "Accessing property on undefined object.",
|
|
16
|
+
confidence: 1,
|
|
13
17
|
fix: "Use optional chaining like 'obj?.prop' or ensure the object is initialized.",
|
|
14
18
|
description: "You're trying to access a property on a variable that is currently undefined."
|
|
15
19
|
},
|
|
16
20
|
{
|
|
17
21
|
match: (err) => err.message.includes("Cannot read properties of null (reading '"),
|
|
18
22
|
message: "Accessing property on a null object.",
|
|
23
|
+
confidence: 1,
|
|
19
24
|
fix: "Check why the object is null before accessing its properties, or use 'obj?.prop'.",
|
|
20
25
|
description: "You're trying to read a property from a variable that is null."
|
|
21
26
|
},
|
|
22
27
|
{
|
|
23
28
|
match: (err) => err.message.includes("is not defined"),
|
|
24
29
|
message: "Reference to an undeclared variable.",
|
|
30
|
+
confidence: 1,
|
|
25
31
|
fix: "Ensure the variable is declared with 'let', 'const', or 'var' and is within its correct scope.",
|
|
26
32
|
description: "You're using a variable that hasn't been defined yet."
|
|
27
33
|
},
|
|
28
34
|
{
|
|
29
35
|
match: (err) => err.message.includes("Cannot destructure property"),
|
|
30
36
|
message: "Failed to destructure object.",
|
|
37
|
+
confidence: 1,
|
|
31
38
|
fix: "Provide a default object like 'const { x } = data || {}' to avoid destructuring null/undefined.",
|
|
32
39
|
description: "You're trying to extract properties from a variable that turned out to be empty."
|
|
33
40
|
},
|
|
@@ -36,18 +43,21 @@ export const rules: SuggesionRule[] = [
|
|
|
36
43
|
{
|
|
37
44
|
match: (err) => err.message.includes(".map is not a function"),
|
|
38
45
|
message: "Array method '.map()' called on non-array value.",
|
|
46
|
+
confidence: 1,
|
|
39
47
|
fix: "Verify that the variable is an array. If it's from an API, ensure the result is '[]' if empty.",
|
|
40
48
|
description: "Commonly happens when an API returns an object or null instead of the expected list."
|
|
41
49
|
},
|
|
42
50
|
{
|
|
43
51
|
match: (err) => err.message.includes(".forEach is not a function"),
|
|
44
52
|
message: "Array method '.forEach()' called on non-array value.",
|
|
53
|
+
confidence: 1,
|
|
45
54
|
fix: "Check if you're actually getting an array. Use 'Array.isArray(obj)' to verify.",
|
|
46
55
|
description: "You attempted to iterate over something that isn't a list."
|
|
47
56
|
},
|
|
48
57
|
{
|
|
49
58
|
match: (err) => err.message.includes("is not a function"),
|
|
50
59
|
message: "Called a non-function value.",
|
|
60
|
+
confidence: 1,
|
|
51
61
|
fix: "Double-check if the property you're calling is actually a function and not undefined or a string.",
|
|
52
62
|
description: "You tried to execute something as a function that isn't one."
|
|
53
63
|
},
|
|
@@ -56,18 +66,21 @@ export const rules: SuggesionRule[] = [
|
|
|
56
66
|
{
|
|
57
67
|
match: (err) => err instanceof SyntaxError && err.message.includes("JSON"),
|
|
58
68
|
message: "Invalid JSON format.",
|
|
69
|
+
confidence: 1,
|
|
59
70
|
fix: "Verify the source string. Use a JSON validator to find trailing commas or incorrect quotes.",
|
|
60
71
|
description: "The JSON you are parsing contains syntax errors."
|
|
61
72
|
},
|
|
62
73
|
{
|
|
63
74
|
match: (err) => err instanceof RangeError && err.message.includes("Maximum call stack size exceeded"),
|
|
64
75
|
message: "Infinite recursion detected.",
|
|
76
|
+
confidence: 1,
|
|
65
77
|
fix: "Look for functions that call themselves without a base case (e.g., missing exit condition).",
|
|
66
78
|
description: "Your code likely has an infinite loop or too many nested function calls."
|
|
67
79
|
},
|
|
68
80
|
{
|
|
69
81
|
match: (err) => err.message.includes("Invalid URL"),
|
|
70
82
|
message: "Failed to construct URL.",
|
|
83
|
+
confidence: 1,
|
|
71
84
|
fix: "Check that the input string is a valid absolute URL (e.g., starts with http:// or https://).",
|
|
72
85
|
description: "The URL constructor was given a malformed or relative string."
|
|
73
86
|
},
|
|
@@ -76,12 +89,14 @@ export const rules: SuggesionRule[] = [
|
|
|
76
89
|
{
|
|
77
90
|
match: (err) => err.message.includes("Failed to fetch"),
|
|
78
91
|
message: "Network request failed.",
|
|
92
|
+
confidence: 1,
|
|
79
93
|
fix: "Check your internet connection or verify if the API server is down or the URL is wrong.",
|
|
80
94
|
description: "The browser could not reach the server. This is often a DNS or connectivity issue."
|
|
81
95
|
},
|
|
82
96
|
{
|
|
83
97
|
match: (err) => err.message.includes("cors") && err.message.includes("access-control-allow-origin"),
|
|
84
98
|
message: "CORS policy violation.",
|
|
99
|
+
confidence: 1,
|
|
85
100
|
fix: "The API server needs to allow your origin. If you control the API, use the 'cors' middleware.",
|
|
86
101
|
description: "The browser's security policy blocked the cross-origin request."
|
|
87
102
|
},
|
|
@@ -90,24 +105,28 @@ export const rules: SuggesionRule[] = [
|
|
|
90
105
|
{
|
|
91
106
|
match: (err: any) => err.code === 'ENOENT',
|
|
92
107
|
message: "File or directory not found.",
|
|
108
|
+
confidence: 1,
|
|
93
109
|
fix: "Verify the file path. Use 'path.join(__dirname, ...)' to ensure you have an absolute path.",
|
|
94
110
|
description: "Node.js could not find the file or folder you're looking for at that location."
|
|
95
111
|
},
|
|
96
112
|
{
|
|
97
113
|
match: (err: any) => err.code === 'EADDRINUSE',
|
|
98
114
|
message: "Port already in use.",
|
|
115
|
+
confidence: 1,
|
|
99
116
|
fix: "Close the process currently using that port (e.g., kill another terminal) or change the PORT.",
|
|
100
117
|
description: "Another application is already running on the port you're trying to use."
|
|
101
118
|
},
|
|
102
119
|
{
|
|
103
120
|
match: (err: any) => err.code === 'ECONNREFUSED',
|
|
104
121
|
message: "Network connection refused.",
|
|
122
|
+
confidence: 1,
|
|
105
123
|
fix: "The target server is not listening. Ensure the API or Database is actually running.",
|
|
106
124
|
description: "The connection attempt was rejected by the target machine."
|
|
107
125
|
},
|
|
108
126
|
{
|
|
109
127
|
match: (err: any) => err.code === 'EACCES',
|
|
110
128
|
message: "Permission denied.",
|
|
129
|
+
confidence: 1,
|
|
111
130
|
fix: "Run the command with sudo/administrator privileges or check filesystem permissions.",
|
|
112
131
|
description: "Your application doesn't have the internal rights to read/write to this location."
|
|
113
132
|
},
|
|
@@ -116,12 +135,14 @@ export const rules: SuggesionRule[] = [
|
|
|
116
135
|
{
|
|
117
136
|
match: (err) => err.message.includes("Hooks can only be called inside of the body of a function component"),
|
|
118
137
|
message: "Invalid Hook Call.",
|
|
138
|
+
confidence: 1,
|
|
119
139
|
fix: "Ensure you aren't calling hooks inside loops, conditions, or nested functions.",
|
|
120
140
|
description: "React hooks must be called at the top level of a Functional Component."
|
|
121
141
|
},
|
|
122
142
|
{
|
|
123
143
|
match: (err) => err.message.includes("Too many re-renders"),
|
|
124
144
|
message: "Infinite re-render loop.",
|
|
145
|
+
confidence: 1,
|
|
125
146
|
fix: "You're likely updating state inside the render body. Move the update into a 'useEffect'.",
|
|
126
147
|
description: "React prevents infinite loops when an update is triggered every single time a component renders."
|
|
127
148
|
},
|
|
@@ -130,30 +151,35 @@ export const rules: SuggesionRule[] = [
|
|
|
130
151
|
{
|
|
131
152
|
match: (err) => err.message.includes("Cannot read properties of null (reading 'style')"),
|
|
132
153
|
message: "Attempted to style a missing element.",
|
|
154
|
+
confidence: 1,
|
|
133
155
|
fix: "The element you're trying to style wasn't found in the DOM. Verify your CSS selector (class or ID) match the HTML exactly.",
|
|
134
156
|
description: "Your query (querySelector or getElementById) returned null."
|
|
135
157
|
},
|
|
136
158
|
{
|
|
137
159
|
match: (err) => err.message.includes("is not a valid selector"),
|
|
138
160
|
message: "Invalid CSS Selector.",
|
|
161
|
+
confidence: 1,
|
|
139
162
|
fix: "Ensure your selector follows CSS rules (e.g., '.class-name', '#id-name', 'div > p'). Check for missing dots/hashes.",
|
|
140
163
|
description: "The string provided to a DOM selection method is not syntactically correct."
|
|
141
164
|
},
|
|
142
165
|
{
|
|
143
166
|
match: (err) => err.message.includes("CSSStyleDeclaration") && err.message.includes("read-only"),
|
|
144
167
|
message: "Attempted to modify a read-only style property.",
|
|
168
|
+
confidence: 1,
|
|
145
169
|
fix: "Some browser styles (like getComputedStyle results) are read-only. Use 'element.style' instead for manual changes.",
|
|
146
170
|
description: "You're trying to write to a value that the browser keeps locked."
|
|
147
171
|
},
|
|
148
172
|
{
|
|
149
173
|
match: (err) => err.message.includes("Failed to execute 'animate' on 'Element'"),
|
|
150
174
|
message: "Invalid Web Animation.",
|
|
175
|
+
confidence: 1,
|
|
151
176
|
fix: "Check your keyframes and timing options. Ensure properties like 'transform' use standard syntax.",
|
|
152
177
|
description: "The Web Animations API rejected your parameters."
|
|
153
178
|
},
|
|
154
179
|
{
|
|
155
180
|
match: (err) => err.message.includes("Failed to execute 'add' on 'DOMTokenList'") || err.message.includes("token contains HTML space characters"),
|
|
156
181
|
message: "Invalid CSS Class Name.",
|
|
182
|
+
confidence: 1,
|
|
157
183
|
fix: "CSS classes cannot contain spaces. Use 'classList.add('class1', 'class2')' with separate arguments instead.",
|
|
158
184
|
description: "You tried to add an invalid token (like a sentence) into an element's className."
|
|
159
185
|
},
|
|
@@ -162,18 +188,21 @@ export const rules: SuggesionRule[] = [
|
|
|
162
188
|
{
|
|
163
189
|
match: (err) => err.message.includes("Each child in a list should have a unique \"key\" prop"),
|
|
164
190
|
message: "Missing React Key Prop.",
|
|
191
|
+
confidence: 1,
|
|
165
192
|
fix: "Add a 'key' prop to the top-level element inside your .map() (e.g., <li key={item.id}>).",
|
|
166
193
|
description: "React needs keys to efficiently track and update items in a list."
|
|
167
194
|
},
|
|
168
195
|
{
|
|
169
196
|
match: (err) => err.message.includes("Changing an uncontrolled input to be controlled"),
|
|
170
197
|
message: "Uncontrolled to Controlled Input switch.",
|
|
198
|
+
confidence: 1,
|
|
171
199
|
fix: "Ensure the 'value' prop is never undefined. Initialize your state with an empty string: useState('') instead of useState().",
|
|
172
200
|
description: "React expects an input to stay either controlled or uncontrolled for its entire lifecycle."
|
|
173
201
|
},
|
|
174
202
|
{
|
|
175
203
|
match: (err) => err.message.includes("ExperimentalWarning"),
|
|
176
204
|
message: "Using Experimental Node.js Feature.",
|
|
205
|
+
confidence: 1,
|
|
177
206
|
fix: "This feature is functional but the API might change in future Node versions. High-risk for production.",
|
|
178
207
|
description: "You're using a feature that is still under active development by the Node.js team."
|
|
179
208
|
},
|
|
@@ -182,18 +211,21 @@ export const rules: SuggesionRule[] = [
|
|
|
182
211
|
{
|
|
183
212
|
match: (err) => err.message.includes("Cannot mix BigInt and other types"),
|
|
184
213
|
message: "BigInt Type Mismatch.",
|
|
214
|
+
confidence: 1,
|
|
185
215
|
fix: "Explicitly convert other types to BigInt using 'BigInt(value)' before performing arithmetic operations.",
|
|
186
216
|
description: "JavaScript does not allow implicit coercion between BigInt and standard Number types."
|
|
187
217
|
},
|
|
188
218
|
{
|
|
189
219
|
match: (err) => err.message.includes("is not iterable"),
|
|
190
220
|
message: "Iteration Failure.",
|
|
221
|
+
confidence: 1,
|
|
191
222
|
fix: "Check if the variable you're trying to spread or loop over (...) is null or an object instead of an Array/Map/Set.",
|
|
192
223
|
description: "You attempted to use a spread operator or for...of loop on a non-iterable value."
|
|
193
224
|
},
|
|
194
225
|
{
|
|
195
226
|
match: (err) => err instanceof SyntaxError && err.message.includes("Invalid regular expression"),
|
|
196
227
|
message: "Malformed Regex.",
|
|
228
|
+
confidence: 1,
|
|
197
229
|
fix: "Check for unclosed parentheses, invalid escape characters, or incorrect flags in your RegExp.",
|
|
198
230
|
description: "The JavaScript engine could not compile your regular expression pattern."
|
|
199
231
|
},
|
|
@@ -202,12 +234,14 @@ export const rules: SuggesionRule[] = [
|
|
|
202
234
|
{
|
|
203
235
|
match: (err) => err.message.includes("QuotaExceededError") || err.message.includes("exceeded the quota"),
|
|
204
236
|
message: "Storage Quota Exceeded.",
|
|
237
|
+
confidence: 1,
|
|
205
238
|
fix: "Clear some space in localStorage/IndexedDB or handle the error by clearing old cached data.",
|
|
206
239
|
description: "You've reached the maximum storage limit allowed by the browser for this origin."
|
|
207
240
|
},
|
|
208
241
|
{
|
|
209
242
|
match: (err) => err.name === 'NotAllowedError' && err.message.includes("play() failed"),
|
|
210
243
|
message: "Autoplay Blocked.",
|
|
244
|
+
confidence: 1,
|
|
211
245
|
fix: "Wait for a user interaction (click/tap) before calling .play() on audio or video elements.",
|
|
212
246
|
description: "Modern browsers block media from playing automatically without a user gesture."
|
|
213
247
|
},
|
|
@@ -216,12 +250,14 @@ export const rules: SuggesionRule[] = [
|
|
|
216
250
|
{
|
|
217
251
|
match: (err) => err.message.includes("Cannot set headers after they are sent to the client"),
|
|
218
252
|
message: "Express Header Conflict.",
|
|
253
|
+
confidence: 1,
|
|
219
254
|
fix: "Check for multiple 'res.send()', 'res.json()', or 'res.end()' calls in a single route handler. Use 'return res.json(...)' to stop execution early.",
|
|
220
255
|
description: "You're trying to send a response to the client after a response has already been finished."
|
|
221
256
|
},
|
|
222
257
|
{
|
|
223
258
|
match: (err: any) => err.code === 'EADDRNOTAVAIL',
|
|
224
259
|
message: "Address Not Available.",
|
|
260
|
+
confidence: 1,
|
|
225
261
|
fix: "Check your host/IP configuration. You might be trying to bind to an IP address that doesn't exist on this machine.",
|
|
226
262
|
description: "The requested network address could not be assigned."
|
|
227
263
|
},
|
|
@@ -230,7 +266,28 @@ export const rules: SuggesionRule[] = [
|
|
|
230
266
|
{
|
|
231
267
|
match: (err) => err.message.includes("Cannot assign to read only property 'current'"),
|
|
232
268
|
message: "Immutable Ref Modification.",
|
|
269
|
+
confidence: 1,
|
|
233
270
|
fix: "Ensure you're not trying to override a ref's .current property directly if it was provided by a parent or a third-party library.",
|
|
234
271
|
description: "React protects certain ref objects from being overwritten to maintain internal consistency."
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// --- CONTEXT-AWARE RULES (New) ---
|
|
275
|
+
{
|
|
276
|
+
match: (err, stack) =>
|
|
277
|
+
err.message.includes('Ref is not a valid selector') &&
|
|
278
|
+
stack.some(f => f.file?.includes('.tsx') || f.file?.includes('.jsx')),
|
|
279
|
+
message: "Ref assigned to a non-DOM element in React.",
|
|
280
|
+
confidence: 0.9,
|
|
281
|
+
fix: "Ensure you're passing 'ref' to an actual HTML tag, not a custom React component.",
|
|
282
|
+
description: "React components don't automatically forward refs unless they use 'forwardRef'."
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
match: (err, stack) =>
|
|
286
|
+
err.message.includes('fs') &&
|
|
287
|
+
stack.some(f => f.file?.includes('node_modules/vite') || f.file?.includes('node_modules/webpack')),
|
|
288
|
+
message: "Attempted to use 'fs' in a browser environment.",
|
|
289
|
+
confidence: 0.95,
|
|
290
|
+
fix: "The 'fs' module is only available in Node.js. Use browser-native APIs for storage or move this code to the server.",
|
|
291
|
+
description: "Your build tool detected an attempt to use a server-only module in the browser."
|
|
235
292
|
}
|
|
236
293
|
];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { formatError, formatErrorJSON } from '../src/formatter/index.js';
|
|
3
|
+
|
|
4
|
+
describe('Formatter', () => {
|
|
5
|
+
it('should format errors with severity levels', () => {
|
|
6
|
+
const error = new Error('Fatal failure');
|
|
7
|
+
(error as any).severity = 'FATAL';
|
|
8
|
+
|
|
9
|
+
const output = formatError(error);
|
|
10
|
+
expect(output).toContain('FATAL');
|
|
11
|
+
expect(output).toContain('Fatal failure');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should include fingerprint and timestamp in ANSI output', () => {
|
|
15
|
+
const error = new Error('Fingerprint test');
|
|
16
|
+
const output = formatError(error);
|
|
17
|
+
|
|
18
|
+
expect(output).toMatch(/ID: [a-f0-9]{8}/);
|
|
19
|
+
expect(output).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should output valid JSON in formatErrorJSON', () => {
|
|
23
|
+
const error = new Error('JSON test');
|
|
24
|
+
const jsonStr = formatErrorJSON(error);
|
|
25
|
+
const json = JSON.parse(jsonStr);
|
|
26
|
+
|
|
27
|
+
expect(json.error.type).toBe('Error');
|
|
28
|
+
expect(json.error.message).toBe('JSON test');
|
|
29
|
+
expect(json.fingerprint).toBeDefined();
|
|
30
|
+
expect(json.timestamp).toBeDefined();
|
|
31
|
+
expect(json.uptime).toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should handle causes in JSON output', () => {
|
|
35
|
+
const cause = new Error('Root cause');
|
|
36
|
+
const error = new Error('Wrapper', { cause });
|
|
37
|
+
|
|
38
|
+
const jsonStr = formatErrorJSON(error);
|
|
39
|
+
const json = JSON.parse(jsonStr);
|
|
40
|
+
|
|
41
|
+
expect(json.error.causes).toHaveLength(1);
|
|
42
|
+
expect(json.error.causes[0].message).toBe('Root cause');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseStack, getErrorChain } from '../src/parser/index.js';
|
|
3
|
+
|
|
4
|
+
describe('Parser', () => {
|
|
5
|
+
it('should identify node built-in modules correctly', () => {
|
|
6
|
+
const error = new Error('test');
|
|
7
|
+
error.stack = `Error: test
|
|
8
|
+
at Object.<anonymous> (fs.js:1:1)
|
|
9
|
+
at Module._compile (node:internal/modules/cjs/loader:1101:14)
|
|
10
|
+
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
|
|
11
|
+
at Module.load (node:internal/modules/cjs/loader:981:32)
|
|
12
|
+
at Function.Module._load (node:internal/modules/cjs/loader:822:12)
|
|
13
|
+
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
|
|
14
|
+
at node:internal/main/run_main_module:17:47`;
|
|
15
|
+
|
|
16
|
+
const frames = parseStack(error);
|
|
17
|
+
expect(frames[0].isNodeInternal).toBe(true); // fs.js
|
|
18
|
+
expect(frames[1].isNodeInternal).toBe(true); // node:internal/...
|
|
19
|
+
expect(frames[6].isNodeInternal).toBe(true); // node:internal/...
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should NOT identify root-level user files as node internals', () => {
|
|
23
|
+
const error = new Error('test');
|
|
24
|
+
error.stack = `Error: test
|
|
25
|
+
at Object.<anonymous> (index.ts:1:1)
|
|
26
|
+
at Module._compile (loader.js:1:1)`;
|
|
27
|
+
|
|
28
|
+
const frames = parseStack(error);
|
|
29
|
+
expect(frames[0].isNodeInternal).toBe(false); // index.ts is user code
|
|
30
|
+
expect(frames[1].isNodeInternal).toBe(true); // loader.js is internal
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should traverse Error.cause correctly', () => {
|
|
34
|
+
const cause = new Error('Original Cause');
|
|
35
|
+
const error = new Error('Wrapper Error', { cause });
|
|
36
|
+
|
|
37
|
+
const chain = getErrorChain(error);
|
|
38
|
+
expect(chain).toHaveLength(2);
|
|
39
|
+
expect(chain[0].error.message).toBe('Wrapper Error');
|
|
40
|
+
expect(chain[1].error.message).toBe('Original Cause');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should handle anonymous frames and unknown methods', () => {
|
|
44
|
+
const error = new Error('test');
|
|
45
|
+
error.stack = `Error: test
|
|
46
|
+
at <anonymous>:1:1`;
|
|
47
|
+
|
|
48
|
+
const frames = parseStack(error);
|
|
49
|
+
expect(frames[0].methodName).toBe('<unknown>');
|
|
50
|
+
});
|
|
51
|
+
});
|