@senzops/apm-node 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 +56 -0
- package/package.json +28 -0
- package/src/core/client.ts +64 -0
- package/src/core/normalizer.ts +44 -0
- package/src/core/transport.ts +58 -0
- package/src/index.ts +28 -0
- package/src/middleware/express.ts +43 -0
- package/src/wrappers/fastify.ts +39 -0
- package/src/wrappers/h3.ts +49 -0
- package/src/wrappers/next.ts +74 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +11 -0
- package/wiki.md +97 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# **@senzops/apm-node**
|
|
2
|
+
|
|
3
|
+
The official Node.js middleware for **Senzor APM**.
|
|
4
|
+
|
|
5
|
+
Zero-overhead, asynchronous, and robust monitoring for your Express/Next.js APIs.
|
|
6
|
+
|
|
7
|
+
## **📦 Installation**
|
|
8
|
+
```sh
|
|
9
|
+
npm install @senzops/apm-node
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## **🚀 Usage**
|
|
13
|
+
|
|
14
|
+
### **Express.js**
|
|
15
|
+
|
|
16
|
+
Add Senzor as the **first** middleware in your app to ensure accurate timing.
|
|
17
|
+
```js
|
|
18
|
+
const express = require('express');
|
|
19
|
+
const senzor = require('@senzops/apm-node');
|
|
20
|
+
|
|
21
|
+
const app = express();
|
|
22
|
+
|
|
23
|
+
// 1\. Initialize
|
|
24
|
+
senzor.init({
|
|
25
|
+
apiKey: "sz_apm_...", // Get this from your Senzor Dashboard
|
|
26
|
+
// Optional config
|
|
27
|
+
// debug: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// 2\. Attach Request Handler
|
|
31
|
+
app.use(senzor.requestHandler());
|
|
32
|
+
|
|
33
|
+
// ... your routes ...
|
|
34
|
+
app.get('/users/:id', (req, res) => {
|
|
35
|
+
res.json({ id: req.params.id });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
app.listen(3000);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## **⚙️ Configuration**
|
|
42
|
+
|
|
43
|
+
| Option | Type | Description |
|
|
44
|
+
| :------------ | :------ | :------------------------------------------------------------ |
|
|
45
|
+
| apiKey | string | **Required.** Your Service API Key. |
|
|
46
|
+
| batchSize | number | Max requests to buffer before sending (Default: 100). |
|
|
47
|
+
| flushInterval | number | Max time (ms) to wait before sending buffer (Default: 10000). |
|
|
48
|
+
| debug | boolean | Enable console logs for debugging connection issues. |
|
|
49
|
+
|
|
50
|
+
## **🛡 Performance**
|
|
51
|
+
|
|
52
|
+
Senzor APM is designed to be **Production Safe**:
|
|
53
|
+
|
|
54
|
+
1. **Non-Blocking:** Data transmission happens asynchronously outside the request-response cycle.
|
|
55
|
+
2. **Fail-Open:** If Senzor ingestion is down, your API will continue to function normally without error.
|
|
56
|
+
3. **Lightweight:** Uses native Node.js timers and buffers. No heavy dependencies.
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@senzops/apm-node",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Universal APM SDK for Senzor",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.0.0",
|
|
22
|
+
"tsup": "^8.0.0",
|
|
23
|
+
"typescript": "^5.0.0"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Transport } from './transport';
|
|
2
|
+
|
|
3
|
+
export interface SenzorOptions {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
batchSize?: number;
|
|
7
|
+
flushInterval?: number;
|
|
8
|
+
debug?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class SenzorClient {
|
|
12
|
+
private transport: Transport | null = null;
|
|
13
|
+
private options: SenzorOptions | null = null;
|
|
14
|
+
|
|
15
|
+
public init(options: SenzorOptions) {
|
|
16
|
+
if (!options.apiKey) {
|
|
17
|
+
console.warn('[Senzor] API Key missing. SDK disabled.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.options = {
|
|
22
|
+
endpoint: 'https://api.senzor.dev/api/ingest/apm',
|
|
23
|
+
batchSize: 100,
|
|
24
|
+
flushInterval: 10000,
|
|
25
|
+
debug: false,
|
|
26
|
+
...options
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
this.transport = new Transport({
|
|
30
|
+
apiKey: this.options.apiKey,
|
|
31
|
+
endpoint: this.options.endpoint!,
|
|
32
|
+
batchSize: this.options.batchSize!,
|
|
33
|
+
flushInterval: this.options.flushInterval!,
|
|
34
|
+
debug: this.options.debug || false
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (this.options.debug) console.log('[Senzor] Initialized');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- Manual Tracking (For any framework) ---
|
|
41
|
+
public track(data: {
|
|
42
|
+
method: string;
|
|
43
|
+
route: string;
|
|
44
|
+
path: string;
|
|
45
|
+
status: number;
|
|
46
|
+
duration: number;
|
|
47
|
+
ip?: string;
|
|
48
|
+
userAgent?: string;
|
|
49
|
+
}) {
|
|
50
|
+
if (!this.transport) return;
|
|
51
|
+
|
|
52
|
+
this.transport.add({
|
|
53
|
+
...data,
|
|
54
|
+
timestamp: new Date().toISOString()
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Force Flush (For Serverless/Lambda) ---
|
|
59
|
+
public async flush() {
|
|
60
|
+
if (this.transport) await this.transport.flush();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const client = new SenzorClient();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic URL Normalizer
|
|
3
|
+
* Converts raw paths with IDs into generic patterns to prevent high cardinality.
|
|
4
|
+
* Example: /users/123/orders/abc-def -> /users/:id/orders/:uuid
|
|
5
|
+
*/
|
|
6
|
+
export const normalizePath = (path: string): string => {
|
|
7
|
+
if (!path || path === '/') return '/';
|
|
8
|
+
|
|
9
|
+
return path
|
|
10
|
+
// Replace UUIDs (long alphanumeric strings)
|
|
11
|
+
.replace(
|
|
12
|
+
/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g,
|
|
13
|
+
':uuid'
|
|
14
|
+
)
|
|
15
|
+
// Replace MongoDB ObjectIds (24 hex chars)
|
|
16
|
+
.replace(/[0-9a-fA-F]{24}/g, ':objectId')
|
|
17
|
+
// Replace pure numeric IDs (e.g., /123)
|
|
18
|
+
.replace(/\/(\d+)(?=\/|$)/g, '/:id')
|
|
19
|
+
// Remove query strings
|
|
20
|
+
.split('?')[0];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Tries to extract route from Framework internals, falls back to heuristic
|
|
25
|
+
*/
|
|
26
|
+
export const getRoute = (req: any, fallbackPath: string): string => {
|
|
27
|
+
// Express / Connect
|
|
28
|
+
if (req.route && req.route.path) {
|
|
29
|
+
return (req.baseUrl || '') + req.route.path;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// H3 / Nitro (Nuxt)
|
|
33
|
+
if (req.context && req.context.matchedRoute) {
|
|
34
|
+
return req.context.matchedRoute.path;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fastify
|
|
38
|
+
if (req.routerPath) {
|
|
39
|
+
return req.routerPath;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Fallback: Heuristic Normalization
|
|
43
|
+
return normalizePath(fallbackPath);
|
|
44
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface TransportConfig {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
endpoint: string;
|
|
4
|
+
batchSize: number;
|
|
5
|
+
flushInterval: number;
|
|
6
|
+
debug: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class Transport {
|
|
10
|
+
private queue: any[] = [];
|
|
11
|
+
private config: TransportConfig;
|
|
12
|
+
private timer: any = null;
|
|
13
|
+
|
|
14
|
+
constructor(config: TransportConfig) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
// Only start timer in non-serverless environments (long running processes)
|
|
17
|
+
if (typeof setInterval !== 'undefined') {
|
|
18
|
+
this.timer = setInterval(() => this.flush(), this.config.flushInterval);
|
|
19
|
+
// Unref if in Node.js to allow process exit
|
|
20
|
+
if (this.timer && typeof this.timer.unref === 'function') {
|
|
21
|
+
this.timer.unref();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public add(event: any) {
|
|
27
|
+
this.queue.push(event);
|
|
28
|
+
if (this.queue.length >= this.config.batchSize) {
|
|
29
|
+
this.flush();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public async flush() {
|
|
34
|
+
if (this.queue.length === 0) return;
|
|
35
|
+
|
|
36
|
+
const batch = [...this.queue];
|
|
37
|
+
this.queue = [];
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Use native fetch (Node 18+, Edge, Browser)
|
|
41
|
+
await fetch(this.config.endpoint, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'x-service-api-key': this.config.apiKey,
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify(batch),
|
|
48
|
+
// keepalive ensures connection stays open even if function ends (vital for APM)
|
|
49
|
+
keepalive: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (this.config.debug) console.log(`[Senzor] Flushed ${batch.length} traces`);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (this.config.debug) console.error('[Senzor] Ingestion Error:', err);
|
|
55
|
+
// We drop data on failure to prevent memory leaks in the app
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { client, SenzorOptions } from './core/client';
|
|
2
|
+
import { expressMiddleware } from './middleware/express';
|
|
3
|
+
import { wrapH3 } from './wrappers/h3';
|
|
4
|
+
import { wrapNextRoute, wrapNextPages } from './wrappers/next';
|
|
5
|
+
import { senzorPlugin } from './wrappers/fastify';
|
|
6
|
+
|
|
7
|
+
const Senzor = {
|
|
8
|
+
// Core
|
|
9
|
+
init: (options: SenzorOptions) => client.init(options),
|
|
10
|
+
flush: () => client.flush(),
|
|
11
|
+
track: client.track.bind(client),
|
|
12
|
+
|
|
13
|
+
// Express / Connect
|
|
14
|
+
requestHandler: expressMiddleware,
|
|
15
|
+
|
|
16
|
+
// Next.js
|
|
17
|
+
wrapNextRoute, // For App Router (Route Handlers)
|
|
18
|
+
wrapNextPages, // For Pages Router (API Routes)
|
|
19
|
+
|
|
20
|
+
// H3 / Nuxt / Nitro
|
|
21
|
+
wrapH3,
|
|
22
|
+
|
|
23
|
+
// Fastify
|
|
24
|
+
fastifyPlugin: senzorPlugin
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default Senzor;
|
|
28
|
+
export { Senzor };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { client } from '../core/client';
|
|
2
|
+
|
|
3
|
+
export const expressMiddleware = () => {
|
|
4
|
+
return (req: any, res: any, next: () => void) => {
|
|
5
|
+
// Start Timer (High Precision)
|
|
6
|
+
const start = performance.now();
|
|
7
|
+
|
|
8
|
+
// Hook into response finish
|
|
9
|
+
res.once('finish', () => {
|
|
10
|
+
try {
|
|
11
|
+
const duration = performance.now() - start;
|
|
12
|
+
|
|
13
|
+
// Route Normalization Logic
|
|
14
|
+
// Express stores route info in req.route
|
|
15
|
+
let route = 'UNKNOWN';
|
|
16
|
+
|
|
17
|
+
if (req.route && req.route.path) {
|
|
18
|
+
// Combined baseUrl (if mounted on /api) + path (/:id)
|
|
19
|
+
route = (req.baseUrl || '') + req.route.path;
|
|
20
|
+
} else if (res.statusCode === 404) {
|
|
21
|
+
route = 'Not Found';
|
|
22
|
+
} else {
|
|
23
|
+
// Fallback for unmapped routes or static files
|
|
24
|
+
route = req.path || 'Wildcard';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
client.track({
|
|
28
|
+
method: req.method,
|
|
29
|
+
route: route,
|
|
30
|
+
path: req.originalUrl || req.url,
|
|
31
|
+
status: res.statusCode,
|
|
32
|
+
duration: duration,
|
|
33
|
+
ip: req.ip || req.socket?.remoteAddress,
|
|
34
|
+
userAgent: req.headers['user-agent'],
|
|
35
|
+
});
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// Fail open
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
next();
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { client } from '../core/client';
|
|
2
|
+
import { SenzorOptions } from '../core/client';
|
|
3
|
+
|
|
4
|
+
// We don't import Fastify types to keep zero-deps, but structure matches
|
|
5
|
+
export const senzorPlugin = (fastify: any, options: SenzorOptions, done: Function) => {
|
|
6
|
+
|
|
7
|
+
// Init if options provided inline, otherwise assume global init
|
|
8
|
+
if (options && options.apiKey) {
|
|
9
|
+
client.init(options);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Hook: On Request (Start Timer)
|
|
13
|
+
fastify.addHook('onRequest', (request: any, reply: any, next: Function) => {
|
|
14
|
+
request.senzorStart = performance.now();
|
|
15
|
+
next();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Hook: On Response (End Timer & Track)
|
|
19
|
+
fastify.addHook('onResponse', (request: any, reply: any, next: Function) => {
|
|
20
|
+
const duration = performance.now() - (request.senzorStart || performance.now());
|
|
21
|
+
|
|
22
|
+
// Fastify provides 'routerPath' (e.g. /user/:id)
|
|
23
|
+
const route = request.routeOptions?.url || request.routerPath;
|
|
24
|
+
|
|
25
|
+
client.track({
|
|
26
|
+
method: request.method,
|
|
27
|
+
route: route || 'UNKNOWN',
|
|
28
|
+
path: request.raw.url || request.url,
|
|
29
|
+
status: reply.statusCode,
|
|
30
|
+
duration: duration,
|
|
31
|
+
ip: request.ip,
|
|
32
|
+
userAgent: request.headers['user-agent']
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
next();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
done();
|
|
39
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { client } from '../core/client';
|
|
2
|
+
import { getRoute } from '../core/normalizer';
|
|
3
|
+
|
|
4
|
+
// Types for H3 (Mocked to avoid heavy peer dependencies)
|
|
5
|
+
type EventHandler = (event: any) => any;
|
|
6
|
+
|
|
7
|
+
export const wrapH3 = (handler: EventHandler) => {
|
|
8
|
+
return async (event: any) => {
|
|
9
|
+
const start = performance.now();
|
|
10
|
+
let status = 200;
|
|
11
|
+
let error: any = null;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const response = await handler(event);
|
|
15
|
+
// Try to determine status from response or event
|
|
16
|
+
if (event.node?.res?.statusCode) {
|
|
17
|
+
status = event.node.res.statusCode;
|
|
18
|
+
}
|
|
19
|
+
return response;
|
|
20
|
+
} catch (err: any) {
|
|
21
|
+
error = err;
|
|
22
|
+
status = err.statusCode || err.status || 500;
|
|
23
|
+
throw err;
|
|
24
|
+
} finally {
|
|
25
|
+
// Non-blocking collection
|
|
26
|
+
const duration = performance.now() - start;
|
|
27
|
+
const req = event.node.req;
|
|
28
|
+
|
|
29
|
+
const path = req.originalUrl || req.url || '/';
|
|
30
|
+
|
|
31
|
+
client.track({
|
|
32
|
+
method: req.method || 'GET',
|
|
33
|
+
route: getRoute(event, path), // H3 often attaches context to event
|
|
34
|
+
path: path,
|
|
35
|
+
status: status,
|
|
36
|
+
duration: duration,
|
|
37
|
+
ip: getIp(req),
|
|
38
|
+
userAgent: req.headers['user-agent'],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// If serverless, we might need to await flush, but for general H3 usage (Node preset)
|
|
42
|
+
// we assume the process stays alive or uses ctx.waitUntil
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const getIp = (req: any) => {
|
|
48
|
+
return req.headers['x-forwarded-for'] || req.socket?.remoteAddress;
|
|
49
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { client } from '../core/client';
|
|
2
|
+
import { normalizePath } from '../core/normalizer';
|
|
3
|
+
|
|
4
|
+
// --- App Router Wrapper (GET, POST, etc.) ---
|
|
5
|
+
export const wrapNextRoute = (handler: Function) => {
|
|
6
|
+
return async (req: Request | any, context?: any) => {
|
|
7
|
+
const start = performance.now();
|
|
8
|
+
let status = 200;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const response = await handler(req, context);
|
|
12
|
+
if (response && typeof response.status === 'number') {
|
|
13
|
+
status = response.status;
|
|
14
|
+
}
|
|
15
|
+
return response;
|
|
16
|
+
} catch (err: any) {
|
|
17
|
+
status = 500;
|
|
18
|
+
throw err;
|
|
19
|
+
} finally {
|
|
20
|
+
const duration = performance.now() - start;
|
|
21
|
+
|
|
22
|
+
// App Router Request is a standard Web Request object
|
|
23
|
+
const url = req.url ? new URL(req.url) : { pathname: '/' };
|
|
24
|
+
|
|
25
|
+
// In App Router, we often rely on file-system path, but context.params helps
|
|
26
|
+
// For now, robust heuristic normalization is best
|
|
27
|
+
const route = normalizePath(url.pathname);
|
|
28
|
+
|
|
29
|
+
client.track({
|
|
30
|
+
method: req.method || 'GET',
|
|
31
|
+
route: route,
|
|
32
|
+
path: url.pathname,
|
|
33
|
+
status: status,
|
|
34
|
+
duration: duration,
|
|
35
|
+
userAgent: req.headers.get ? req.headers.get('user-agent') : undefined,
|
|
36
|
+
// IP extraction from Web Request is tricky without headers
|
|
37
|
+
ip: req.headers.get ? req.headers.get('x-forwarded-for') : undefined,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Vercel/Serverless Flush Safety
|
|
41
|
+
// We purposefully do NOT await flush here to avoid latency.
|
|
42
|
+
// Ideally user configures transport to sync flush or uses waitUntil
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// --- Pages Router Wrapper (req, res) ---
|
|
48
|
+
export const wrapNextPages = (handler: Function) => {
|
|
49
|
+
return async (req: any, res: any) => {
|
|
50
|
+
const start = performance.now();
|
|
51
|
+
|
|
52
|
+
// Hook into response finish
|
|
53
|
+
const done = () => {
|
|
54
|
+
const duration = performance.now() - start;
|
|
55
|
+
const path = req.url || '/';
|
|
56
|
+
|
|
57
|
+
client.track({
|
|
58
|
+
method: req.method || 'GET',
|
|
59
|
+
route: normalizePath(path.split('?')[0]),
|
|
60
|
+
path: path,
|
|
61
|
+
status: res.statusCode || 200,
|
|
62
|
+
duration: duration,
|
|
63
|
+
ip: req.headers['x-forwarded-for'] || req.socket?.remoteAddress,
|
|
64
|
+
userAgent: req.headers['user-agent']
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
res.once('finish', done);
|
|
69
|
+
// Handle error case if next/error isn't used
|
|
70
|
+
res.once('close', done);
|
|
71
|
+
|
|
72
|
+
return handler(req, res);
|
|
73
|
+
};
|
|
74
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*"],
|
|
13
|
+
"exclude": ["node_modules", "dist"]
|
|
14
|
+
}
|
package/tsup.config.ts
ADDED
package/wiki.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
## How to Use this (Production Ready)
|
|
2
|
+
This design resolves the "Express-only" limitation.
|
|
3
|
+
|
|
4
|
+
#### **Scenario A: Express / NestJS (Standard Node)**
|
|
5
|
+
```js
|
|
6
|
+
import { Senzor } from '@senzops/apm-node';
|
|
7
|
+
|
|
8
|
+
Senzor.init({ apiKey: "sz_apm_..." });
|
|
9
|
+
app.use(Senzor.requestHandler());
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
#### **Scenario B: Next.js (Server Actions / API Routes)**
|
|
13
|
+
Next.js doesn't use middleware the same way. Users can wrap handlers or use a manual track.
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
// pages/api/user.ts
|
|
17
|
+
import { Senzor } from '@senzops/apm-node';
|
|
18
|
+
|
|
19
|
+
export default async function handler(req, res) {
|
|
20
|
+
const start = performance.now();
|
|
21
|
+
|
|
22
|
+
// ... logic ...
|
|
23
|
+
|
|
24
|
+
// Track manually at end
|
|
25
|
+
Senzor.track({
|
|
26
|
+
method: req.method,
|
|
27
|
+
route: '/api/user',
|
|
28
|
+
path: req.url,
|
|
29
|
+
status: res.statusCode,
|
|
30
|
+
duration: performance.now() - start
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Critical for Vercel/Serverless: Wait for flush
|
|
34
|
+
await Senzor.flush();
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### **Scenario C: Nitro / H3 (NuxtJS)**
|
|
39
|
+
```javascript
|
|
40
|
+
// server/middleware/senzor.ts
|
|
41
|
+
import { Senzor } from '@senzops/apm-node';
|
|
42
|
+
|
|
43
|
+
Senzor.init({ apiKey: "..." });
|
|
44
|
+
|
|
45
|
+
export default defineEventHandler(async (event) => {
|
|
46
|
+
const start = performance.now();
|
|
47
|
+
|
|
48
|
+
// Process request
|
|
49
|
+
await callHandler(event);
|
|
50
|
+
|
|
51
|
+
Senzor.track({
|
|
52
|
+
method: event.method,
|
|
53
|
+
route: event.path, // Nitro might need regex to normalize
|
|
54
|
+
path: event.path,
|
|
55
|
+
status: event.node.res.statusCode,
|
|
56
|
+
duration: performance.now() - start
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### **Build Instructions**
|
|
62
|
+
Run this inside the `apm-node` directory:
|
|
63
|
+
```bash
|
|
64
|
+
npm install
|
|
65
|
+
npm run build
|
|
66
|
+
This will produce a lightweight `dist/` folder ready for publishing to NPM.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### **Using Wrappers**
|
|
70
|
+
**1. Express (Standard)**
|
|
71
|
+
```js
|
|
72
|
+
app.use(Senzor.requestHandler());
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**2. Next.js App Router (`app/api/route.ts`)**
|
|
76
|
+
```javascript
|
|
77
|
+
import { Senzor } from '@senzops/apm-node';
|
|
78
|
+
|
|
79
|
+
export const GET = Senzor.wrapNextRoute(async (request) => {
|
|
80
|
+
return Response.json({ success: true });
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
**3. Nuxt / Nitro (`server/api/test.ts`)**
|
|
84
|
+
```javascript
|
|
85
|
+
import { Senzor } from '@senzops/apm-node';
|
|
86
|
+
|
|
87
|
+
export default Senzor.wrapH3(defineEventHandler((event) => {
|
|
88
|
+
return { hello: 'world' }
|
|
89
|
+
}));
|
|
90
|
+
```
|
|
91
|
+
**4. Fastify**
|
|
92
|
+
```javascript
|
|
93
|
+
import { Senzor } from '@senzops/apm-node';
|
|
94
|
+
|
|
95
|
+
fastify.register(Senzor.fastifyPlugin, { apiKey: '...' });
|
|
96
|
+
```
|
|
97
|
+
This approach provides **native robustness** for each framework. It captures errors (500s), 404s (Route not found), and correct timing without the user manually writing `track()` calls.
|