@vantatrace/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
Binary file
@@ -0,0 +1,17 @@
1
+ export declare function getSystemContext(): {
2
+ nodeVersion: string;
3
+ hostname: string;
4
+ pid: number;
5
+ platform: NodeJS.Platform;
6
+ arch: string;
7
+ memory: {
8
+ rss: number;
9
+ heapTotal: number;
10
+ heapUsed: number;
11
+ external: number;
12
+ freeMem: number;
13
+ totalMem: number;
14
+ };
15
+ loadavg: number[];
16
+ uptime: number;
17
+ };
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getSystemContext = getSystemContext;
37
+ const os = __importStar(require("os"));
38
+ function getSystemContext() {
39
+ const memoryUsage = process.memoryUsage();
40
+ return {
41
+ nodeVersion: process.version,
42
+ hostname: os.hostname(),
43
+ pid: process.pid,
44
+ platform: os.platform(),
45
+ arch: os.arch(),
46
+ memory: {
47
+ rss: memoryUsage.rss,
48
+ heapTotal: memoryUsage.heapTotal,
49
+ heapUsed: memoryUsage.heapUsed,
50
+ external: memoryUsage.external,
51
+ freeMem: os.freemem(),
52
+ totalMem: os.totalmem()
53
+ },
54
+ loadavg: os.loadavg(),
55
+ uptime: Math.round(process.uptime())
56
+ };
57
+ }
@@ -0,0 +1,34 @@
1
+ import { VantaTraceOptions, VantaTraceContext } from './types';
2
+ export declare class VantaTrace {
3
+ private apiKey;
4
+ private serviceName;
5
+ private environment;
6
+ private debug;
7
+ private apiUrl;
8
+ constructor(options: VantaTraceOptions);
9
+ /**
10
+ * Primary method to capture exceptions and send them to VantaTrace
11
+ */
12
+ captureException(error: any, context?: VantaTraceContext): void;
13
+ /**
14
+ * Helper method to capture critical errors
15
+ */
16
+ captureCritical(error: any, context?: Omit<VantaTraceContext, 'severity'>): void;
17
+ /**
18
+ * Helper method to capture warning errors
19
+ */
20
+ captureWarning(error: any, context?: Omit<VantaTraceContext, 'severity'>): void;
21
+ /**
22
+ * Helper method to capture info messages/errors
23
+ */
24
+ captureInfo(error: any, context?: Omit<VantaTraceContext, 'severity'>): void;
25
+ /**
26
+ * Express error handling middleware
27
+ */
28
+ expressMiddleware(): (err: any, req: any, res: any, next: any) => void;
29
+ /**
30
+ * Automatically catch all uncaught exceptions and unhandled rejections
31
+ */
32
+ initGlobalHandlers(): void;
33
+ }
34
+ export default VantaTrace;
package/dist/index.js ADDED
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VantaTrace = void 0;
4
+ const normalizer_1 = require("./normalizer");
5
+ const context_1 = require("./context");
6
+ const transport_1 = require("./transport");
7
+ class VantaTrace {
8
+ constructor(options) {
9
+ this.apiKey = options.apiKey || '';
10
+ this.serviceName = options.serviceName || 'unknown-service';
11
+ let envInput = options.environment || process.env.NODE_ENV || 'development';
12
+ if (this.apiKey.includes('live')) {
13
+ envInput = 'live';
14
+ }
15
+ else if (this.apiKey.includes('test')) {
16
+ envInput = 'test';
17
+ }
18
+ this.environment = (envInput === 'production' || envInput === 'prod' || envInput === 'live') ? 'live' : 'test';
19
+ this.debug = !!options.debug;
20
+ // Default to localhost:6000/api/events (Standard Ingestion Endpoint)
21
+ this.apiUrl = options.apiUrl || 'https://api.vantatrace.com/api/events';
22
+ if (!options.apiKey && this.debug) {
23
+ console.warn('[VantaTrace] WARNING: API key is missing. SDK will run in dry-run mode.');
24
+ }
25
+ if (this.debug) {
26
+ console.log(`[VantaTrace] Initialized SDK for service "${this.serviceName}" on environment "${this.environment}".`);
27
+ }
28
+ }
29
+ /**
30
+ * Primary method to capture exceptions and send them to VantaTrace
31
+ */
32
+ captureException(error, context) {
33
+ try {
34
+ if (!this.apiKey) {
35
+ if (this.debug) {
36
+ console.log('[VantaTrace] Dry-run: captured exception:', error, 'Context:', context);
37
+ }
38
+ return;
39
+ }
40
+ const normalized = (0, normalizer_1.normalizeError)(error);
41
+ const systemContext = (0, context_1.getSystemContext)();
42
+ const payload = {
43
+ apiKey: this.apiKey,
44
+ serviceName: this.serviceName,
45
+ environment: this.environment,
46
+ timestamp: new Date().toISOString(),
47
+ error: normalized,
48
+ context: context || {},
49
+ system: systemContext,
50
+ severity: context?.severity
51
+ };
52
+ if (this.debug) {
53
+ console.log(`[VantaTrace] Sending error event: ${normalized.name} - ${normalized.message} (Severity: ${payload.severity || 'default'})`);
54
+ }
55
+ (0, transport_1.sendPayload)(this.apiUrl, this.apiKey, payload, this.debug);
56
+ }
57
+ catch (e) {
58
+ if (this.debug) {
59
+ console.error(`[VantaTrace] Failed to capture exception internally: ${e.message}`);
60
+ }
61
+ }
62
+ }
63
+ /**
64
+ * Helper method to capture critical errors
65
+ */
66
+ captureCritical(error, context) {
67
+ this.captureException(error, { ...context, severity: 'critical' });
68
+ }
69
+ /**
70
+ * Helper method to capture warning errors
71
+ */
72
+ captureWarning(error, context) {
73
+ this.captureException(error, { ...context, severity: 'warning' });
74
+ }
75
+ /**
76
+ * Helper method to capture info messages/errors
77
+ */
78
+ captureInfo(error, context) {
79
+ this.captureException(error, { ...context, severity: 'info' });
80
+ }
81
+ /**
82
+ * Express error handling middleware
83
+ */
84
+ expressMiddleware() {
85
+ return (err, req, res, next) => {
86
+ let userId = undefined;
87
+ if (req.user && typeof req.user === 'object') {
88
+ userId = req.user.id || req.user._id || req.user.userId;
89
+ }
90
+ else if (req.userId) {
91
+ userId = req.userId;
92
+ }
93
+ // Sanitize request body if password exists
94
+ let sanitizedBody = undefined;
95
+ if (req.body && typeof req.body === 'object') {
96
+ sanitizedBody = { ...req.body };
97
+ if ('password' in sanitizedBody)
98
+ sanitizedBody.password = '[REDACTED]';
99
+ if ('token' in sanitizedBody)
100
+ sanitizedBody.token = '[REDACTED]';
101
+ }
102
+ // Extract client IP
103
+ const ip = req.ip || req.headers['x-forwarded-for'] || req.socket?.remoteAddress;
104
+ // Extract and sanitize headers
105
+ const sanitizedHeaders = {};
106
+ const sensitiveHeaderKeys = ['authorization', 'cookie', 'set-cookie', 'x-api-key', 'proxy-authorization'];
107
+ if (req.headers && typeof req.headers === 'object') {
108
+ for (const [key, value] of Object.entries(req.headers)) {
109
+ if (sensitiveHeaderKeys.includes(key.toLowerCase())) {
110
+ sanitizedHeaders[key] = '[REDACTED]';
111
+ }
112
+ else {
113
+ sanitizedHeaders[key] = value;
114
+ }
115
+ }
116
+ }
117
+ this.captureException(err, {
118
+ userId: userId ? String(userId) : undefined,
119
+ route: req.route?.path || req.path || req.url,
120
+ method: req.method,
121
+ ip: ip ? String(ip) : undefined,
122
+ headers: sanitizedHeaders,
123
+ metadata: {
124
+ query: req.query,
125
+ body: sanitizedBody
126
+ }
127
+ });
128
+ next(err);
129
+ };
130
+ }
131
+ /**
132
+ * Automatically catch all uncaught exceptions and unhandled rejections
133
+ */
134
+ initGlobalHandlers() {
135
+ process.on('uncaughtException', (err) => {
136
+ if (this.debug) {
137
+ console.log('[VantaTrace] Captured uncaught exception globally');
138
+ }
139
+ this.captureException(err);
140
+ // Let the exception bubble up to avoid inconsistent process state (standard practice)
141
+ // but give a short time window for the async request to finish sending.
142
+ setTimeout(() => {
143
+ process.exit(1);
144
+ }, 500);
145
+ });
146
+ process.on('unhandledRejection', (reason) => {
147
+ if (this.debug) {
148
+ console.log('[VantaTrace] Captured unhandled promise rejection globally');
149
+ }
150
+ const err = reason instanceof Error ? reason : new Error(String(reason));
151
+ this.captureException(err);
152
+ });
153
+ }
154
+ }
155
+ exports.VantaTrace = VantaTrace;
156
+ exports.default = VantaTrace;
@@ -0,0 +1,10 @@
1
+ export declare function getFingerprint(name: string, message: string, stack: string): string;
2
+ export declare function normalizeError(err: any): {
3
+ name: string;
4
+ message: string;
5
+ stack: string;
6
+ fingerprint: string;
7
+ code?: string;
8
+ statusCode?: number;
9
+ extra?: Record<string, any>;
10
+ };
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getFingerprint = getFingerprint;
37
+ exports.normalizeError = normalizeError;
38
+ const crypto = __importStar(require("crypto"));
39
+ function getFingerprint(name, message, stack) {
40
+ const hash = crypto.createHash('md5');
41
+ // Group by name, message, and the first 3 lines of the stack trace to ignore line number variations deep in libraries
42
+ const cleanStack = (stack || '')
43
+ .split('\n')
44
+ .slice(0, 4)
45
+ .map(line => line.trim())
46
+ .join('\n');
47
+ hash.update(`${name}:${message}:${cleanStack}`);
48
+ return hash.digest('hex');
49
+ }
50
+ function normalizeError(err) {
51
+ let name = 'Error';
52
+ let message = 'Unknown error';
53
+ let stack = '';
54
+ let code = undefined;
55
+ let statusCode = undefined;
56
+ let extra = undefined;
57
+ if (err instanceof Error) {
58
+ name = err.name || 'Error';
59
+ message = err.message || 'Unknown error';
60
+ stack = err.stack || '';
61
+ if ('code' in err)
62
+ code = String(err.code);
63
+ if ('status' in err)
64
+ statusCode = Number(err.status);
65
+ if ('statusCode' in err)
66
+ statusCode = Number(err.statusCode);
67
+ const extraKeys = Object.keys(err).filter(k => !['name', 'message', 'stack', 'code', 'status', 'statusCode'].includes(k));
68
+ if (extraKeys.length > 0) {
69
+ extra = {};
70
+ for (const key of extraKeys) {
71
+ extra[key] = err[key];
72
+ }
73
+ }
74
+ }
75
+ else if (typeof err === 'string') {
76
+ message = err;
77
+ stack = new Error(err).stack || '';
78
+ }
79
+ else if (err && typeof err === 'object') {
80
+ name = err.name || err.constructor?.name || 'Error';
81
+ message = err.message || JSON.stringify(err);
82
+ stack = err.stack || new Error(message).stack || '';
83
+ if ('code' in err)
84
+ code = String(err.code);
85
+ if ('status' in err)
86
+ statusCode = Number(err.status);
87
+ if ('statusCode' in err)
88
+ statusCode = Number(err.statusCode);
89
+ const extraKeys = Object.keys(err).filter(k => !['name', 'message', 'stack', 'code', 'status', 'statusCode'].includes(k));
90
+ if (extraKeys.length > 0) {
91
+ extra = {};
92
+ for (const key of extraKeys) {
93
+ extra[key] = err[key];
94
+ }
95
+ }
96
+ }
97
+ else {
98
+ message = String(err);
99
+ stack = new Error(message).stack || '';
100
+ }
101
+ const fingerprint = getFingerprint(name, message, stack);
102
+ return { name, message, stack, fingerprint, code, statusCode, extra };
103
+ }
@@ -0,0 +1,2 @@
1
+ import { ErrorPayload } from './types';
2
+ export declare function sendPayload(apiUrl: string, apiKey: string, payload: ErrorPayload, debug?: boolean, retriesRemaining?: number): void;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.sendPayload = sendPayload;
37
+ const http = __importStar(require("http"));
38
+ const https = __importStar(require("https"));
39
+ const url_1 = require("url");
40
+ function sendPayload(apiUrl, apiKey, payload, debug = false, retriesRemaining = 1) {
41
+ const logDebug = (msg) => {
42
+ if (debug) {
43
+ console.log(`[VantaTrace] ${msg}`);
44
+ }
45
+ };
46
+ // Run asynchronously to ensure we don't block the caller
47
+ process.nextTick(() => {
48
+ try {
49
+ const parsedUrl = new url_1.URL(apiUrl);
50
+ const postData = JSON.stringify(payload);
51
+ const options = {
52
+ hostname: parsedUrl.hostname,
53
+ port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
54
+ path: parsedUrl.pathname + parsedUrl.search,
55
+ method: 'POST',
56
+ headers: {
57
+ 'Content-Type': 'application/json',
58
+ 'Content-Length': Buffer.byteLength(postData),
59
+ 'x-api-key': apiKey
60
+ },
61
+ timeout: 300 // 300ms max timeout
62
+ };
63
+ const client = parsedUrl.protocol === 'https:' ? https : http;
64
+ const req = client.request(options, (res) => {
65
+ // Consume response data to prevent resource leaks
66
+ res.on('data', () => { });
67
+ res.on('end', () => {
68
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
69
+ logDebug('Event sent successfully');
70
+ }
71
+ else {
72
+ logDebug(`Failed to send event. Status: ${res.statusCode}`);
73
+ handleRetry();
74
+ }
75
+ });
76
+ });
77
+ const handleRetry = () => {
78
+ if (retriesRemaining > 0) {
79
+ logDebug(`Retry attempt ${2 - retriesRemaining}`);
80
+ sendPayload(apiUrl, apiKey, payload, debug, retriesRemaining - 1);
81
+ }
82
+ };
83
+ req.on('error', (err) => {
84
+ logDebug(`Request error: ${err.message}`);
85
+ handleRetry();
86
+ });
87
+ req.on('timeout', () => {
88
+ logDebug('Timeout reached, aborting request');
89
+ req.destroy();
90
+ });
91
+ req.write(postData);
92
+ req.end();
93
+ }
94
+ catch (err) {
95
+ logDebug(`Transport execution failed: ${err.message}`);
96
+ }
97
+ });
98
+ }
@@ -0,0 +1,50 @@
1
+ export interface VantaTraceOptions {
2
+ apiKey: string;
3
+ serviceName: string;
4
+ environment?: string;
5
+ debug?: boolean;
6
+ apiUrl?: string;
7
+ }
8
+ export interface VantaTraceContext {
9
+ userId?: string;
10
+ route?: string;
11
+ method?: string;
12
+ ip?: string;
13
+ headers?: Record<string, any>;
14
+ metadata?: Record<string, any>;
15
+ severity?: 'critical' | 'warning' | 'info';
16
+ }
17
+ export interface ErrorPayload {
18
+ apiKey: string;
19
+ serviceName: string;
20
+ environment: string;
21
+ timestamp: string;
22
+ error: {
23
+ message: string;
24
+ stack: string;
25
+ name: string;
26
+ fingerprint: string;
27
+ code?: string;
28
+ statusCode?: number;
29
+ extra?: Record<string, any>;
30
+ };
31
+ context: VantaTraceContext;
32
+ system: {
33
+ nodeVersion: string;
34
+ hostname: string;
35
+ pid: number;
36
+ platform: string;
37
+ arch: string;
38
+ memory: {
39
+ rss: number;
40
+ heapTotal: number;
41
+ heapUsed: number;
42
+ external?: number;
43
+ freeMem: number;
44
+ totalMem: number;
45
+ };
46
+ loadavg: number[];
47
+ uptime: number;
48
+ };
49
+ severity?: 'critical' | 'warning' | 'info';
50
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@vantatrace/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Drop-in observability SDK for Node.js microservices powering error tracking, structured logging, and real-time telemetry pipelines.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "prepublishOnly": "npm run build"
10
+ },
11
+ "keywords": [
12
+ "observability",
13
+ "error-tracking",
14
+ "logging",
15
+ "telemetry",
16
+ "nodejs",
17
+ "microservices",
18
+ "monitoring",
19
+ "crash-reporting",
20
+ "exception-handling",
21
+ "debugging",
22
+ "apm",
23
+ "distributed-systems",
24
+ "vantatrace"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/Iqrarijaz/vantatrace_sdk.git"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20.11.0",
34
+ "typescript": "^5.3.3"
35
+ }
36
+ }
package/src/context.ts ADDED
@@ -0,0 +1,22 @@
1
+ import * as os from 'os';
2
+
3
+ export function getSystemContext() {
4
+ const memoryUsage = process.memoryUsage();
5
+ return {
6
+ nodeVersion: process.version,
7
+ hostname: os.hostname(),
8
+ pid: process.pid,
9
+ platform: os.platform(),
10
+ arch: os.arch(),
11
+ memory: {
12
+ rss: memoryUsage.rss,
13
+ heapTotal: memoryUsage.heapTotal,
14
+ heapUsed: memoryUsage.heapUsed,
15
+ external: memoryUsage.external,
16
+ freeMem: os.freemem(),
17
+ totalMem: os.totalmem()
18
+ },
19
+ loadavg: os.loadavg(),
20
+ uptime: Math.round(process.uptime())
21
+ };
22
+ }
package/src/index.ts ADDED
@@ -0,0 +1,175 @@
1
+ import { VantaTraceOptions, VantaTraceContext, ErrorPayload } from './types';
2
+ import { normalizeError } from './normalizer';
3
+ import { getSystemContext } from './context';
4
+ import { sendPayload } from './transport';
5
+
6
+ export class VantaTrace {
7
+ private apiKey: string;
8
+ private serviceName: string;
9
+ private environment: string;
10
+ private debug: boolean;
11
+ private apiUrl: string;
12
+
13
+ constructor(options: VantaTraceOptions) {
14
+ this.apiKey = options.apiKey || '';
15
+ this.serviceName = options.serviceName || 'unknown-service';
16
+
17
+ let envInput = options.environment || process.env.NODE_ENV || 'development';
18
+ if (this.apiKey.includes('live')) {
19
+ envInput = 'live';
20
+ } else if (this.apiKey.includes('test')) {
21
+ envInput = 'test';
22
+ }
23
+
24
+ this.environment = (envInput === 'production' || envInput === 'prod' || envInput === 'live') ? 'live' : 'test';
25
+ this.debug = !!options.debug;
26
+
27
+ // Default to localhost:6000/api/events (Standard Ingestion Endpoint)
28
+ this.apiUrl = options.apiUrl || 'https://api.vantatrace.com/api/events';
29
+
30
+ if (!options.apiKey && this.debug) {
31
+ console.warn('[VantaTrace] WARNING: API key is missing. SDK will run in dry-run mode.');
32
+ }
33
+
34
+ if (this.debug) {
35
+ console.log(`[VantaTrace] Initialized SDK for service "${this.serviceName}" on environment "${this.environment}".`);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Primary method to capture exceptions and send them to VantaTrace
41
+ */
42
+ public captureException(error: any, context?: VantaTraceContext): void {
43
+ try {
44
+ if (!this.apiKey) {
45
+ if (this.debug) {
46
+ console.log('[VantaTrace] Dry-run: captured exception:', error, 'Context:', context);
47
+ }
48
+ return;
49
+ }
50
+
51
+ const normalized = normalizeError(error);
52
+ const systemContext = getSystemContext();
53
+
54
+ const payload: ErrorPayload = {
55
+ apiKey: this.apiKey,
56
+ serviceName: this.serviceName,
57
+ environment: this.environment,
58
+ timestamp: new Date().toISOString(),
59
+ error: normalized,
60
+ context: context || {},
61
+ system: systemContext,
62
+ severity: context?.severity
63
+ };
64
+
65
+ if (this.debug) {
66
+ console.log(`[VantaTrace] Sending error event: ${normalized.name} - ${normalized.message} (Severity: ${payload.severity || 'default'})`);
67
+ }
68
+
69
+ sendPayload(this.apiUrl, this.apiKey, payload, this.debug);
70
+ } catch (e: any) {
71
+ if (this.debug) {
72
+ console.error(`[VantaTrace] Failed to capture exception internally: ${e.message}`);
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Helper method to capture critical errors
79
+ */
80
+ public captureCritical(error: any, context?: Omit<VantaTraceContext, 'severity'>): void {
81
+ this.captureException(error, { ...context, severity: 'critical' });
82
+ }
83
+
84
+ /**
85
+ * Helper method to capture warning errors
86
+ */
87
+ public captureWarning(error: any, context?: Omit<VantaTraceContext, 'severity'>): void {
88
+ this.captureException(error, { ...context, severity: 'warning' });
89
+ }
90
+
91
+ /**
92
+ * Helper method to capture info messages/errors
93
+ */
94
+ public captureInfo(error: any, context?: Omit<VantaTraceContext, 'severity'>): void {
95
+ this.captureException(error, { ...context, severity: 'info' });
96
+ }
97
+
98
+ /**
99
+ * Express error handling middleware
100
+ */
101
+ public expressMiddleware() {
102
+ return (err: any, req: any, res: any, next: any) => {
103
+ let userId = undefined;
104
+ if (req.user && typeof req.user === 'object') {
105
+ userId = req.user.id || req.user._id || req.user.userId;
106
+ } else if (req.userId) {
107
+ userId = req.userId;
108
+ }
109
+
110
+ // Sanitize request body if password exists
111
+ let sanitizedBody = undefined;
112
+ if (req.body && typeof req.body === 'object') {
113
+ sanitizedBody = { ...req.body };
114
+ if ('password' in sanitizedBody) sanitizedBody.password = '[REDACTED]';
115
+ if ('token' in sanitizedBody) sanitizedBody.token = '[REDACTED]';
116
+ }
117
+
118
+ // Extract client IP
119
+ const ip = req.ip || (req.headers['x-forwarded-for'] as string) || req.socket?.remoteAddress;
120
+
121
+ // Extract and sanitize headers
122
+ const sanitizedHeaders: Record<string, any> = {};
123
+ const sensitiveHeaderKeys = ['authorization', 'cookie', 'set-cookie', 'x-api-key', 'proxy-authorization'];
124
+ if (req.headers && typeof req.headers === 'object') {
125
+ for (const [key, value] of Object.entries(req.headers)) {
126
+ if (sensitiveHeaderKeys.includes(key.toLowerCase())) {
127
+ sanitizedHeaders[key] = '[REDACTED]';
128
+ } else {
129
+ sanitizedHeaders[key] = value;
130
+ }
131
+ }
132
+ }
133
+
134
+ this.captureException(err, {
135
+ userId: userId ? String(userId) : undefined,
136
+ route: req.route?.path || req.path || req.url,
137
+ method: req.method,
138
+ ip: ip ? String(ip) : undefined,
139
+ headers: sanitizedHeaders,
140
+ metadata: {
141
+ query: req.query,
142
+ body: sanitizedBody
143
+ }
144
+ });
145
+ next(err);
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Automatically catch all uncaught exceptions and unhandled rejections
151
+ */
152
+ public initGlobalHandlers(): void {
153
+ process.on('uncaughtException', (err) => {
154
+ if (this.debug) {
155
+ console.log('[VantaTrace] Captured uncaught exception globally');
156
+ }
157
+ this.captureException(err);
158
+
159
+ // Let the exception bubble up to avoid inconsistent process state (standard practice)
160
+ // but give a short time window for the async request to finish sending.
161
+ setTimeout(() => {
162
+ process.exit(1);
163
+ }, 500);
164
+ });
165
+
166
+ process.on('unhandledRejection', (reason) => {
167
+ if (this.debug) {
168
+ console.log('[VantaTrace] Captured unhandled promise rejection globally');
169
+ }
170
+ const err = reason instanceof Error ? reason : new Error(String(reason));
171
+ this.captureException(err);
172
+ });
173
+ }
174
+ }
175
+ export default VantaTrace;
@@ -0,0 +1,74 @@
1
+ import * as crypto from 'crypto';
2
+
3
+ export function getFingerprint(name: string, message: string, stack: string): string {
4
+ const hash = crypto.createHash('md5');
5
+ // Group by name, message, and the first 3 lines of the stack trace to ignore line number variations deep in libraries
6
+ const cleanStack = (stack || '')
7
+ .split('\n')
8
+ .slice(0, 4)
9
+ .map(line => line.trim())
10
+ .join('\n');
11
+ hash.update(`${name}:${message}:${cleanStack}`);
12
+ return hash.digest('hex');
13
+ }
14
+
15
+ export function normalizeError(err: any): {
16
+ name: string;
17
+ message: string;
18
+ stack: string;
19
+ fingerprint: string;
20
+ code?: string;
21
+ statusCode?: number;
22
+ extra?: Record<string, any>;
23
+ } {
24
+ let name = 'Error';
25
+ let message = 'Unknown error';
26
+ let stack = '';
27
+ let code: string | undefined = undefined;
28
+ let statusCode: number | undefined = undefined;
29
+ let extra: Record<string, any> | undefined = undefined;
30
+
31
+ if (err instanceof Error) {
32
+ name = err.name || 'Error';
33
+ message = err.message || 'Unknown error';
34
+ stack = err.stack || '';
35
+
36
+ if ('code' in err) code = String((err as any).code);
37
+ if ('status' in err) statusCode = Number((err as any).status);
38
+ if ('statusCode' in err) statusCode = Number((err as any).statusCode);
39
+
40
+ const extraKeys = Object.keys(err).filter(k => !['name', 'message', 'stack', 'code', 'status', 'statusCode'].includes(k));
41
+ if (extraKeys.length > 0) {
42
+ extra = {};
43
+ for (const key of extraKeys) {
44
+ extra[key] = (err as any)[key];
45
+ }
46
+ }
47
+ } else if (typeof err === 'string') {
48
+ message = err;
49
+ stack = new Error(err).stack || '';
50
+ } else if (err && typeof err === 'object') {
51
+ name = err.name || err.constructor?.name || 'Error';
52
+ message = err.message || JSON.stringify(err);
53
+ stack = err.stack || new Error(message).stack || '';
54
+
55
+ if ('code' in err) code = String(err.code);
56
+ if ('status' in err) statusCode = Number(err.status);
57
+ if ('statusCode' in err) statusCode = Number(err.statusCode);
58
+
59
+ const extraKeys = Object.keys(err).filter(k => !['name', 'message', 'stack', 'code', 'status', 'statusCode'].includes(k));
60
+ if (extraKeys.length > 0) {
61
+ extra = {};
62
+ for (const key of extraKeys) {
63
+ extra[key] = err[key];
64
+ }
65
+ }
66
+ } else {
67
+ message = String(err);
68
+ stack = new Error(message).stack || '';
69
+ }
70
+
71
+ const fingerprint = getFingerprint(name, message, stack);
72
+
73
+ return { name, message, stack, fingerprint, code, statusCode, extra };
74
+ }
@@ -0,0 +1,76 @@
1
+ import * as http from 'http';
2
+ import * as https from 'https';
3
+ import { URL } from 'url';
4
+ import { ErrorPayload } from './types';
5
+
6
+ export function sendPayload(
7
+ apiUrl: string,
8
+ apiKey: string,
9
+ payload: ErrorPayload,
10
+ debug: boolean = false,
11
+ retriesRemaining: number = 1
12
+ ): void {
13
+ const logDebug = (msg: string) => {
14
+ if (debug) {
15
+ console.log(`[VantaTrace] ${msg}`);
16
+ }
17
+ };
18
+
19
+ // Run asynchronously to ensure we don't block the caller
20
+ process.nextTick(() => {
21
+ try {
22
+ const parsedUrl = new URL(apiUrl);
23
+ const postData = JSON.stringify(payload);
24
+
25
+ const options: http.RequestOptions | https.RequestOptions = {
26
+ hostname: parsedUrl.hostname,
27
+ port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
28
+ path: parsedUrl.pathname + parsedUrl.search,
29
+ method: 'POST',
30
+ headers: {
31
+ 'Content-Type': 'application/json',
32
+ 'Content-Length': Buffer.byteLength(postData),
33
+ 'x-api-key': apiKey
34
+ },
35
+ timeout: 300 // 300ms max timeout
36
+ };
37
+
38
+ const client = parsedUrl.protocol === 'https:' ? https : http;
39
+
40
+ const req = client.request(options, (res) => {
41
+ // Consume response data to prevent resource leaks
42
+ res.on('data', () => {});
43
+ res.on('end', () => {
44
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
45
+ logDebug('Event sent successfully');
46
+ } else {
47
+ logDebug(`Failed to send event. Status: ${res.statusCode}`);
48
+ handleRetry();
49
+ }
50
+ });
51
+ });
52
+
53
+ const handleRetry = () => {
54
+ if (retriesRemaining > 0) {
55
+ logDebug(`Retry attempt ${2 - retriesRemaining}`);
56
+ sendPayload(apiUrl, apiKey, payload, debug, retriesRemaining - 1);
57
+ }
58
+ };
59
+
60
+ req.on('error', (err) => {
61
+ logDebug(`Request error: ${err.message}`);
62
+ handleRetry();
63
+ });
64
+
65
+ req.on('timeout', () => {
66
+ logDebug('Timeout reached, aborting request');
67
+ req.destroy();
68
+ });
69
+
70
+ req.write(postData);
71
+ req.end();
72
+ } catch (err: any) {
73
+ logDebug(`Transport execution failed: ${err.message}`);
74
+ }
75
+ });
76
+ }
package/src/types.ts ADDED
@@ -0,0 +1,52 @@
1
+ export interface VantaTraceOptions {
2
+ apiKey: string;
3
+ serviceName: string;
4
+ environment?: string;
5
+ debug?: boolean;
6
+ apiUrl?: string;
7
+ }
8
+
9
+ export interface VantaTraceContext {
10
+ userId?: string;
11
+ route?: string;
12
+ method?: string;
13
+ ip?: string;
14
+ headers?: Record<string, any>;
15
+ metadata?: Record<string, any>;
16
+ severity?: 'critical' | 'warning' | 'info';
17
+ }
18
+
19
+ export interface ErrorPayload {
20
+ apiKey: string;
21
+ serviceName: string;
22
+ environment: string;
23
+ timestamp: string;
24
+ error: {
25
+ message: string;
26
+ stack: string;
27
+ name: string;
28
+ fingerprint: string;
29
+ code?: string;
30
+ statusCode?: number;
31
+ extra?: Record<string, any>;
32
+ };
33
+ context: VantaTraceContext;
34
+ system: {
35
+ nodeVersion: string;
36
+ hostname: string;
37
+ pid: number;
38
+ platform: string;
39
+ arch: string;
40
+ memory: {
41
+ rss: number;
42
+ heapTotal: number;
43
+ heapUsed: number;
44
+ external?: number;
45
+ freeMem: number;
46
+ totalMem: number;
47
+ };
48
+ loadavg: number[];
49
+ uptime: number;
50
+ };
51
+ severity?: 'critical' | 'warning' | 'info';
52
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["es2020"],
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src/**/*"]
15
+ }
Binary file