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 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;