bugvaulty 1.0.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 +259 -0
- package/package.json +36 -0
- package/react/ErrorBoundary.jsx +67 -0
- package/react/index.js +1 -0
- package/src/aiService.js +215 -0
- package/src/errorCatcher.js +98 -0
- package/src/index.js +213 -0
- package/src/notionService.js +184 -0
- package/src/utils.js +124 -0
package/README.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# BugVaulty
|
|
2
|
+
|
|
3
|
+
BugVaulty automatically catches errors from Node.js, Express, and React apps, gets AI-generated fixes, and saves everything to Notion.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install bugvaulty
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Init API
|
|
12
|
+
|
|
13
|
+
Pass keys directly to `init()` or via environment variables for production safety.
|
|
14
|
+
|
|
15
|
+
Environment variable support:
|
|
16
|
+
- `BUGVAULTY_NOTION_TOKEN`
|
|
17
|
+
- `BUGVAULTY_NOTION_PAGE_ID`
|
|
18
|
+
- `BUGVAULTY_AI_PROVIDER`
|
|
19
|
+
- `BUGVAULTY_AI_API_KEY`
|
|
20
|
+
- `BUGVAULTY_CAPTURE_CONSOLE_ERRORS=true` (optional)
|
|
21
|
+
|
|
22
|
+
### Option 1: Groq
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
const bugvaulty = require('bugvaulty');
|
|
26
|
+
|
|
27
|
+
bugvaulty.init({
|
|
28
|
+
notionToken: 'ntn_xxx',
|
|
29
|
+
notionPageId: 'xxx',
|
|
30
|
+
ai: {
|
|
31
|
+
provider: 'groq',
|
|
32
|
+
apiKey: 'gsk_xxx'
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Option 2: OpenAI
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
bugvaulty.init({
|
|
41
|
+
notionToken: 'ntn_xxx',
|
|
42
|
+
notionPageId: 'xxx',
|
|
43
|
+
ai: {
|
|
44
|
+
provider: 'openai',
|
|
45
|
+
apiKey: 'sk-xxx'
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Option 3: Claude
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
bugvaulty.init({
|
|
54
|
+
notionToken: 'ntn_xxx',
|
|
55
|
+
notionPageId: 'xxx',
|
|
56
|
+
ai: {
|
|
57
|
+
provider: 'claude',
|
|
58
|
+
apiKey: 'sk-ant-xxx'
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Option 4: No AI (Notion only)
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
bugvaulty.init({
|
|
67
|
+
notionToken: 'ntn_xxx',
|
|
68
|
+
notionPageId: 'xxx'
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Validation behavior in `init()`
|
|
73
|
+
|
|
74
|
+
- Missing `notionToken` -> throws `Error("BugVaulty: notionToken is required")`
|
|
75
|
+
- Missing `notionPageId` -> throws `Error("BugVaulty: notionPageId is required")`
|
|
76
|
+
- `ai.provider` set but missing `ai.apiKey` -> warns:
|
|
77
|
+
- `[BugVaulty] Warning: AI provider set but no apiKey provided. Errors will be saved without AI analysis.`
|
|
78
|
+
- Unknown provider -> warns:
|
|
79
|
+
- `[BugVaulty] Warning: Unknown AI provider. Supported: groq, openai, claude`
|
|
80
|
+
|
|
81
|
+
On successful init:
|
|
82
|
+
|
|
83
|
+
```txt
|
|
84
|
+
[BugVaulty] Initialized successfully. Tracking errors to Notion.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Express integration
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
const express = require('express');
|
|
91
|
+
const bugvaulty = require('bugvaulty');
|
|
92
|
+
|
|
93
|
+
const app = express();
|
|
94
|
+
|
|
95
|
+
bugvaulty.init({
|
|
96
|
+
notionToken: 'ntn_xxx',
|
|
97
|
+
notionPageId: 'xxx',
|
|
98
|
+
ai: {
|
|
99
|
+
provider: 'groq',
|
|
100
|
+
apiKey: 'gsk_xxx'
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// add after your routes
|
|
105
|
+
app.use(bugvaulty.expressMiddleware());
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
BugVaulty automatically catches:
|
|
109
|
+
- `process.on('uncaughtException')`
|
|
110
|
+
- `process.on('unhandledRejection')`
|
|
111
|
+
- Express route errors via `expressMiddleware()`
|
|
112
|
+
|
|
113
|
+
## React integration
|
|
114
|
+
|
|
115
|
+
```jsx
|
|
116
|
+
import React from 'react';
|
|
117
|
+
import { BugVaultyProvider } from 'bugvaulty/react';
|
|
118
|
+
import App from './App';
|
|
119
|
+
|
|
120
|
+
export default function Root() {
|
|
121
|
+
return (
|
|
122
|
+
<BugVaultyProvider
|
|
123
|
+
keys={{
|
|
124
|
+
notionToken: 'ntn_xxx',
|
|
125
|
+
notionPageId: 'xxx',
|
|
126
|
+
ai: {
|
|
127
|
+
provider: 'groq',
|
|
128
|
+
apiKey: 'gsk_xxx'
|
|
129
|
+
}
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<App />
|
|
133
|
+
</BugVaultyProvider>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
If a component crashes, fallback UI shows:
|
|
139
|
+
|
|
140
|
+
```txt
|
|
141
|
+
Something went wrong. BugVaulty has logged this error.
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## What BugVaulty stores in Notion
|
|
145
|
+
|
|
146
|
+
For each error, BugVaulty will:
|
|
147
|
+
1. Detect project name from your `package.json` `name` or current folder name.
|
|
148
|
+
2. Find or create a project page under your root Notion page.
|
|
149
|
+
3. Create a child page with this structure:
|
|
150
|
+
|
|
151
|
+
```md
|
|
152
|
+
🐛 ErrorType: ErrorMessage
|
|
153
|
+
|
|
154
|
+
## 📋 Error Details
|
|
155
|
+
- 📅 Date
|
|
156
|
+
- ⏰ Time
|
|
157
|
+
- 📁 Path
|
|
158
|
+
- 📍 Line
|
|
159
|
+
|
|
160
|
+
## ❌ What Went Wrong
|
|
161
|
+
[AI explanation]
|
|
162
|
+
|
|
163
|
+
## 💡 Solution
|
|
164
|
+
[AI step-by-step solution]
|
|
165
|
+
|
|
166
|
+
## 🔧 Code Fix
|
|
167
|
+
[code block]
|
|
168
|
+
|
|
169
|
+
## 🚀 How To Avoid This
|
|
170
|
+
[AI prevention tips]
|
|
171
|
+
|
|
172
|
+
## ⭐ Difficulty: Beginner
|
|
173
|
+
## 🏷️ Tags: #React #TypeError
|
|
174
|
+
|
|
175
|
+
## 📄 Stack Trace
|
|
176
|
+
[raw stack trace code block]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## AI providers and APIs
|
|
180
|
+
|
|
181
|
+
- Groq
|
|
182
|
+
- URL: `https://api.groq.com/openai/v1/chat/completions`
|
|
183
|
+
- Model: `llama-3.3-70b-versatile`
|
|
184
|
+
- OpenAI
|
|
185
|
+
- URL: `https://api.openai.com/v1/chat/completions`
|
|
186
|
+
- Model: `gpt-4o`
|
|
187
|
+
- Claude (Anthropic)
|
|
188
|
+
- URL: `https://api.anthropic.com/v1/messages`
|
|
189
|
+
- Model: `claude-sonnet-4-20250514`
|
|
190
|
+
|
|
191
|
+
All providers normalize to this JSON shape:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"whatWentWrong": "simple explanation",
|
|
196
|
+
"solution": "step by step solution",
|
|
197
|
+
"codeFix": "corrected code",
|
|
198
|
+
"howToAvoid": "prevention tips",
|
|
199
|
+
"difficulty": "Beginner",
|
|
200
|
+
"tags": ["React", "TypeError"]
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Reliability and safety behavior
|
|
205
|
+
|
|
206
|
+
- BugVaulty never throws its own errors back to your app.
|
|
207
|
+
- If AI fails, BugVaulty still logs to Notion with fallback analysis.
|
|
208
|
+
- If Notion fails, BugVaulty logs an error and never crashes your app.
|
|
209
|
+
- Every error is grouped automatically by project name in Notion.
|
|
210
|
+
- Optional extended detection:
|
|
211
|
+
- `process.on('warning')`
|
|
212
|
+
- `process.on('multipleResolves')`
|
|
213
|
+
- `console.error` interception (enable via `capture.consoleErrors`)
|
|
214
|
+
|
|
215
|
+
Console messages:
|
|
216
|
+
|
|
217
|
+
```txt
|
|
218
|
+
[BugVaulty] Error tracked successfully → ProjectName/ErrorType
|
|
219
|
+
[BugVaulty] AI analysis failed, saving without solution.
|
|
220
|
+
[BugVaulty] Failed to save to Notion: {error message}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## API
|
|
224
|
+
|
|
225
|
+
### `init(options)`
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
init({
|
|
229
|
+
notionToken?: string,
|
|
230
|
+
notionPageId?: string,
|
|
231
|
+
ai?: {
|
|
232
|
+
provider: 'groq' | 'openai' | 'claude',
|
|
233
|
+
apiKey: string
|
|
234
|
+
},
|
|
235
|
+
capture?: {
|
|
236
|
+
processWarnings?: boolean,
|
|
237
|
+
multipleResolves?: boolean,
|
|
238
|
+
consoleErrors?: boolean
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
If `notionToken` and `notionPageId` are omitted, BugVaulty reads:
|
|
244
|
+
- `BUGVAULTY_NOTION_TOKEN`
|
|
245
|
+
- `BUGVAULTY_NOTION_PAGE_ID`
|
|
246
|
+
|
|
247
|
+
### `expressMiddleware()`
|
|
248
|
+
Returns standard Express error middleware `(err, req, res, next)`.
|
|
249
|
+
|
|
250
|
+
### `trackReactError(error, context?)`
|
|
251
|
+
Used internally by `BugVaultyProvider`, but exported for advanced cases.
|
|
252
|
+
|
|
253
|
+
## Notes
|
|
254
|
+
|
|
255
|
+
- Keys are stored only in memory from `init()` config.
|
|
256
|
+
- BugVaulty does not require manual error reporting after setup.
|
|
257
|
+
- To let BugVaulty create pages under a parent page, your integration must have access to that page in Notion.
|
|
258
|
+
- React integration requires `react` in your app.
|
|
259
|
+
# bug-vaulty
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bugvaulty",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Automatically track errors to Notion with AI solutions",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/Nitin-kumar-yadav1307/bug-vaulty.git"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/index.js",
|
|
10
|
+
"type": "commonjs",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.js",
|
|
13
|
+
"./react": "./react/index.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"react",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"notion",
|
|
22
|
+
"error-tracking",
|
|
23
|
+
"debugging",
|
|
24
|
+
"ai",
|
|
25
|
+
"mcp"
|
|
26
|
+
],
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@notionhq/client": "^2.2.15",
|
|
31
|
+
"axios": "^1.6.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": ">=16.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
const bugvaulty = require('../src');
|
|
3
|
+
|
|
4
|
+
class BugVaultyProvider extends React.Component {
|
|
5
|
+
constructor(props) {
|
|
6
|
+
super(props);
|
|
7
|
+
this.state = {
|
|
8
|
+
hasError: false,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
this.hasInitialized = false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
componentDidMount() {
|
|
15
|
+
this.initIfNeeded();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
componentDidUpdate(prevProps) {
|
|
19
|
+
if (prevProps.keys !== this.props.keys) {
|
|
20
|
+
this.initIfNeeded();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
componentDidCatch(error, info) {
|
|
25
|
+
this.setState({ hasError: true });
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
bugvaulty.trackReactError(error, {
|
|
29
|
+
source: 'reactErrorBoundary',
|
|
30
|
+
componentStack: info && info.componentStack,
|
|
31
|
+
});
|
|
32
|
+
} catch (_err) {
|
|
33
|
+
// Never crash host apps.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
initIfNeeded() {
|
|
38
|
+
if (this.hasInitialized) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
if (this.props.keys) {
|
|
44
|
+
bugvaulty.init(this.props.keys);
|
|
45
|
+
this.hasInitialized = true;
|
|
46
|
+
}
|
|
47
|
+
} catch (_err) {
|
|
48
|
+
// Never crash host apps.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
render() {
|
|
53
|
+
if (this.state.hasError) {
|
|
54
|
+
return React.createElement(
|
|
55
|
+
'div',
|
|
56
|
+
null,
|
|
57
|
+
'Something went wrong. BugVaulty has logged this error.'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return this.props.children;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
BugVaultyProvider,
|
|
67
|
+
};
|
package/react/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./ErrorBoundary.jsx');
|
package/src/aiService.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const { safeJsonParse, normalizeTags } = require('./utils');
|
|
3
|
+
|
|
4
|
+
const GROQ_API_URL = 'https://api.groq.com/openai/v1/chat/completions';
|
|
5
|
+
const GROQ_MODEL = 'llama-3.3-70b-versatile';
|
|
6
|
+
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
7
|
+
const OPENAI_MODEL = 'gpt-4o';
|
|
8
|
+
const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages';
|
|
9
|
+
const CLAUDE_MODEL = 'claude-sonnet-4-20250514';
|
|
10
|
+
|
|
11
|
+
function fallbackAnalysis() {
|
|
12
|
+
return {
|
|
13
|
+
whatWentWrong: 'No AI provider configured',
|
|
14
|
+
solution: 'Add an AI provider in bugvaulty.init() to get solutions',
|
|
15
|
+
codeFix: '',
|
|
16
|
+
howToAvoid: '',
|
|
17
|
+
difficulty: 'Unknown',
|
|
18
|
+
tags: [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sanitizeAnalysis(raw) {
|
|
23
|
+
const base = fallbackAnalysis();
|
|
24
|
+
if (!raw || typeof raw !== 'object') {
|
|
25
|
+
return base;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
whatWentWrong: String(raw.whatWentWrong || base.whatWentWrong),
|
|
30
|
+
solution: String(raw.solution || base.solution),
|
|
31
|
+
codeFix: String(raw.codeFix || base.codeFix),
|
|
32
|
+
howToAvoid: String(raw.howToAvoid || base.howToAvoid),
|
|
33
|
+
difficulty: String(raw.difficulty || base.difficulty),
|
|
34
|
+
tags: normalizeTags(raw.tags && Array.isArray(raw.tags) ? raw.tags : base.tags),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extractJsonFromText(message) {
|
|
39
|
+
if (typeof message !== 'string') {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fenced = message.match(/```json\s*([\s\S]*?)\s*```/i);
|
|
44
|
+
if (fenced && fenced[1]) {
|
|
45
|
+
return fenced[1].trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const firstBrace = message.indexOf('{');
|
|
49
|
+
const lastBrace = message.lastIndexOf('}');
|
|
50
|
+
|
|
51
|
+
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
|
52
|
+
return message.slice(firstBrace, lastBrace + 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return message;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildPrompt(errorData) {
|
|
59
|
+
return [
|
|
60
|
+
'Analyze this JavaScript/Node/React error and return JSON only.',
|
|
61
|
+
'Required keys: whatWentWrong, solution, codeFix, howToAvoid, difficulty, tags.',
|
|
62
|
+
'difficulty must be Beginner, Intermediate, or Advanced.',
|
|
63
|
+
'tags must be an array of short strings.',
|
|
64
|
+
'',
|
|
65
|
+
JSON.stringify(errorData, null, 2),
|
|
66
|
+
].join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function analyzeWithGroq(errorData, apiKey) {
|
|
70
|
+
const response = await axios.post(
|
|
71
|
+
GROQ_API_URL,
|
|
72
|
+
{
|
|
73
|
+
model: GROQ_MODEL,
|
|
74
|
+
temperature: 0.2,
|
|
75
|
+
messages: [
|
|
76
|
+
{
|
|
77
|
+
role: 'system',
|
|
78
|
+
content:
|
|
79
|
+
'You are an expert debugger. Always return valid JSON only with the exact requested keys.',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
role: 'user',
|
|
83
|
+
content: buildPrompt(errorData),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${apiKey}`,
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
},
|
|
92
|
+
timeout: 15000,
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
response &&
|
|
98
|
+
response.data &&
|
|
99
|
+
response.data.choices &&
|
|
100
|
+
response.data.choices[0] &&
|
|
101
|
+
response.data.choices[0].message &&
|
|
102
|
+
response.data.choices[0].message.content
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function analyzeWithOpenAI(errorData, apiKey) {
|
|
107
|
+
const response = await axios.post(
|
|
108
|
+
OPENAI_API_URL,
|
|
109
|
+
{
|
|
110
|
+
model: OPENAI_MODEL,
|
|
111
|
+
temperature: 0.2,
|
|
112
|
+
messages: [
|
|
113
|
+
{
|
|
114
|
+
role: 'system',
|
|
115
|
+
content:
|
|
116
|
+
'You are an expert debugger. Always return valid JSON only with the exact requested keys.',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
role: 'user',
|
|
120
|
+
content: buildPrompt(errorData),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
headers: {
|
|
126
|
+
Authorization: `Bearer ${apiKey}`,
|
|
127
|
+
'Content-Type': 'application/json',
|
|
128
|
+
},
|
|
129
|
+
timeout: 15000,
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
response &&
|
|
135
|
+
response.data &&
|
|
136
|
+
response.data.choices &&
|
|
137
|
+
response.data.choices[0] &&
|
|
138
|
+
response.data.choices[0].message &&
|
|
139
|
+
response.data.choices[0].message.content
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function analyzeWithClaude(errorData, apiKey) {
|
|
144
|
+
const response = await axios.post(
|
|
145
|
+
CLAUDE_API_URL,
|
|
146
|
+
{
|
|
147
|
+
model: CLAUDE_MODEL,
|
|
148
|
+
max_tokens: 700,
|
|
149
|
+
temperature: 0.2,
|
|
150
|
+
messages: [
|
|
151
|
+
{
|
|
152
|
+
role: 'user',
|
|
153
|
+
content: buildPrompt(errorData),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
headers: {
|
|
159
|
+
'x-api-key': apiKey,
|
|
160
|
+
'anthropic-version': '2023-06-01',
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
},
|
|
163
|
+
timeout: 15000,
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const content = response && response.data && response.data.content;
|
|
168
|
+
if (Array.isArray(content) && content[0] && content[0].text) {
|
|
169
|
+
return content[0].text;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function analyzeError(errorData, aiConfig) {
|
|
176
|
+
if (!aiConfig || !aiConfig.provider || !aiConfig.apiKey) {
|
|
177
|
+
return fallbackAnalysis();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const provider = String(aiConfig.provider).toLowerCase();
|
|
181
|
+
const apiKey = String(aiConfig.apiKey);
|
|
182
|
+
|
|
183
|
+
if (provider !== 'groq' && provider !== 'openai' && provider !== 'claude') {
|
|
184
|
+
return fallbackAnalysis();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
let content = null;
|
|
189
|
+
|
|
190
|
+
if (provider === 'groq') {
|
|
191
|
+
content = await analyzeWithGroq(errorData, apiKey);
|
|
192
|
+
} else if (provider === 'openai') {
|
|
193
|
+
content = await analyzeWithOpenAI(errorData, apiKey);
|
|
194
|
+
} else if (provider === 'claude') {
|
|
195
|
+
content = await analyzeWithClaude(errorData, apiKey);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const jsonText = extractJsonFromText(content || '');
|
|
199
|
+
const parsed = safeJsonParse(jsonText, null);
|
|
200
|
+
|
|
201
|
+
if (!parsed) {
|
|
202
|
+
console.warn('[BugVaulty] AI analysis failed, saving without solution.');
|
|
203
|
+
return fallbackAnalysis();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return sanitizeAnalysis(parsed);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.warn('[BugVaulty] AI analysis failed, saving without solution.');
|
|
209
|
+
return fallbackAnalysis();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
analyzeError,
|
|
215
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const registered = {
|
|
2
|
+
processHandlers: false,
|
|
3
|
+
warningHandler: false,
|
|
4
|
+
multipleResolvesHandler: false,
|
|
5
|
+
consoleErrorPatched: false,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
let originalConsoleError = null;
|
|
9
|
+
|
|
10
|
+
function registerProcessErrorHandlers(onError, captureOptions) {
|
|
11
|
+
const options = captureOptions || {};
|
|
12
|
+
|
|
13
|
+
if (registered.processHandlers) {
|
|
14
|
+
patchConsoleError(onError, options);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
process.on('uncaughtException', (error) => {
|
|
19
|
+
safeHandle(onError, error, { source: 'uncaughtException' });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
process.on('unhandledRejection', (reason) => {
|
|
23
|
+
safeHandle(onError, reason, { source: 'unhandledRejection' });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (options.processWarnings && !registered.warningHandler) {
|
|
27
|
+
process.on('warning', (warning) => {
|
|
28
|
+
safeHandle(onError, warning, { source: 'process.warning' });
|
|
29
|
+
});
|
|
30
|
+
registered.warningHandler = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (options.multipleResolves && !registered.multipleResolvesHandler) {
|
|
34
|
+
process.on('multipleResolves', (type, _promise, reason) => {
|
|
35
|
+
safeHandle(onError, reason || new Error(`multipleResolves: ${type}`), {
|
|
36
|
+
source: 'process.multipleResolves',
|
|
37
|
+
resolveType: type,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
registered.multipleResolvesHandler = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
patchConsoleError(onError, options);
|
|
44
|
+
registered.processHandlers = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function patchConsoleError(onError, options) {
|
|
48
|
+
if (!options.consoleErrors || registered.consoleErrorPatched) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
originalConsoleError = console.error;
|
|
53
|
+
console.error = function patchedConsoleError(...args) {
|
|
54
|
+
originalConsoleError.apply(console, args);
|
|
55
|
+
|
|
56
|
+
const firstError = args.find((arg) => arg instanceof Error);
|
|
57
|
+
const firstString = args.find((arg) => typeof arg === 'string');
|
|
58
|
+
|
|
59
|
+
const tracked = firstError || firstString || 'console.error called';
|
|
60
|
+
safeHandle(onError, tracked, {
|
|
61
|
+
source: 'console.error',
|
|
62
|
+
preview: firstString ? String(firstString).slice(0, 500) : '',
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
registered.consoleErrorPatched = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createExpressMiddleware(onError) {
|
|
70
|
+
return function bugvaultyExpressMiddleware(err, req, _res, next) {
|
|
71
|
+
if (err) {
|
|
72
|
+
safeHandle(onError, err, {
|
|
73
|
+
source: 'express',
|
|
74
|
+
method: req && req.method,
|
|
75
|
+
route: req && (req.originalUrl || req.url),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof next === 'function') {
|
|
80
|
+
next(err);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function safeHandle(onError, errorLike, context) {
|
|
86
|
+
try {
|
|
87
|
+
Promise.resolve(onError(errorLike, context)).catch(() => {
|
|
88
|
+
// Silent by design.
|
|
89
|
+
});
|
|
90
|
+
} catch (_err) {
|
|
91
|
+
// Silent by design.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
registerProcessErrorHandlers,
|
|
97
|
+
createExpressMiddleware,
|
|
98
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
const { registerProcessErrorHandlers, createExpressMiddleware } = require('./errorCatcher');
|
|
2
|
+
const { analyzeError } = require('./aiService');
|
|
3
|
+
const { NotionService } = require('./notionService');
|
|
4
|
+
const { normalizeError, getProjectName } = require('./utils');
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_PROVIDERS = ['groq', 'openai', 'claude'];
|
|
7
|
+
|
|
8
|
+
const state = {
|
|
9
|
+
initialized: false,
|
|
10
|
+
config: null,
|
|
11
|
+
projectName: null,
|
|
12
|
+
notionService: null,
|
|
13
|
+
trackedFingerprint: new Map(),
|
|
14
|
+
queue: Promise.resolve(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function resolveConfig(options) {
|
|
18
|
+
const input = options && typeof options === 'object' ? options : {};
|
|
19
|
+
const env = process.env || {};
|
|
20
|
+
|
|
21
|
+
const providerRaw =
|
|
22
|
+
(input.ai && input.ai.provider) || env.BUGVAULTY_AI_PROVIDER || null;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
notionToken: input.notionToken || env.BUGVAULTY_NOTION_TOKEN || null,
|
|
26
|
+
notionPageId: input.notionPageId || env.BUGVAULTY_NOTION_PAGE_ID || null,
|
|
27
|
+
ai: {
|
|
28
|
+
provider: providerRaw ? String(providerRaw).toLowerCase() : null,
|
|
29
|
+
apiKey:
|
|
30
|
+
(input.ai && input.ai.apiKey) || env.BUGVAULTY_AI_API_KEY || null,
|
|
31
|
+
},
|
|
32
|
+
capture: {
|
|
33
|
+
processWarnings:
|
|
34
|
+
input.capture && typeof input.capture.processWarnings === 'boolean'
|
|
35
|
+
? input.capture.processWarnings
|
|
36
|
+
: true,
|
|
37
|
+
multipleResolves:
|
|
38
|
+
input.capture && typeof input.capture.multipleResolves === 'boolean'
|
|
39
|
+
? input.capture.multipleResolves
|
|
40
|
+
: true,
|
|
41
|
+
consoleErrors:
|
|
42
|
+
input.capture && typeof input.capture.consoleErrors === 'boolean'
|
|
43
|
+
? input.capture.consoleErrors
|
|
44
|
+
: env.BUGVAULTY_CAPTURE_CONSOLE_ERRORS === 'true',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function validateOptions(config) {
|
|
50
|
+
if (!config || typeof config !== 'object') {
|
|
51
|
+
throw new Error('BugVaulty: notionToken is required');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!config.notionToken) {
|
|
55
|
+
throw new Error('BugVaulty: notionToken is required');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!config.notionPageId) {
|
|
59
|
+
throw new Error('BugVaulty: notionPageId is required');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const provider = config.ai && config.ai.provider ? String(config.ai.provider).toLowerCase() : null;
|
|
63
|
+
const apiKey = config.ai && config.ai.apiKey ? String(config.ai.apiKey) : '';
|
|
64
|
+
|
|
65
|
+
if (provider && !apiKey) {
|
|
66
|
+
console.warn(
|
|
67
|
+
'[BugVaulty] Warning: AI provider set but no apiKey provided. Errors will be saved without AI analysis.'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (provider && SUPPORTED_PROVIDERS.indexOf(provider) === -1) {
|
|
72
|
+
console.warn('[BugVaulty] Warning: Unknown AI provider. Supported: groq, openai, claude');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function init(options) {
|
|
77
|
+
const resolvedConfig = resolveConfig(options);
|
|
78
|
+
validateOptions(resolvedConfig);
|
|
79
|
+
state.config = resolvedConfig;
|
|
80
|
+
|
|
81
|
+
state.projectName = getProjectName();
|
|
82
|
+
state.notionService = new NotionService(state.config.notionToken, state.config.notionPageId);
|
|
83
|
+
|
|
84
|
+
registerProcessErrorHandlers(trackError, state.config.capture);
|
|
85
|
+
state.initialized = true;
|
|
86
|
+
|
|
87
|
+
console.log('[BugVaulty] Initialized successfully. Tracking errors to Notion.');
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
expressMiddleware,
|
|
91
|
+
trackReactError,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function expressMiddleware() {
|
|
96
|
+
return createExpressMiddleware(trackError);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getFingerprint(normalized) {
|
|
100
|
+
return [normalized.errorType, normalized.message, normalized.filePath, normalized.lineNumber].join('|');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isDuplicate(normalized) {
|
|
104
|
+
const fingerprint = getFingerprint(normalized);
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
const lastSeen = state.trackedFingerprint.get(fingerprint);
|
|
107
|
+
|
|
108
|
+
if (lastSeen && now - lastSeen < 1500) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
state.trackedFingerprint.set(fingerprint, now);
|
|
113
|
+
|
|
114
|
+
if (state.trackedFingerprint.size > 200) {
|
|
115
|
+
const firstKey = state.trackedFingerprint.keys().next().value;
|
|
116
|
+
state.trackedFingerprint.delete(firstKey);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function trackError(errorLike, context) {
|
|
123
|
+
state.queue = state.queue
|
|
124
|
+
.then(() => processTrackError(errorLike, context))
|
|
125
|
+
.catch(() => {
|
|
126
|
+
// Never allow tracking queue failures to bubble.
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return state.queue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getFallbackAnalysis() {
|
|
133
|
+
return {
|
|
134
|
+
whatWentWrong: 'No AI provider configured',
|
|
135
|
+
solution: 'Add an AI provider in bugvaulty.init() to get solutions',
|
|
136
|
+
codeFix: '',
|
|
137
|
+
howToAvoid: '',
|
|
138
|
+
difficulty: 'Unknown',
|
|
139
|
+
tags: [],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function processTrackError(errorLike, context) {
|
|
144
|
+
if (!state.initialized || !state.notionService) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const enrichedContext = {
|
|
149
|
+
...(context || {}),
|
|
150
|
+
runtime: {
|
|
151
|
+
pid: process.pid,
|
|
152
|
+
nodeVersion: process.version,
|
|
153
|
+
platform: process.platform,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const normalized = normalizeError(errorLike, enrichedContext);
|
|
158
|
+
if (isDuplicate(normalized)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let analysis;
|
|
163
|
+
try {
|
|
164
|
+
analysis = await analyzeError(
|
|
165
|
+
{
|
|
166
|
+
projectName: state.projectName,
|
|
167
|
+
errorType: normalized.errorType,
|
|
168
|
+
message: normalized.message,
|
|
169
|
+
stack: normalized.stack,
|
|
170
|
+
filePath: normalized.filePath,
|
|
171
|
+
lineNumber: normalized.lineNumber,
|
|
172
|
+
context: normalized.context,
|
|
173
|
+
},
|
|
174
|
+
state.config.ai
|
|
175
|
+
);
|
|
176
|
+
} catch (_err) {
|
|
177
|
+
analysis = getFallbackAnalysis();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const payload = {
|
|
181
|
+
...normalized,
|
|
182
|
+
analysis,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const projectPageId = await state.notionService.getOrCreateProjectPage(state.projectName);
|
|
187
|
+
const errorPageId = await state.notionService.createErrorPage(projectPageId, payload);
|
|
188
|
+
|
|
189
|
+
if (errorPageId) {
|
|
190
|
+
console.log(
|
|
191
|
+
`[BugVaulty] Error tracked successfully → ${state.projectName}/${normalized.errorType}`
|
|
192
|
+
);
|
|
193
|
+
} else {
|
|
194
|
+
console.warn('[BugVaulty] Failed to save to Notion: Unknown Notion error');
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
const message = err && err.message ? err.message : 'Unknown Notion error';
|
|
198
|
+
console.warn(`[BugVaulty] Failed to save to Notion: ${message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function trackReactError(errorLike, context) {
|
|
203
|
+
return trackError(errorLike, {
|
|
204
|
+
source: 'react',
|
|
205
|
+
...(context || {}),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
init,
|
|
211
|
+
expressMiddleware,
|
|
212
|
+
trackReactError,
|
|
213
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const { Client } = require('@notionhq/client');
|
|
2
|
+
|
|
3
|
+
class NotionService {
|
|
4
|
+
constructor(notionToken, rootPageId) {
|
|
5
|
+
this.notion = new Client({ auth: notionToken });
|
|
6
|
+
this.rootPageId = rootPageId;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async getOrCreateProjectPage(projectName) {
|
|
10
|
+
try {
|
|
11
|
+
const search = await this.notion.search({
|
|
12
|
+
query: projectName,
|
|
13
|
+
filter: {
|
|
14
|
+
property: 'object',
|
|
15
|
+
value: 'page',
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const exactMatch = (search.results || []).find((page) => {
|
|
20
|
+
const title = this.extractPageTitle(page);
|
|
21
|
+
const parentId = page && page.parent && page.parent.page_id;
|
|
22
|
+
return title === projectName && parentId === this.rootPageId;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (exactMatch) {
|
|
26
|
+
return exactMatch.id;
|
|
27
|
+
}
|
|
28
|
+
} catch (_err) {
|
|
29
|
+
// Continue and try create flow.
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const created = await this.notion.pages.create({
|
|
34
|
+
parent: { page_id: this.rootPageId },
|
|
35
|
+
properties: {
|
|
36
|
+
title: {
|
|
37
|
+
title: [
|
|
38
|
+
{
|
|
39
|
+
text: {
|
|
40
|
+
content: projectName,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return created.id;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
extractPageTitle(page) {
|
|
55
|
+
if (!page || !page.properties) {
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const titleProperty = Object.values(page.properties).find(
|
|
60
|
+
(property) => property && property.type === 'title'
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const parts = titleProperty && Array.isArray(titleProperty.title) ? titleProperty.title : [];
|
|
64
|
+
return parts.map((part) => (part && part.plain_text ? part.plain_text : '')).join('').trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async createErrorPage(projectPageId, payload) {
|
|
68
|
+
if (!projectPageId) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const cleanMessage = String(payload.message || 'No message').slice(0, 50);
|
|
73
|
+
const title = `🐛 ${payload.errorType}: ${cleanMessage}`;
|
|
74
|
+
const tags = Array.isArray(payload.analysis.tags) ? payload.analysis.tags : [];
|
|
75
|
+
const tagLine = tags.map((tag) => `#${String(tag).replace(/\s+/g, '')}`).join(' ');
|
|
76
|
+
|
|
77
|
+
const blocks = [
|
|
78
|
+
this.heading('📋 Error Details'),
|
|
79
|
+
this.bullet(`📅 Date: ${payload.date}`),
|
|
80
|
+
this.bullet(`⏰ Time: ${payload.time}`),
|
|
81
|
+
this.bullet(`📁 Path: ${payload.filePath || 'Unknown'}`),
|
|
82
|
+
this.bullet(`📍 Line: ${payload.lineNumber || 'Unknown'}`),
|
|
83
|
+
this.heading('❌ What Went Wrong'),
|
|
84
|
+
this.paragraph(payload.analysis.whatWentWrong),
|
|
85
|
+
this.heading('💡 Solution'),
|
|
86
|
+
this.paragraph(payload.analysis.solution),
|
|
87
|
+
this.heading('🔧 Code Fix'),
|
|
88
|
+
this.code(payload.analysis.codeFix),
|
|
89
|
+
this.heading('🚀 How To Avoid This'),
|
|
90
|
+
this.paragraph(payload.analysis.howToAvoid),
|
|
91
|
+
this.heading(`⭐ Difficulty: ${payload.analysis.difficulty}`),
|
|
92
|
+
this.heading(`🏷️ Tags: ${tagLine || 'None'}`),
|
|
93
|
+
this.heading('📄 Stack Trace'),
|
|
94
|
+
this.code(payload.stack || 'No stack trace'),
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const response = await this.notion.pages.create({
|
|
99
|
+
parent: { page_id: projectPageId },
|
|
100
|
+
properties: {
|
|
101
|
+
title: {
|
|
102
|
+
title: [
|
|
103
|
+
{
|
|
104
|
+
text: {
|
|
105
|
+
content: title,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
children: blocks,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return response.id;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
heading(content) {
|
|
121
|
+
return {
|
|
122
|
+
object: 'block',
|
|
123
|
+
type: 'heading_2',
|
|
124
|
+
heading_2: {
|
|
125
|
+
rich_text: [
|
|
126
|
+
{
|
|
127
|
+
type: 'text',
|
|
128
|
+
text: { content: String(content || '') },
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
paragraph(content) {
|
|
136
|
+
return {
|
|
137
|
+
object: 'block',
|
|
138
|
+
type: 'paragraph',
|
|
139
|
+
paragraph: {
|
|
140
|
+
rich_text: [
|
|
141
|
+
{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: { content: String(content || '') },
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
bullet(content) {
|
|
151
|
+
return {
|
|
152
|
+
object: 'block',
|
|
153
|
+
type: 'bulleted_list_item',
|
|
154
|
+
bulleted_list_item: {
|
|
155
|
+
rich_text: [
|
|
156
|
+
{
|
|
157
|
+
type: 'text',
|
|
158
|
+
text: { content: String(content || '') },
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
code(content) {
|
|
166
|
+
return {
|
|
167
|
+
object: 'block',
|
|
168
|
+
type: 'code',
|
|
169
|
+
code: {
|
|
170
|
+
rich_text: [
|
|
171
|
+
{
|
|
172
|
+
type: 'text',
|
|
173
|
+
text: { content: String(content || '') },
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
language: 'javascript',
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
NotionService,
|
|
184
|
+
};
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function getProjectName() {
|
|
5
|
+
const packagePath = path.join(process.cwd(), 'package.json');
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
if (fs.existsSync(packagePath)) {
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
10
|
+
if (pkg && typeof pkg.name === 'string' && pkg.name.trim()) {
|
|
11
|
+
return pkg.name.trim();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
} catch (_err) {
|
|
15
|
+
// Silent by design: BugVaulty should never break host apps.
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return path.basename(process.cwd());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseStackTrace(stack) {
|
|
22
|
+
if (!stack || typeof stack !== 'string') {
|
|
23
|
+
return { filePath: 'Unknown', lineNumber: null };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const lines = stack.split('\n').map((line) => line.trim());
|
|
27
|
+
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const withParens = line.match(/\((.+):(\d+):(\d+)\)/);
|
|
30
|
+
if (withParens) {
|
|
31
|
+
return {
|
|
32
|
+
filePath: withParens[1],
|
|
33
|
+
lineNumber: Number(withParens[2]),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const withoutParens = line.match(/at (.+):(\d+):(\d+)/);
|
|
38
|
+
if (withoutParens) {
|
|
39
|
+
return {
|
|
40
|
+
filePath: withoutParens[1],
|
|
41
|
+
lineNumber: Number(withoutParens[2]),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { filePath: 'Unknown', lineNumber: null };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getCurrentDateTime(dateInput) {
|
|
50
|
+
const date = dateInput instanceof Date ? dateInput : new Date();
|
|
51
|
+
|
|
52
|
+
const dateStr = date.toISOString().slice(0, 10);
|
|
53
|
+
const timeStr = date.toLocaleTimeString('en-US', {
|
|
54
|
+
hour12: true,
|
|
55
|
+
hour: '2-digit',
|
|
56
|
+
minute: '2-digit',
|
|
57
|
+
second: '2-digit',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return { date: dateStr, time: timeStr };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toError(reason) {
|
|
64
|
+
if (reason instanceof Error) {
|
|
65
|
+
return reason;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof reason === 'string') {
|
|
69
|
+
return new Error(reason);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
return new Error(JSON.stringify(reason));
|
|
74
|
+
} catch (_err) {
|
|
75
|
+
return new Error('Unknown error reason');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeError(inputError, context) {
|
|
80
|
+
const error = toError(inputError);
|
|
81
|
+
const stackInfo = parseStackTrace(error.stack || '');
|
|
82
|
+
const now = new Date();
|
|
83
|
+
const dateParts = getCurrentDateTime(now);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
errorType: error.name || 'Error',
|
|
87
|
+
message: error.message || 'No error message',
|
|
88
|
+
stack: error.stack || '',
|
|
89
|
+
filePath: stackInfo.filePath,
|
|
90
|
+
lineNumber: stackInfo.lineNumber,
|
|
91
|
+
date: dateParts.date,
|
|
92
|
+
time: dateParts.time,
|
|
93
|
+
timestamp: now.toISOString(),
|
|
94
|
+
context: context || {},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function safeJsonParse(value, fallback) {
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(value);
|
|
101
|
+
} catch (_err) {
|
|
102
|
+
return fallback;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeTags(tags) {
|
|
107
|
+
if (!Array.isArray(tags)) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return tags
|
|
112
|
+
.map((tag) => String(tag).trim())
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.slice(0, 8);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
getProjectName,
|
|
119
|
+
parseStackTrace,
|
|
120
|
+
getCurrentDateTime,
|
|
121
|
+
normalizeError,
|
|
122
|
+
safeJsonParse,
|
|
123
|
+
normalizeTags,
|
|
124
|
+
};
|