codepulse-sdk 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/dist/index.d.ts +7 -0
- package/dist/index.js +142 -0
- package/package.json +27 -0
- package/src/index.ts +163 -0
- package/test-math.js +2 -0
- package/test.js +37 -0
- package/tsconfig.json +14 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.init = init;
|
|
7
|
+
const module_1 = __importDefault(require("module"));
|
|
8
|
+
const perf_hooks_1 = require("perf_hooks");
|
|
9
|
+
const http_1 = __importDefault(require("http"));
|
|
10
|
+
const https_1 = __importDefault(require("https"));
|
|
11
|
+
let options = null;
|
|
12
|
+
let statsBuffer = new Map();
|
|
13
|
+
let intervalId = null;
|
|
14
|
+
const wrappedCache = new WeakMap();
|
|
15
|
+
function formatTimestamp() {
|
|
16
|
+
const date = new Date();
|
|
17
|
+
return date.toISOString().replace('T', ' ').substring(0, 19);
|
|
18
|
+
}
|
|
19
|
+
function recordStats(filePath, funcName, durationMs) {
|
|
20
|
+
if (!options)
|
|
21
|
+
return;
|
|
22
|
+
const relativePath = filePath.split(process.cwd())[1]?.replace(/^[\\\/]/, '') || filePath;
|
|
23
|
+
const key = `${relativePath}::${funcName}`;
|
|
24
|
+
const existing = statsBuffer.get(key) || { call_count: 0, total_duration_ms: 0, last_timestamp: '' };
|
|
25
|
+
existing.call_count++;
|
|
26
|
+
existing.total_duration_ms += durationMs;
|
|
27
|
+
existing.last_timestamp = formatTimestamp();
|
|
28
|
+
statsBuffer.set(key, existing);
|
|
29
|
+
}
|
|
30
|
+
function wrapFunction(fn, filePath, funcName) {
|
|
31
|
+
return new Proxy(fn, {
|
|
32
|
+
apply(target, thisArg, argumentsList) {
|
|
33
|
+
const start = perf_hooks_1.performance.now();
|
|
34
|
+
try {
|
|
35
|
+
const result = Reflect.apply(target, thisArg, argumentsList);
|
|
36
|
+
if (result instanceof Promise) {
|
|
37
|
+
return result.then(val => {
|
|
38
|
+
recordStats(filePath, funcName, perf_hooks_1.performance.now() - start);
|
|
39
|
+
return val;
|
|
40
|
+
}).catch(err => {
|
|
41
|
+
recordStats(filePath, funcName, perf_hooks_1.performance.now() - start);
|
|
42
|
+
throw err;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
recordStats(filePath, funcName, perf_hooks_1.performance.now() - start);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
recordStats(filePath, funcName, perf_hooks_1.performance.now() - start);
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function wrapExports(exportsObj, filePath, defaultName = 'default') {
|
|
56
|
+
if (typeof exportsObj === 'function') {
|
|
57
|
+
return wrapFunction(exportsObj, filePath, exportsObj.name || defaultName);
|
|
58
|
+
}
|
|
59
|
+
else if (typeof exportsObj === 'object' && exportsObj !== null) {
|
|
60
|
+
return new Proxy(exportsObj, {
|
|
61
|
+
get(target, prop, receiver) {
|
|
62
|
+
const val = Reflect.get(target, prop, receiver);
|
|
63
|
+
if (typeof val === 'function') {
|
|
64
|
+
return wrapFunction(val, filePath, String(prop));
|
|
65
|
+
}
|
|
66
|
+
return val;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return exportsObj;
|
|
71
|
+
}
|
|
72
|
+
function flushStats() {
|
|
73
|
+
if (!options || statsBuffer.size === 0)
|
|
74
|
+
return;
|
|
75
|
+
const payload = Array.from(statsBuffer.entries()).map(([key, stat]) => {
|
|
76
|
+
const [file_path, function_name] = key.split('::');
|
|
77
|
+
return {
|
|
78
|
+
project_id: options.projectId,
|
|
79
|
+
repo: options.githubRepo,
|
|
80
|
+
file_path,
|
|
81
|
+
function_name,
|
|
82
|
+
call_count: stat.call_count,
|
|
83
|
+
avg_duration_ms: stat.total_duration_ms / stat.call_count,
|
|
84
|
+
timestamp: stat.last_timestamp,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
statsBuffer.clear();
|
|
88
|
+
try {
|
|
89
|
+
const url = new URL(options.ingestUrl);
|
|
90
|
+
const client = url.protocol === 'https:' ? https_1.default : http_1.default;
|
|
91
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
92
|
+
if (options.apiKey)
|
|
93
|
+
headers['x-api-key'] = options.apiKey;
|
|
94
|
+
const req = client.request(url, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers
|
|
97
|
+
});
|
|
98
|
+
req.on('error', (err) => {
|
|
99
|
+
console.error('[CodePulse] Error flushing stats:', err.message);
|
|
100
|
+
});
|
|
101
|
+
req.write(JSON.stringify(payload));
|
|
102
|
+
req.end();
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
console.error('[CodePulse] Failed to parse ingestUrl:', err);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function init(opts) {
|
|
109
|
+
if (options) {
|
|
110
|
+
console.warn('[CodePulse] Already initialized');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
options = opts;
|
|
114
|
+
const originalLoad = module_1.default._load;
|
|
115
|
+
module_1.default._load = function (request, parent, isMain) {
|
|
116
|
+
const exports = originalLoad.apply(this, arguments);
|
|
117
|
+
try {
|
|
118
|
+
if (options && typeof request === 'string') {
|
|
119
|
+
const filename = module_1.default._resolveFilename(request, parent, isMain);
|
|
120
|
+
if (filename && !filename.includes('node_modules') && !filename.startsWith('node:') && require('path').isAbsolute(filename)) {
|
|
121
|
+
if ((typeof exports === 'function' || typeof exports === 'object') && exports !== null) {
|
|
122
|
+
if (!wrappedCache.has(exports)) {
|
|
123
|
+
const wrapped = wrapExports(exports, filename);
|
|
124
|
+
wrappedCache.set(exports, wrapped);
|
|
125
|
+
return wrapped;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
return wrappedCache.get(exports);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
// Ignore resolution errors during interception
|
|
136
|
+
}
|
|
137
|
+
return exports;
|
|
138
|
+
};
|
|
139
|
+
intervalId = setInterval(flushStats, 5000);
|
|
140
|
+
intervalId.unref(); // Don't block event loop exit
|
|
141
|
+
console.log('[CodePulse] SDK initialized for project:', opts.projectId);
|
|
142
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codepulse-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"description": "Zero-config runtime observability SDK for Node.js",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"observability",
|
|
9
|
+
"apm",
|
|
10
|
+
"telemetry",
|
|
11
|
+
"nodejs",
|
|
12
|
+
"monitoring"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/Ramya-Shah/codepulse"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.0.0",
|
|
21
|
+
"@types/node": "^20.0.0"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"dev": "tsc -w"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import Module from 'module';
|
|
2
|
+
import { performance } from 'perf_hooks';
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
|
|
6
|
+
export interface InitOptions {
|
|
7
|
+
ingestUrl: string;
|
|
8
|
+
projectId: string;
|
|
9
|
+
githubRepo: string;
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface CallStat {
|
|
14
|
+
call_count: number;
|
|
15
|
+
total_duration_ms: number;
|
|
16
|
+
last_timestamp: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let options: InitOptions | null = null;
|
|
20
|
+
let statsBuffer = new Map<string, CallStat>();
|
|
21
|
+
let intervalId: NodeJS.Timeout | null = null;
|
|
22
|
+
const wrappedCache = new WeakMap<any, any>();
|
|
23
|
+
|
|
24
|
+
function formatTimestamp(): string {
|
|
25
|
+
const date = new Date();
|
|
26
|
+
return date.toISOString().replace('T', ' ').substring(0, 19);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function recordStats(filePath: string, funcName: string, durationMs: number) {
|
|
30
|
+
if (!options) return;
|
|
31
|
+
|
|
32
|
+
const relativePath = filePath.split(process.cwd())[1]?.replace(/^[\\\/]/, '') || filePath;
|
|
33
|
+
const key = `${relativePath}::${funcName}`;
|
|
34
|
+
const existing = statsBuffer.get(key) || { call_count: 0, total_duration_ms: 0, last_timestamp: '' };
|
|
35
|
+
|
|
36
|
+
existing.call_count++;
|
|
37
|
+
existing.total_duration_ms += durationMs;
|
|
38
|
+
existing.last_timestamp = formatTimestamp();
|
|
39
|
+
|
|
40
|
+
statsBuffer.set(key, existing);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function wrapFunction(fn: Function, filePath: string, funcName: string) {
|
|
44
|
+
return new Proxy(fn, {
|
|
45
|
+
apply(target, thisArg, argumentsList) {
|
|
46
|
+
const start = performance.now();
|
|
47
|
+
try {
|
|
48
|
+
const result = Reflect.apply(target, thisArg, argumentsList);
|
|
49
|
+
if (result instanceof Promise) {
|
|
50
|
+
return result.then(val => {
|
|
51
|
+
recordStats(filePath, funcName, performance.now() - start);
|
|
52
|
+
return val;
|
|
53
|
+
}).catch(err => {
|
|
54
|
+
recordStats(filePath, funcName, performance.now() - start);
|
|
55
|
+
throw err;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
recordStats(filePath, funcName, performance.now() - start);
|
|
59
|
+
return result;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
recordStats(filePath, funcName, performance.now() - start);
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function wrapExports(exportsObj: any, filePath: string, defaultName: string = 'default'): any {
|
|
69
|
+
if (typeof exportsObj === 'function') {
|
|
70
|
+
return wrapFunction(exportsObj, filePath, exportsObj.name || defaultName);
|
|
71
|
+
} else if (typeof exportsObj === 'object' && exportsObj !== null) {
|
|
72
|
+
return new Proxy(exportsObj, {
|
|
73
|
+
get(target, prop, receiver) {
|
|
74
|
+
const val = Reflect.get(target, prop, receiver);
|
|
75
|
+
if (typeof val === 'function') {
|
|
76
|
+
return wrapFunction(val, filePath, String(prop));
|
|
77
|
+
}
|
|
78
|
+
return val;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return exportsObj;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function flushStats() {
|
|
86
|
+
if (!options || statsBuffer.size === 0) return;
|
|
87
|
+
|
|
88
|
+
const payload = Array.from(statsBuffer.entries()).map(([key, stat]) => {
|
|
89
|
+
const [file_path, function_name] = key.split('::');
|
|
90
|
+
return {
|
|
91
|
+
project_id: options!.projectId,
|
|
92
|
+
repo: options!.githubRepo,
|
|
93
|
+
file_path,
|
|
94
|
+
function_name,
|
|
95
|
+
call_count: stat.call_count,
|
|
96
|
+
avg_duration_ms: stat.total_duration_ms / stat.call_count,
|
|
97
|
+
timestamp: stat.last_timestamp,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
statsBuffer.clear();
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const url = new URL(options.ingestUrl);
|
|
105
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
106
|
+
|
|
107
|
+
const headers: any = { 'Content-Type': 'application/json' };
|
|
108
|
+
if (options.apiKey) headers['x-api-key'] = options.apiKey;
|
|
109
|
+
|
|
110
|
+
const req = client.request(url, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
req.on('error', (err) => {
|
|
116
|
+
console.error('[CodePulse] Error flushing stats:', err.message);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
req.write(JSON.stringify(payload));
|
|
120
|
+
req.end();
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.error('[CodePulse] Failed to parse ingestUrl:', err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function init(opts: InitOptions) {
|
|
127
|
+
if (options) {
|
|
128
|
+
console.warn('[CodePulse] Already initialized');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
options = opts;
|
|
132
|
+
|
|
133
|
+
const originalLoad = (Module as any)._load;
|
|
134
|
+
(Module as any)._load = function(request: string, parent: any, isMain: boolean) {
|
|
135
|
+
const exports = originalLoad.apply(this, arguments);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
if (options && typeof request === 'string') {
|
|
139
|
+
const filename = (Module as any)._resolveFilename(request, parent, isMain);
|
|
140
|
+
if (filename && !filename.includes('node_modules') && !filename.startsWith('node:') && require('path').isAbsolute(filename)) {
|
|
141
|
+
if ((typeof exports === 'function' || typeof exports === 'object') && exports !== null) {
|
|
142
|
+
if (!wrappedCache.has(exports)) {
|
|
143
|
+
const wrapped = wrapExports(exports, filename);
|
|
144
|
+
wrappedCache.set(exports, wrapped);
|
|
145
|
+
return wrapped;
|
|
146
|
+
} else {
|
|
147
|
+
return wrappedCache.get(exports);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
// Ignore resolution errors during interception
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return exports;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
intervalId = setInterval(flushStats, 5000);
|
|
160
|
+
intervalId.unref(); // Don't block event loop exit
|
|
161
|
+
|
|
162
|
+
console.log('[CodePulse] SDK initialized for project:', opts.projectId);
|
|
163
|
+
}
|
package/test-math.js
ADDED
package/test.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { init } = require('./dist/index.js');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
|
|
4
|
+
// Dummy ingest server to capture the SDK payload
|
|
5
|
+
const server = http.createServer((req, res) => {
|
|
6
|
+
if (req.method === 'POST' && req.url === '/ingest') {
|
|
7
|
+
let body = '';
|
|
8
|
+
req.on('data', chunk => body += chunk.toString());
|
|
9
|
+
req.on('end', () => {
|
|
10
|
+
console.log('\n--- Received Payload from SDK ---');
|
|
11
|
+
console.log(JSON.stringify(JSON.parse(body), null, 2));
|
|
12
|
+
console.log('---------------------------------');
|
|
13
|
+
res.end('ok');
|
|
14
|
+
process.exit(0);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
server.listen(3005, () => {
|
|
19
|
+
console.log('Dummy ingest server on :3005');
|
|
20
|
+
|
|
21
|
+
// Initialize SDK
|
|
22
|
+
init({
|
|
23
|
+
ingestUrl: 'http://localhost:3005/ingest',
|
|
24
|
+
projectId: 'test-project',
|
|
25
|
+
githubRepo: 'test-repo'
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Require local module *after* init to trigger the hook
|
|
29
|
+
const math = require('./test-math.js');
|
|
30
|
+
|
|
31
|
+
console.log('Calling math functions...');
|
|
32
|
+
math.add(5, 10);
|
|
33
|
+
math.sub(10, 5);
|
|
34
|
+
|
|
35
|
+
// Wait for SDK to flush (5s)
|
|
36
|
+
console.log('Waiting 6s for SDK to flush...');
|
|
37
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|