sniplog 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 +294 -0
- package/package.json +43 -0
- package/src/browser.js +144 -0
- package/src/index.js +18 -0
- package/src/node.js +326 -0
package/README.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# SnipLog SDK
|
|
2
|
+
|
|
3
|
+
Enterprise-grade error tracking SDK for Node.js/Express backends and browser frontends.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install sniplog-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Node.js / Express Backend (Current Focus)
|
|
14
|
+
|
|
15
|
+
#### Basic Setup
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
const express = require('express');
|
|
19
|
+
const SnipLog = require('sniplog-sdk');
|
|
20
|
+
|
|
21
|
+
const app = express();
|
|
22
|
+
|
|
23
|
+
// Initialize SnipLog
|
|
24
|
+
const sniplog = new SnipLog({
|
|
25
|
+
endpoint: 'http://localhost:3000/api/errors',
|
|
26
|
+
projectKey: 'your-project-key',
|
|
27
|
+
autoCaptureExceptions: true, // Auto-capture uncaught exceptions
|
|
28
|
+
timeout: 5000
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Add request middleware (adds req.sniplog helper)
|
|
32
|
+
app.use(sniplog.requestMiddleware());
|
|
33
|
+
|
|
34
|
+
// Your routes here...
|
|
35
|
+
|
|
36
|
+
// Add error middleware - MUST be after all routes
|
|
37
|
+
app.use(sniplog.errorMiddleware());
|
|
38
|
+
|
|
39
|
+
app.listen(3000);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### Configuration Options
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
const sniplog = new SnipLog({
|
|
46
|
+
endpoint: 'http://localhost:3000/api/errors', // SnipLog backend URL
|
|
47
|
+
projectKey: 'your-api-key', // Project API key
|
|
48
|
+
autoCaptureExceptions: true, // Auto-capture process errors (default: true)
|
|
49
|
+
enabled: true, // Enable/disable SDK (default: true)
|
|
50
|
+
timeout: 5000, // Request timeout in ms (default: 5000)
|
|
51
|
+
discordWebhook: 'https://discord.com/api/webhooks/...' // Optional: Discord webhook URL
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Manual Error Capture
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
app.get('/api/data', async (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
const data = await fetchData();
|
|
61
|
+
res.json(data);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// Capture error with context
|
|
64
|
+
req.sniplog.captureError(err, {
|
|
65
|
+
userId: req.user?.id,
|
|
66
|
+
operation: 'fetch-data',
|
|
67
|
+
endpoint: '/api/data'
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
res.status(500).json({ error: 'Failed to fetch data' });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Capture Messages (Non-Errors)
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
app.post('/api/users', (req, res) => {
|
|
79
|
+
req.sniplog.captureMessage('New user registration attempt', {
|
|
80
|
+
level: 'info',
|
|
81
|
+
email: req.body.email
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ... handle registration
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### Express Middleware Features
|
|
89
|
+
|
|
90
|
+
**Request Middleware** (`sniplog.requestMiddleware()`)
|
|
91
|
+
- Adds `req.sniplog.captureError()` and `req.sniplog.captureMessage()` helpers
|
|
92
|
+
- Automatically includes request context (method, URL, IP, user-agent)
|
|
93
|
+
|
|
94
|
+
**Error Middleware** (`sniplog.errorMiddleware()`)
|
|
95
|
+
- Captures all errors that reach Express error handlers
|
|
96
|
+
- Includes full request context
|
|
97
|
+
- Must be placed AFTER all routes and middleware
|
|
98
|
+
|
|
99
|
+
#### Auto-Capture Features
|
|
100
|
+
|
|
101
|
+
When `autoCaptureExceptions: true`:
|
|
102
|
+
- `uncaughtException` events are captured
|
|
103
|
+
- `unhandledRejection` events are captured
|
|
104
|
+
|
|
105
|
+
Each captured error includes:
|
|
106
|
+
- Error message and stack trace
|
|
107
|
+
- System info (Node version, OS, hostname)
|
|
108
|
+
- Process ID and session ID
|
|
109
|
+
- Custom metadata you provide
|
|
110
|
+
|
|
111
|
+
#### Example: Complete Express App
|
|
112
|
+
|
|
113
|
+
See `example-express-app.js` for a full working example.
|
|
114
|
+
|
|
115
|
+
To run the example:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Start the SnipLog backend first (in another terminal)
|
|
119
|
+
cd backend
|
|
120
|
+
npm start
|
|
121
|
+
|
|
122
|
+
# Run the example app
|
|
123
|
+
cd sdk
|
|
124
|
+
node example-express-app.js
|
|
125
|
+
|
|
126
|
+
# Test error capture
|
|
127
|
+
curl http://localhost:4000/test-error
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### Browser / Frontend (Coming Soon)
|
|
133
|
+
|
|
134
|
+
For frontend integration, include `browser.js` directly:
|
|
135
|
+
|
|
136
|
+
```html
|
|
137
|
+
<script src="/path/to/sniplog-sdk/src/browser.js"></script>
|
|
138
|
+
<script>
|
|
139
|
+
SnipLog.init({
|
|
140
|
+
endpoint: 'http://localhost:3000/api/errors',
|
|
141
|
+
projectKey: 'your-project-key'
|
|
142
|
+
});
|
|
143
|
+
</script>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The browser SDK will automatically capture:
|
|
147
|
+
- `window.onerror` (script errors)
|
|
148
|
+
- `unhandledrejection` (promise rejections)
|
|
149
|
+
- `console.error` (optional)
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## API Reference
|
|
154
|
+
|
|
155
|
+
### Node.js SDK
|
|
156
|
+
|
|
157
|
+
#### `new SnipLog(config)`
|
|
158
|
+
|
|
159
|
+
Creates a new SnipLog instance.
|
|
160
|
+
|
|
161
|
+
**Parameters:**
|
|
162
|
+
- `config.endpoint` (string): SnipLog backend URL
|
|
163
|
+
- `config.projectKey` (string): Your project API key
|
|
164
|
+
- `config.autoCaptureExceptions` (boolean): Auto-capture process errors (default: true)
|
|
165
|
+
- `config.enabled` (boolean): Enable/disable SDK (default: true)
|
|
166
|
+
- `config.timeout` (number): Request timeout in ms (default: 5000)
|
|
167
|
+
- `config.discordWebhook` (string): Optional Discord webhook URL for real-time notifications
|
|
168
|
+
|
|
169
|
+
#### `sniplog.captureError(error, metadata)`
|
|
170
|
+
|
|
171
|
+
Manually capture an error.
|
|
172
|
+
|
|
173
|
+
**Parameters:**
|
|
174
|
+
- `error` (Error): The error object
|
|
175
|
+
- `metadata` (object): Additional context (userId, operation, etc.)
|
|
176
|
+
|
|
177
|
+
#### `sniplog.captureMessage(message, metadata)`
|
|
178
|
+
|
|
179
|
+
Capture a non-error message/event.
|
|
180
|
+
|
|
181
|
+
**Parameters:**
|
|
182
|
+
- `message` (string): The message
|
|
183
|
+
- `metadata` (object): Additional context
|
|
184
|
+
|
|
185
|
+
#### `sniplog.requestMiddleware()`
|
|
186
|
+
|
|
187
|
+
Returns Express middleware that adds `req.sniplog` helpers.
|
|
188
|
+
|
|
189
|
+
#### `sniplog.errorMiddleware()`
|
|
190
|
+
|
|
191
|
+
Returns Express error handling middleware. Place after all routes.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Discord Webhook Integration
|
|
196
|
+
|
|
197
|
+
SnipLog supports real-time error notifications via Discord webhooks. Errors will be sent to both the backend database AND your Discord channel.
|
|
198
|
+
|
|
199
|
+
### Setup Discord Webhook
|
|
200
|
+
|
|
201
|
+
1. Open Discord and go to your server
|
|
202
|
+
2. Go to **Server Settings** > **Integrations** > **Webhooks**
|
|
203
|
+
3. Click **New Webhook** or **Create Webhook**
|
|
204
|
+
4. Copy the **Webhook URL**
|
|
205
|
+
|
|
206
|
+
### SDK Configuration
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
const sniplog = new SnipLog({
|
|
210
|
+
endpoint: 'http://localhost:3000/api/errors',
|
|
211
|
+
projectKey: 'your-api-key',
|
|
212
|
+
discordWebhook: 'https://discord.com/api/webhooks/1234567890/your-webhook-token'
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Backend Configuration
|
|
217
|
+
|
|
218
|
+
Alternatively, configure Discord webhook in the backend (all projects will use it):
|
|
219
|
+
|
|
220
|
+
**.env file:**
|
|
221
|
+
```bash
|
|
222
|
+
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/1234567890/your-webhook-token
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Discord Message Format
|
|
226
|
+
|
|
227
|
+
When an error occurs, you'll receive a Discord message like:
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
🚨 **expressError**: Cannot read property 'id' of undefined
|
|
231
|
+
📅 **Time**: 10/31/2025, 3:45:12 PM
|
|
232
|
+
🔗 **URL**: /api/users/123
|
|
233
|
+
📍 **Method**: GET
|
|
234
|
+
💻 **Host**: production-server
|
|
235
|
+
👤 **User**: user-456
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
Error: Cannot read property 'id' of undefined
|
|
239
|
+
at getUserProfile (/app/routes/users.js:45:20)
|
|
240
|
+
at Layer.handle [as handle_request]
|
|
241
|
+
at next (/app/node_modules/express/lib/router/route.js:137:13)
|
|
242
|
+
```
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Features
|
|
246
|
+
|
|
247
|
+
- ✅ Real-time notifications in Discord
|
|
248
|
+
- ✅ Formatted with emojis and markdown
|
|
249
|
+
- ✅ Includes error message, stack trace, and metadata
|
|
250
|
+
- ✅ Truncated to fit Discord's 2000 character limit
|
|
251
|
+
- ✅ Works for both SDK and backend configurations
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Development
|
|
256
|
+
|
|
257
|
+
### Project Structure
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
sdk/
|
|
261
|
+
├── src/
|
|
262
|
+
│ ├── index.js # Auto-detects environment (Node/browser)
|
|
263
|
+
│ ├── node.js # Node.js/Express SDK
|
|
264
|
+
│ └── browser.js # Browser SDK (for future frontend use)
|
|
265
|
+
├── example-express-app.js
|
|
266
|
+
├── package.json
|
|
267
|
+
└── README.md
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Testing Locally
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# Link SDK locally
|
|
274
|
+
cd sdk
|
|
275
|
+
npm link
|
|
276
|
+
|
|
277
|
+
# Use in your project
|
|
278
|
+
cd your-express-app
|
|
279
|
+
npm link sniplog-sdk
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Contributing
|
|
283
|
+
|
|
284
|
+
We welcome contributions! If you'd like to improve the SDK, please open an issue or submit a pull request.
|
|
285
|
+
|
|
286
|
+
For details, see our [CONTRIBUTING.md](./CONTRIBUTING.md) guide.
|
|
287
|
+
|
|
288
|
+
Thanks for helping make SnipLog better!
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sniplog",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SnipLog SDK for error tracking - supports both Node.js/Express backend and browser frontend",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"No tests configured\" && exit 0"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"error-tracking",
|
|
11
|
+
"crash-logging",
|
|
12
|
+
"sniplog",
|
|
13
|
+
"express-middleware",
|
|
14
|
+
"error-monitoring"
|
|
15
|
+
],
|
|
16
|
+
"author": "ikislay, 0xgajendra",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"files": [
|
|
19
|
+
"src/index.js",
|
|
20
|
+
"src/node.js",
|
|
21
|
+
"src/browser.js",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./src/index.js",
|
|
26
|
+
"./node": "./src/node.js",
|
|
27
|
+
"./browser": "./src/browser.js"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=14.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"express": "^4.18.2"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/iKislay/sniplog.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/iKislay/sniplog/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/iKislay/sniplog#readme"
|
|
43
|
+
}
|
package/src/browser.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/*
|
|
2
|
+
SnipLog SDK (minimal, zero-dependency)
|
|
3
|
+
Usage:
|
|
4
|
+
<script src="/path/to/snipsdk.js"></script>
|
|
5
|
+
<script>
|
|
6
|
+
SnipLog.init({ endpoint: 'http://localhost:3000/api/errors', projectKey: 'dev-project-key' });
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
The SDK captures: window.onerror, unhandledrejection, and console.error (best-effort)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
;(function (global) {
|
|
13
|
+
const DEFAULT_ENDPOINT = 'http://localhost:3001/api/errors';
|
|
14
|
+
|
|
15
|
+
function uuidv4() {
|
|
16
|
+
// simple UUID (RFC4122 v4-like)
|
|
17
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
18
|
+
const r = (Math.random() * 16) | 0;
|
|
19
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
20
|
+
return v.toString(16);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function safeStringify(obj) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(obj);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return String(obj);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function detectDevice() {
|
|
33
|
+
const ua = navigator.userAgent || '';
|
|
34
|
+
if (/Mobi|Android/i.test(ua)) return 'mobile';
|
|
35
|
+
if (/Tablet|iPad/i.test(ua)) return 'tablet';
|
|
36
|
+
return 'desktop';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function send(payload, opts) {
|
|
40
|
+
const endpoint = (opts && opts.endpoint) || DEFAULT_ENDPOINT;
|
|
41
|
+
const projectKey = (opts && opts.projectKey) || '';
|
|
42
|
+
try {
|
|
43
|
+
const body = safeStringify(payload);
|
|
44
|
+
// Use fetch if available
|
|
45
|
+
if (typeof fetch === 'function') {
|
|
46
|
+
fetch(endpoint, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
Authorization: `Bearer ${projectKey}`
|
|
51
|
+
},
|
|
52
|
+
body
|
|
53
|
+
}).catch(() => {
|
|
54
|
+
// swallow network errors
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fallback to XHR
|
|
60
|
+
const xhr = new XMLHttpRequest();
|
|
61
|
+
xhr.open('POST', endpoint, true);
|
|
62
|
+
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
63
|
+
xhr.setRequestHeader('Authorization', `Bearer ${projectKey}`);
|
|
64
|
+
xhr.send(body);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
// ignore
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const SnipLog = {
|
|
71
|
+
_opts: { endpoint: DEFAULT_ENDPOINT, projectKey: '' },
|
|
72
|
+
_sessionId: uuidv4(),
|
|
73
|
+
init: function (opts) {
|
|
74
|
+
this._opts = Object.assign({}, this._opts, opts || {});
|
|
75
|
+
// capture window.onerror
|
|
76
|
+
if (typeof window !== 'undefined') {
|
|
77
|
+
const self = this;
|
|
78
|
+
window.addEventListener('error', function (ev) {
|
|
79
|
+
try {
|
|
80
|
+
const e = ev.error || {};
|
|
81
|
+
const payload = {
|
|
82
|
+
message: e && e.message ? e.message : (ev.message || 'window.error'),
|
|
83
|
+
stack: (e && e.stack) || `${ev.filename}:${ev.lineno}:${ev.colno}`,
|
|
84
|
+
url: window.location.href,
|
|
85
|
+
line: ev.lineno || null,
|
|
86
|
+
column: ev.colno || null,
|
|
87
|
+
browser: { userAgent: navigator.userAgent, platform: navigator.platform },
|
|
88
|
+
device: detectDevice(),
|
|
89
|
+
sessionId: self._sessionId,
|
|
90
|
+
metadata: {},
|
|
91
|
+
ts: new Date().toISOString()
|
|
92
|
+
};
|
|
93
|
+
send(payload, self._opts);
|
|
94
|
+
} catch (ignore) {}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// unhandledrejection
|
|
98
|
+
window.addEventListener('unhandledrejection', function (ev) {
|
|
99
|
+
try {
|
|
100
|
+
const reason = ev.reason;
|
|
101
|
+
const payload = {
|
|
102
|
+
message: reason && reason.message ? reason.message : String(reason || 'unhandledrejection'),
|
|
103
|
+
stack: (reason && reason.stack) || '',
|
|
104
|
+
url: window.location.href,
|
|
105
|
+
browser: { userAgent: navigator.userAgent, platform: navigator.platform },
|
|
106
|
+
device: detectDevice(),
|
|
107
|
+
sessionId: self._sessionId,
|
|
108
|
+
metadata: { type: 'promise' },
|
|
109
|
+
ts: new Date().toISOString()
|
|
110
|
+
};
|
|
111
|
+
send(payload, self._opts);
|
|
112
|
+
} catch (ignore) {}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// console.error wrapper (best-effort, does not break app)
|
|
116
|
+
try {
|
|
117
|
+
const origConsoleError = console.error.bind(console);
|
|
118
|
+
console.error = function () {
|
|
119
|
+
try {
|
|
120
|
+
const args = Array.prototype.slice.call(arguments);
|
|
121
|
+
const payload = {
|
|
122
|
+
message: 'console.error',
|
|
123
|
+
stack: '',
|
|
124
|
+
url: window.location.href,
|
|
125
|
+
browser: { userAgent: navigator.userAgent, platform: navigator.platform },
|
|
126
|
+
device: detectDevice(),
|
|
127
|
+
sessionId: self._sessionId,
|
|
128
|
+
metadata: { console: args.map(a => (typeof a === 'object' ? safeStringify(a) : String(a))).join(' | ') },
|
|
129
|
+
ts: new Date().toISOString()
|
|
130
|
+
};
|
|
131
|
+
send(payload, self._opts);
|
|
132
|
+
} catch (ignore) {}
|
|
133
|
+
// always call original
|
|
134
|
+
origConsoleError.apply(null, arguments);
|
|
135
|
+
};
|
|
136
|
+
} catch (ignore) {}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// expose globally
|
|
142
|
+
global.SnipLog = SnipLog;
|
|
143
|
+
})(typeof window !== 'undefined' ? window : this);
|
|
144
|
+
|
package/src/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SnipLog SDK - Universal Entry Point
|
|
3
|
+
* Auto-detects environment and exports appropriate SDK
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Detect if running in Node.js environment
|
|
7
|
+
const isNode = typeof process !== 'undefined' &&
|
|
8
|
+
process.versions != null &&
|
|
9
|
+
process.versions.node != null;
|
|
10
|
+
|
|
11
|
+
if (isNode) {
|
|
12
|
+
// Export Node.js version for backend/Express use
|
|
13
|
+
module.exports = require('./node');
|
|
14
|
+
} else {
|
|
15
|
+
// Browser environment - this should not be reached in normal Node usage
|
|
16
|
+
// For browser, users should include browser.js directly via <script> tag
|
|
17
|
+
throw new Error('SnipLog: Use browser.js for frontend integration via <script> tag');
|
|
18
|
+
}
|
package/src/node.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SnipLog SDK - Node.js/Express Backend Version
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const SnipLog = require('sniplog-sdk/node');
|
|
6
|
+
* const sniplog = new SnipLog({ endpoint: 'http://localhost:3000/api/errors', projectKey: 'your-key' });
|
|
7
|
+
*
|
|
8
|
+
* // Express middleware (captures uncaught errors)
|
|
9
|
+
* app.use(sniplog.errorMiddleware());
|
|
10
|
+
*
|
|
11
|
+
* // Manual capture
|
|
12
|
+
* sniplog.captureError(new Error('something failed'), { userId: '123' });
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const http = require('http');
|
|
16
|
+
const https = require('https');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
|
|
20
|
+
class SnipLog {
|
|
21
|
+
constructor(config = {}) {
|
|
22
|
+
this.endpoint = config.endpoint || 'http://localhost:3001/api/errors';
|
|
23
|
+
this.projectKey = config.projectKey || '';
|
|
24
|
+
this.webhookUrl = config.webhookUrl || config.discordWebhook || ''; // Discord webhook URL
|
|
25
|
+
this.sessionId = this._generateSessionId();
|
|
26
|
+
this.enabled = config.enabled !== false;
|
|
27
|
+
this.timeout = config.timeout || 5000;
|
|
28
|
+
|
|
29
|
+
// Auto-capture process-level errors
|
|
30
|
+
if (config.autoCaptureExceptions !== false) {
|
|
31
|
+
this._attachGlobalHandlers();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_generateSessionId() {
|
|
36
|
+
return crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_attachGlobalHandlers() {
|
|
40
|
+
// Capture uncaught exceptions
|
|
41
|
+
process.on('uncaughtException', (err) => {
|
|
42
|
+
this.captureError(err, { type: 'uncaughtException' });
|
|
43
|
+
// Allow process to continue or exit based on your policy
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Capture unhandled promise rejections
|
|
47
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
48
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
49
|
+
this.captureError(err, { type: 'unhandledRejection' });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Capture an error manually
|
|
55
|
+
* @param {Error} error - The error object
|
|
56
|
+
* @param {Object} metadata - Additional context
|
|
57
|
+
*/
|
|
58
|
+
captureError(error, metadata = {}) {
|
|
59
|
+
if (!this.enabled) return;
|
|
60
|
+
|
|
61
|
+
const payload = this._buildPayload(error, metadata);
|
|
62
|
+
this._send(payload);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Capture a message (non-error event)
|
|
67
|
+
* @param {string} message - The message
|
|
68
|
+
* @param {Object} metadata - Additional context
|
|
69
|
+
*/
|
|
70
|
+
captureMessage(message, metadata = {}) {
|
|
71
|
+
if (!this.enabled) return;
|
|
72
|
+
|
|
73
|
+
const payload = {
|
|
74
|
+
message: message,
|
|
75
|
+
stack: '',
|
|
76
|
+
url: metadata.url || '',
|
|
77
|
+
browser: this._getSystemInfo(),
|
|
78
|
+
device: 'server',
|
|
79
|
+
sessionId: this.sessionId,
|
|
80
|
+
metadata: { ...metadata, level: metadata.level || 'info' },
|
|
81
|
+
ts: new Date().toISOString()
|
|
82
|
+
};
|
|
83
|
+
this._send(payload);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_buildPayload(error, metadata = {}) {
|
|
87
|
+
return {
|
|
88
|
+
message: error.message || 'Unknown error',
|
|
89
|
+
stack: error.stack || '',
|
|
90
|
+
url: metadata.url || '',
|
|
91
|
+
line: this._extractLineNumber(error),
|
|
92
|
+
column: this._extractColumnNumber(error),
|
|
93
|
+
browser: this._getSystemInfo(),
|
|
94
|
+
device: 'server',
|
|
95
|
+
sessionId: this.sessionId,
|
|
96
|
+
metadata: {
|
|
97
|
+
...metadata,
|
|
98
|
+
errorName: error.name,
|
|
99
|
+
hostname: os.hostname(),
|
|
100
|
+
pid: process.pid
|
|
101
|
+
},
|
|
102
|
+
ts: new Date().toISOString()
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_extractLineNumber(error) {
|
|
107
|
+
if (!error.stack) return null;
|
|
108
|
+
const match = error.stack.match(/:(\d+)(?::(\d+))?\)?$/m);
|
|
109
|
+
return match ? parseInt(match[1], 10) : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_extractColumnNumber(error) {
|
|
113
|
+
if (!error.stack) return null;
|
|
114
|
+
const match = error.stack.match(/:(\d+):(\d+)\)?$/m);
|
|
115
|
+
return match ? parseInt(match[2], 10) : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_getSystemInfo() {
|
|
119
|
+
return {
|
|
120
|
+
userAgent: `Node.js/${process.version}`,
|
|
121
|
+
platform: `${os.platform()} ${os.release()}`,
|
|
122
|
+
arch: os.arch(),
|
|
123
|
+
nodeVersion: process.version
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_send(payload) {
|
|
128
|
+
const url = new URL(this.endpoint);
|
|
129
|
+
const isHttps = url.protocol === 'https:';
|
|
130
|
+
const client = isHttps ? https : http;
|
|
131
|
+
|
|
132
|
+
const body = JSON.stringify(payload);
|
|
133
|
+
const options = {
|
|
134
|
+
hostname: url.hostname,
|
|
135
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
136
|
+
path: url.pathname + url.search,
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: {
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
'Content-Length': Buffer.byteLength(body),
|
|
141
|
+
'Authorization': `Bearer ${this.projectKey}`
|
|
142
|
+
},
|
|
143
|
+
timeout: this.timeout
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const req = client.request(options, (res) => {
|
|
147
|
+
// Consume response to free up memory
|
|
148
|
+
res.on('data', () => {});
|
|
149
|
+
res.on('end', () => {
|
|
150
|
+
if (res.statusCode >= 400) {
|
|
151
|
+
console.warn(`[SnipLog] Error reporting failed with status ${res.statusCode}`);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
req.on('error', (err) => {
|
|
157
|
+
console.warn('[SnipLog] Failed to send error:', err.message);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
req.on('timeout', () => {
|
|
161
|
+
req.destroy();
|
|
162
|
+
console.warn('[SnipLog] Request timeout');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
req.write(body);
|
|
166
|
+
req.end();
|
|
167
|
+
|
|
168
|
+
// Send to Discord webhook if configured
|
|
169
|
+
if (this.webhookUrl) {
|
|
170
|
+
this._sendToDiscord(payload);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Send error notification to Discord webhook
|
|
176
|
+
* @param {Object} payload - The error payload
|
|
177
|
+
*/
|
|
178
|
+
_sendToDiscord(payload) {
|
|
179
|
+
try {
|
|
180
|
+
const url = new URL(this.webhookUrl);
|
|
181
|
+
const isHttps = url.protocol === 'https:';
|
|
182
|
+
const client = isHttps ? https : http;
|
|
183
|
+
|
|
184
|
+
// Format Discord message
|
|
185
|
+
const errorMessage = this._formatDiscordMessage(payload);
|
|
186
|
+
const discordPayload = JSON.stringify({ content: errorMessage });
|
|
187
|
+
|
|
188
|
+
const options = {
|
|
189
|
+
hostname: url.hostname,
|
|
190
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
191
|
+
path: url.pathname + url.search,
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: {
|
|
194
|
+
'Content-Type': 'application/json',
|
|
195
|
+
'Content-Length': Buffer.byteLength(discordPayload)
|
|
196
|
+
},
|
|
197
|
+
timeout: this.timeout
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const req = client.request(options, (res) => {
|
|
201
|
+
res.on('data', () => {});
|
|
202
|
+
res.on('end', () => {
|
|
203
|
+
if (res.statusCode >= 400) {
|
|
204
|
+
console.warn(`[SnipLog] Discord webhook failed with status ${res.statusCode}`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
req.on('error', (err) => {
|
|
210
|
+
console.warn('[SnipLog] Failed to send to Discord:', err.message);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
req.on('timeout', () => {
|
|
214
|
+
req.destroy();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
req.write(discordPayload);
|
|
218
|
+
req.end();
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.warn('[SnipLog] Discord webhook error:', err.message);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Format error payload for Discord message
|
|
226
|
+
* @param {Object} payload - The error payload
|
|
227
|
+
* @returns {string} Formatted Discord message
|
|
228
|
+
*/
|
|
229
|
+
_formatDiscordMessage(payload) {
|
|
230
|
+
const emoji = payload.metadata?.type === 'expressError' ? '🚨' : '⚠️';
|
|
231
|
+
const errorType = payload.metadata?.type || 'Error';
|
|
232
|
+
const timestamp = new Date(payload.ts).toLocaleString();
|
|
233
|
+
|
|
234
|
+
let message = `${emoji} **${errorType}**: ${payload.message}\n`;
|
|
235
|
+
message += `📅 **Time**: ${timestamp}\n`;
|
|
236
|
+
message += `🔗 **URL**: ${payload.url || 'N/A'}\n`;
|
|
237
|
+
|
|
238
|
+
if (payload.metadata?.method) {
|
|
239
|
+
message += `📍 **Method**: ${payload.metadata.method}\n`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (payload.metadata?.hostname) {
|
|
243
|
+
message += `💻 **Host**: ${payload.metadata.hostname}\n`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (payload.metadata?.userId) {
|
|
247
|
+
message += `👤 **User**: ${payload.metadata.userId}\n`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Add stack trace (truncated for Discord's limit)
|
|
251
|
+
if (payload.stack) {
|
|
252
|
+
const stackLines = payload.stack.split('\n').slice(0, 5).join('\n');
|
|
253
|
+
message += `\n\`\`\`\n${stackLines}\n\`\`\``;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Discord has a 2000 character limit
|
|
257
|
+
if (message.length > 1900) {
|
|
258
|
+
message = message.substring(0, 1900) + '\n... (truncated)';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return message;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Express error middleware
|
|
266
|
+
* Place this AFTER all routes and other middleware
|
|
267
|
+
*
|
|
268
|
+
* Example:
|
|
269
|
+
* app.use(sniplog.errorMiddleware());
|
|
270
|
+
*/
|
|
271
|
+
errorMiddleware() {
|
|
272
|
+
const self = this;
|
|
273
|
+
return function sniplogErrorHandler(err, req, res, next) {
|
|
274
|
+
// Capture the error
|
|
275
|
+
self.captureError(err, {
|
|
276
|
+
type: 'expressError',
|
|
277
|
+
method: req.method,
|
|
278
|
+
url: req.originalUrl || req.url,
|
|
279
|
+
ip: req.ip || req.connection.remoteAddress,
|
|
280
|
+
userAgent: req.get('user-agent'),
|
|
281
|
+
userId: req.user ? req.user.id : undefined,
|
|
282
|
+
body: req.body,
|
|
283
|
+
query: req.query,
|
|
284
|
+
params: req.params
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Pass error to next error handler
|
|
288
|
+
next(err);
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Express request middleware (captures request context for later errors)
|
|
294
|
+
* Place this BEFORE routes
|
|
295
|
+
*
|
|
296
|
+
* Example:
|
|
297
|
+
* app.use(sniplog.requestMiddleware());
|
|
298
|
+
*/
|
|
299
|
+
requestMiddleware() {
|
|
300
|
+
const self = this;
|
|
301
|
+
return function sniplogRequestHandler(req, res, next) {
|
|
302
|
+
// Attach a helper to capture errors within this request
|
|
303
|
+
req.sniplog = {
|
|
304
|
+
captureError: (err, metadata = {}) => {
|
|
305
|
+
self.captureError(err, {
|
|
306
|
+
...metadata,
|
|
307
|
+
method: req.method,
|
|
308
|
+
url: req.originalUrl || req.url,
|
|
309
|
+
ip: req.ip || req.connection.remoteAddress,
|
|
310
|
+
userAgent: req.get('user-agent')
|
|
311
|
+
});
|
|
312
|
+
},
|
|
313
|
+
captureMessage: (message, metadata = {}) => {
|
|
314
|
+
self.captureMessage(message, {
|
|
315
|
+
...metadata,
|
|
316
|
+
method: req.method,
|
|
317
|
+
url: req.originalUrl || req.url
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
next();
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = SnipLog;
|