@appbuildersph/watcher 0.0.1
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 +37 -0
- package/dist/index.cjs +177 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +86 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +171 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @appbuildersph/watcher
|
|
2
|
+
|
|
3
|
+
Application watcher for the App Builders PH SDK — continuous CPU/RAM/event-loop
|
|
4
|
+
sampling, crash capture, and health/performance scoring.
|
|
5
|
+
|
|
6
|
+
## What it monitors
|
|
7
|
+
|
|
8
|
+
- CPU (% of one core) and event-loop delay, sampled on an interval
|
|
9
|
+
- RSS/heap memory vs. system free/total memory
|
|
10
|
+
- Uptime
|
|
11
|
+
- A lightweight heap-growth heuristic that flags a likely memory leak
|
|
12
|
+
- `uncaughtException` / `unhandledRejection` — auto-captured as incidents
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { Watcher } from "@appbuildersph/watcher";
|
|
18
|
+
|
|
19
|
+
const watcher = new Watcher({
|
|
20
|
+
intervalMs: 5000,
|
|
21
|
+
onIncident: (incident) => logger.critical(incident.message, { incident }),
|
|
22
|
+
onMetrics: (snapshot) => logger.debug("metrics", { snapshot }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
watcher.start();
|
|
26
|
+
|
|
27
|
+
watcher.health();
|
|
28
|
+
// => { healthScore, performanceScore, uptimeSeconds, lastSync, latestMetrics, recentIncidents }
|
|
29
|
+
|
|
30
|
+
watcher.createIncident("manual", "payment webhook retried 5 times");
|
|
31
|
+
watcher.stop();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`healthScore` and `performanceScore` are 0-100: performance is penalized by CPU and
|
|
35
|
+
event-loop delay, and health is further penalized by incidents raised in the last 5
|
|
36
|
+
minutes. Incidents from `uncaughtException`/`unhandledRejection` are captured
|
|
37
|
+
automatically unless `captureCrashes: false` is set.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var os = require('os');
|
|
5
|
+
var perf_hooks = require('perf_hooks');
|
|
6
|
+
|
|
7
|
+
// src/watcher.ts
|
|
8
|
+
var MetricsSampler = class {
|
|
9
|
+
lastCpuUsage = process.cpuUsage();
|
|
10
|
+
lastSampleTime = process.hrtime.bigint();
|
|
11
|
+
eventLoopMonitor = perf_hooks.monitorEventLoopDelay({ resolution: 20 });
|
|
12
|
+
constructor() {
|
|
13
|
+
this.eventLoopMonitor.enable();
|
|
14
|
+
}
|
|
15
|
+
sample() {
|
|
16
|
+
const now = process.hrtime.bigint();
|
|
17
|
+
const elapsedMs = Number(now - this.lastSampleTime) / 1e6;
|
|
18
|
+
const cpuUsage = process.cpuUsage(this.lastCpuUsage);
|
|
19
|
+
const cpuMs = (cpuUsage.user + cpuUsage.system) / 1e3;
|
|
20
|
+
const cpuPercent = elapsedMs > 0 ? Math.min(100, cpuMs / elapsedMs * 100) : 0;
|
|
21
|
+
this.lastCpuUsage = process.cpuUsage();
|
|
22
|
+
this.lastSampleTime = now;
|
|
23
|
+
const mem = process.memoryUsage();
|
|
24
|
+
const eventLoopDelayMs = this.eventLoopMonitor.mean / 1e6 || 0;
|
|
25
|
+
this.eventLoopMonitor.reset();
|
|
26
|
+
return {
|
|
27
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28
|
+
cpuPercent: Number(cpuPercent.toFixed(2)),
|
|
29
|
+
memory: {
|
|
30
|
+
rssBytes: mem.rss,
|
|
31
|
+
heapUsedBytes: mem.heapUsed,
|
|
32
|
+
heapTotalBytes: mem.heapTotal,
|
|
33
|
+
systemFreeBytes: os.freemem(),
|
|
34
|
+
systemTotalBytes: os.totalmem()
|
|
35
|
+
},
|
|
36
|
+
eventLoopDelayMs: Number(eventLoopDelayMs.toFixed(2)),
|
|
37
|
+
uptimeSeconds: Math.floor(process.uptime())
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
dispose() {
|
|
41
|
+
this.eventLoopMonitor.disable();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/scoring.ts
|
|
46
|
+
function computePerformanceScore(snapshot) {
|
|
47
|
+
const cpuPenalty = snapshot.cpuPercent;
|
|
48
|
+
const eventLoopPenalty = Math.min(50, snapshot.eventLoopDelayMs);
|
|
49
|
+
const score = 100 - cpuPenalty * 0.5 - eventLoopPenalty;
|
|
50
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
51
|
+
}
|
|
52
|
+
function computeHealthScore(performanceScore, recentIncidentCount) {
|
|
53
|
+
const incidentPenalty = Math.min(60, recentIncidentCount * 15);
|
|
54
|
+
return Math.max(0, Math.min(100, performanceScore - incidentPenalty));
|
|
55
|
+
}
|
|
56
|
+
function detectMemoryLeakTrend(history) {
|
|
57
|
+
if (history.length < 6) return false;
|
|
58
|
+
const heapValues = history.map((s) => s.memory.heapUsedBytes);
|
|
59
|
+
const midpoint = Math.floor(heapValues.length / 2);
|
|
60
|
+
const firstHalfAvg = average(heapValues.slice(0, midpoint));
|
|
61
|
+
const secondHalfAvg = average(heapValues.slice(midpoint));
|
|
62
|
+
if (firstHalfAvg === 0) return false;
|
|
63
|
+
const growth = (secondHalfAvg - firstHalfAvg) / firstHalfAvg;
|
|
64
|
+
return growth > 0.2;
|
|
65
|
+
}
|
|
66
|
+
function average(values) {
|
|
67
|
+
if (values.length === 0) return 0;
|
|
68
|
+
return values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/watcher.ts
|
|
72
|
+
var INCIDENT_WINDOW_MS = 5 * 60 * 1e3;
|
|
73
|
+
var Watcher = class {
|
|
74
|
+
intervalMs;
|
|
75
|
+
historySize;
|
|
76
|
+
captureCrashes;
|
|
77
|
+
onIncidentCallback;
|
|
78
|
+
onMetricsCallback;
|
|
79
|
+
sampler = null;
|
|
80
|
+
timer = null;
|
|
81
|
+
history = [];
|
|
82
|
+
incidents = [];
|
|
83
|
+
leakIncidentRaised = false;
|
|
84
|
+
handleUncaughtException = (error) => {
|
|
85
|
+
this.createIncident("uncaughtException", error.message, { stack: error.stack });
|
|
86
|
+
};
|
|
87
|
+
handleUnhandledRejection = (reason) => {
|
|
88
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
89
|
+
const stack = reason instanceof Error ? reason.stack : void 0;
|
|
90
|
+
this.createIncident("unhandledRejection", message, { stack });
|
|
91
|
+
};
|
|
92
|
+
constructor(options = {}) {
|
|
93
|
+
this.intervalMs = options.intervalMs ?? 5e3;
|
|
94
|
+
this.historySize = options.historySize ?? 60;
|
|
95
|
+
this.captureCrashes = options.captureCrashes ?? true;
|
|
96
|
+
this.onIncidentCallback = options.onIncident;
|
|
97
|
+
this.onMetricsCallback = options.onMetrics;
|
|
98
|
+
}
|
|
99
|
+
start() {
|
|
100
|
+
if (this.timer) return;
|
|
101
|
+
this.sampler = new MetricsSampler();
|
|
102
|
+
this.timer = setInterval(() => this.tick(), this.intervalMs);
|
|
103
|
+
this.timer.unref?.();
|
|
104
|
+
if (this.captureCrashes) {
|
|
105
|
+
process.on("uncaughtException", this.handleUncaughtException);
|
|
106
|
+
process.on("unhandledRejection", this.handleUnhandledRejection);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
stop() {
|
|
110
|
+
if (this.timer) {
|
|
111
|
+
clearInterval(this.timer);
|
|
112
|
+
this.timer = null;
|
|
113
|
+
}
|
|
114
|
+
this.sampler?.dispose();
|
|
115
|
+
this.sampler = null;
|
|
116
|
+
if (this.captureCrashes) {
|
|
117
|
+
process.off("uncaughtException", this.handleUncaughtException);
|
|
118
|
+
process.off("unhandledRejection", this.handleUnhandledRejection);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Takes and records a single sample immediately (also used by start()'s interval). */
|
|
122
|
+
tick() {
|
|
123
|
+
const sampler = this.sampler ?? (this.sampler = new MetricsSampler());
|
|
124
|
+
const snapshot = sampler.sample();
|
|
125
|
+
this.history.push(snapshot);
|
|
126
|
+
if (this.history.length > this.historySize) this.history.shift();
|
|
127
|
+
if (detectMemoryLeakTrend(this.history) && !this.leakIncidentRaised) {
|
|
128
|
+
this.leakIncidentRaised = true;
|
|
129
|
+
this.createIncident("memoryLeak", "Sustained heap growth detected across recent samples");
|
|
130
|
+
}
|
|
131
|
+
this.onMetricsCallback?.(snapshot);
|
|
132
|
+
return snapshot;
|
|
133
|
+
}
|
|
134
|
+
createIncident(source, message, context) {
|
|
135
|
+
const incident = {
|
|
136
|
+
id: crypto.randomUUID(),
|
|
137
|
+
source,
|
|
138
|
+
message,
|
|
139
|
+
stack: context?.stack,
|
|
140
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
141
|
+
context
|
|
142
|
+
};
|
|
143
|
+
this.incidents.push(incident);
|
|
144
|
+
this.onIncidentCallback?.(incident);
|
|
145
|
+
return incident;
|
|
146
|
+
}
|
|
147
|
+
health() {
|
|
148
|
+
const latestMetrics = this.history.at(-1) ?? null;
|
|
149
|
+
const recentIncidents = this.incidents.filter(
|
|
150
|
+
(incident) => Date.now() - new Date(incident.timestamp).getTime() < INCIDENT_WINDOW_MS
|
|
151
|
+
);
|
|
152
|
+
const performanceScore = latestMetrics ? computePerformanceScore(latestMetrics) : 100;
|
|
153
|
+
const healthScore = computeHealthScore(performanceScore, recentIncidents.length);
|
|
154
|
+
return {
|
|
155
|
+
healthScore,
|
|
156
|
+
performanceScore,
|
|
157
|
+
uptimeSeconds: latestMetrics?.uptimeSeconds ?? Math.floor(process.uptime()),
|
|
158
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
159
|
+
latestMetrics,
|
|
160
|
+
recentIncidents
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
getHistory() {
|
|
164
|
+
return [...this.history];
|
|
165
|
+
}
|
|
166
|
+
getIncidents() {
|
|
167
|
+
return [...this.incidents];
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
exports.MetricsSampler = MetricsSampler;
|
|
172
|
+
exports.Watcher = Watcher;
|
|
173
|
+
exports.computeHealthScore = computeHealthScore;
|
|
174
|
+
exports.computePerformanceScore = computePerformanceScore;
|
|
175
|
+
exports.detectMemoryLeakTrend = detectMemoryLeakTrend;
|
|
176
|
+
//# sourceMappingURL=index.cjs.map
|
|
177
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/metrics.ts","../src/scoring.ts","../src/watcher.ts"],"names":["monitorEventLoopDelay","freemem","totalmem","randomUUID"],"mappings":"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","file":"index.cjs","sourcesContent":["import { freemem, totalmem } from \"node:os\";\nimport { monitorEventLoopDelay } from \"node:perf_hooks\";\nimport type { MetricsSnapshot } from \"./types.js\";\n\n/** Samples CPU (as % of one core) and event-loop delay across successive calls. */\nexport class MetricsSampler {\n private lastCpuUsage = process.cpuUsage();\n private lastSampleTime = process.hrtime.bigint();\n private readonly eventLoopMonitor = monitorEventLoopDelay({ resolution: 20 });\n\n constructor() {\n this.eventLoopMonitor.enable();\n }\n\n sample(): MetricsSnapshot {\n const now = process.hrtime.bigint();\n const elapsedMs = Number(now - this.lastSampleTime) / 1e6;\n const cpuUsage = process.cpuUsage(this.lastCpuUsage);\n const cpuMs = (cpuUsage.user + cpuUsage.system) / 1000;\n const cpuPercent = elapsedMs > 0 ? Math.min(100, (cpuMs / elapsedMs) * 100) : 0;\n\n this.lastCpuUsage = process.cpuUsage();\n this.lastSampleTime = now;\n\n const mem = process.memoryUsage();\n const eventLoopDelayMs = this.eventLoopMonitor.mean / 1e6 || 0;\n this.eventLoopMonitor.reset();\n\n return {\n timestamp: new Date().toISOString(),\n cpuPercent: Number(cpuPercent.toFixed(2)),\n memory: {\n rssBytes: mem.rss,\n heapUsedBytes: mem.heapUsed,\n heapTotalBytes: mem.heapTotal,\n systemFreeBytes: freemem(),\n systemTotalBytes: totalmem(),\n },\n eventLoopDelayMs: Number(eventLoopDelayMs.toFixed(2)),\n uptimeSeconds: Math.floor(process.uptime()),\n };\n }\n\n dispose(): void {\n this.eventLoopMonitor.disable();\n }\n}\n","import type { MetricsSnapshot } from \"./types.js\";\n\n/** 0-100 score: penalizes high CPU and event-loop delay. */\nexport function computePerformanceScore(snapshot: MetricsSnapshot): number {\n const cpuPenalty = snapshot.cpuPercent;\n const eventLoopPenalty = Math.min(50, snapshot.eventLoopDelayMs);\n const score = 100 - cpuPenalty * 0.5 - eventLoopPenalty;\n return Math.max(0, Math.min(100, Math.round(score)));\n}\n\n/** 0-100 score: performance plus a penalty for recent incident volume. */\nexport function computeHealthScore(\n performanceScore: number,\n recentIncidentCount: number,\n): number {\n const incidentPenalty = Math.min(60, recentIncidentCount * 15);\n return Math.max(0, Math.min(100, performanceScore - incidentPenalty));\n}\n\n/**\n * Detects a sustained upward trend in heap usage across the sample history —\n * a lightweight proxy for a memory leak (not a definitive diagnosis).\n */\nexport function detectMemoryLeakTrend(history: MetricsSnapshot[]): boolean {\n if (history.length < 6) return false;\n\n const heapValues = history.map((s) => s.memory.heapUsedBytes);\n const midpoint = Math.floor(heapValues.length / 2);\n const firstHalfAvg = average(heapValues.slice(0, midpoint));\n const secondHalfAvg = average(heapValues.slice(midpoint));\n\n if (firstHalfAvg === 0) return false;\n const growth = (secondHalfAvg - firstHalfAvg) / firstHalfAvg;\n return growth > 0.2;\n}\n\nfunction average(values: number[]): number {\n if (values.length === 0) return 0;\n return values.reduce((sum, v) => sum + v, 0) / values.length;\n}\n","import { randomUUID } from \"node:crypto\";\nimport { MetricsSampler } from \"./metrics.js\";\nimport { computeHealthScore, computePerformanceScore, detectMemoryLeakTrend } from \"./scoring.js\";\nimport type {\n HealthReportDetail,\n Incident,\n IncidentSource,\n MetricsSnapshot,\n WatcherOptions,\n} from \"./types.js\";\n\nconst INCIDENT_WINDOW_MS = 5 * 60 * 1000;\n\nexport class Watcher {\n private readonly intervalMs: number;\n private readonly historySize: number;\n private readonly captureCrashes: boolean;\n private readonly onIncidentCallback?: (incident: Incident) => void;\n private readonly onMetricsCallback?: (snapshot: MetricsSnapshot) => void;\n\n private sampler: MetricsSampler | null = null;\n private timer: ReturnType<typeof setInterval> | null = null;\n private history: MetricsSnapshot[] = [];\n private incidents: Incident[] = [];\n private leakIncidentRaised = false;\n\n private readonly handleUncaughtException = (error: Error) => {\n this.createIncident(\"uncaughtException\", error.message, { stack: error.stack });\n };\n\n private readonly handleUnhandledRejection = (reason: unknown) => {\n const message = reason instanceof Error ? reason.message : String(reason);\n const stack = reason instanceof Error ? reason.stack : undefined;\n this.createIncident(\"unhandledRejection\", message, { stack });\n };\n\n constructor(options: WatcherOptions = {}) {\n this.intervalMs = options.intervalMs ?? 5000;\n this.historySize = options.historySize ?? 60;\n this.captureCrashes = options.captureCrashes ?? true;\n this.onIncidentCallback = options.onIncident;\n this.onMetricsCallback = options.onMetrics;\n }\n\n start(): void {\n if (this.timer) return;\n\n this.sampler = new MetricsSampler();\n this.timer = setInterval(() => this.tick(), this.intervalMs);\n this.timer.unref?.();\n\n if (this.captureCrashes) {\n process.on(\"uncaughtException\", this.handleUncaughtException);\n process.on(\"unhandledRejection\", this.handleUnhandledRejection);\n }\n }\n\n stop(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n this.sampler?.dispose();\n this.sampler = null;\n\n if (this.captureCrashes) {\n process.off(\"uncaughtException\", this.handleUncaughtException);\n process.off(\"unhandledRejection\", this.handleUnhandledRejection);\n }\n }\n\n /** Takes and records a single sample immediately (also used by start()'s interval). */\n tick(): MetricsSnapshot {\n const sampler = this.sampler ?? (this.sampler = new MetricsSampler());\n const snapshot = sampler.sample();\n\n this.history.push(snapshot);\n if (this.history.length > this.historySize) this.history.shift();\n\n if (detectMemoryLeakTrend(this.history) && !this.leakIncidentRaised) {\n this.leakIncidentRaised = true;\n this.createIncident(\"memoryLeak\", \"Sustained heap growth detected across recent samples\");\n }\n\n this.onMetricsCallback?.(snapshot);\n return snapshot;\n }\n\n createIncident(\n source: IncidentSource,\n message: string,\n context?: Record<string, unknown>,\n ): Incident {\n const incident: Incident = {\n id: randomUUID(),\n source,\n message,\n stack: context?.stack as string | undefined,\n timestamp: new Date().toISOString(),\n context,\n };\n\n this.incidents.push(incident);\n this.onIncidentCallback?.(incident);\n return incident;\n }\n\n health(): HealthReportDetail {\n const latestMetrics = this.history.at(-1) ?? null;\n const recentIncidents = this.incidents.filter(\n (incident) => Date.now() - new Date(incident.timestamp).getTime() < INCIDENT_WINDOW_MS,\n );\n\n const performanceScore = latestMetrics ? computePerformanceScore(latestMetrics) : 100;\n const healthScore = computeHealthScore(performanceScore, recentIncidents.length);\n\n return {\n healthScore,\n performanceScore,\n uptimeSeconds: latestMetrics?.uptimeSeconds ?? Math.floor(process.uptime()),\n lastSync: new Date().toISOString(),\n latestMetrics,\n recentIncidents,\n };\n }\n\n getHistory(): MetricsSnapshot[] {\n return [...this.history];\n }\n\n getIncidents(): Incident[] {\n return [...this.incidents];\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
interface MetricsSnapshot {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
cpuPercent: number;
|
|
4
|
+
memory: {
|
|
5
|
+
rssBytes: number;
|
|
6
|
+
heapUsedBytes: number;
|
|
7
|
+
heapTotalBytes: number;
|
|
8
|
+
systemFreeBytes: number;
|
|
9
|
+
systemTotalBytes: number;
|
|
10
|
+
};
|
|
11
|
+
eventLoopDelayMs: number;
|
|
12
|
+
uptimeSeconds: number;
|
|
13
|
+
}
|
|
14
|
+
type IncidentSource = "uncaughtException" | "unhandledRejection" | "memoryLeak" | "manual";
|
|
15
|
+
interface Incident {
|
|
16
|
+
id: string;
|
|
17
|
+
source: IncidentSource;
|
|
18
|
+
message: string;
|
|
19
|
+
stack?: string;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
context?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
interface HealthReportDetail {
|
|
24
|
+
healthScore: number;
|
|
25
|
+
performanceScore: number;
|
|
26
|
+
uptimeSeconds: number;
|
|
27
|
+
lastSync: string;
|
|
28
|
+
latestMetrics: MetricsSnapshot | null;
|
|
29
|
+
recentIncidents: Incident[];
|
|
30
|
+
}
|
|
31
|
+
interface WatcherOptions {
|
|
32
|
+
/** Sampling interval in milliseconds. Default 5000. */
|
|
33
|
+
intervalMs?: number;
|
|
34
|
+
/** Number of metric samples kept for trend analysis (e.g. memory leak detection). Default 60. */
|
|
35
|
+
historySize?: number;
|
|
36
|
+
/** Install process-level crash handlers (uncaughtException/unhandledRejection). Default true. */
|
|
37
|
+
captureCrashes?: boolean;
|
|
38
|
+
onIncident?: (incident: Incident) => void;
|
|
39
|
+
onMetrics?: (snapshot: MetricsSnapshot) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare class Watcher {
|
|
43
|
+
private readonly intervalMs;
|
|
44
|
+
private readonly historySize;
|
|
45
|
+
private readonly captureCrashes;
|
|
46
|
+
private readonly onIncidentCallback?;
|
|
47
|
+
private readonly onMetricsCallback?;
|
|
48
|
+
private sampler;
|
|
49
|
+
private timer;
|
|
50
|
+
private history;
|
|
51
|
+
private incidents;
|
|
52
|
+
private leakIncidentRaised;
|
|
53
|
+
private readonly handleUncaughtException;
|
|
54
|
+
private readonly handleUnhandledRejection;
|
|
55
|
+
constructor(options?: WatcherOptions);
|
|
56
|
+
start(): void;
|
|
57
|
+
stop(): void;
|
|
58
|
+
/** Takes and records a single sample immediately (also used by start()'s interval). */
|
|
59
|
+
tick(): MetricsSnapshot;
|
|
60
|
+
createIncident(source: IncidentSource, message: string, context?: Record<string, unknown>): Incident;
|
|
61
|
+
health(): HealthReportDetail;
|
|
62
|
+
getHistory(): MetricsSnapshot[];
|
|
63
|
+
getIncidents(): Incident[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Samples CPU (as % of one core) and event-loop delay across successive calls. */
|
|
67
|
+
declare class MetricsSampler {
|
|
68
|
+
private lastCpuUsage;
|
|
69
|
+
private lastSampleTime;
|
|
70
|
+
private readonly eventLoopMonitor;
|
|
71
|
+
constructor();
|
|
72
|
+
sample(): MetricsSnapshot;
|
|
73
|
+
dispose(): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 0-100 score: penalizes high CPU and event-loop delay. */
|
|
77
|
+
declare function computePerformanceScore(snapshot: MetricsSnapshot): number;
|
|
78
|
+
/** 0-100 score: performance plus a penalty for recent incident volume. */
|
|
79
|
+
declare function computeHealthScore(performanceScore: number, recentIncidentCount: number): number;
|
|
80
|
+
/**
|
|
81
|
+
* Detects a sustained upward trend in heap usage across the sample history —
|
|
82
|
+
* a lightweight proxy for a memory leak (not a definitive diagnosis).
|
|
83
|
+
*/
|
|
84
|
+
declare function detectMemoryLeakTrend(history: MetricsSnapshot[]): boolean;
|
|
85
|
+
|
|
86
|
+
export { type HealthReportDetail, type Incident, type IncidentSource, MetricsSampler, type MetricsSnapshot, Watcher, type WatcherOptions, computeHealthScore, computePerformanceScore, detectMemoryLeakTrend };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
interface MetricsSnapshot {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
cpuPercent: number;
|
|
4
|
+
memory: {
|
|
5
|
+
rssBytes: number;
|
|
6
|
+
heapUsedBytes: number;
|
|
7
|
+
heapTotalBytes: number;
|
|
8
|
+
systemFreeBytes: number;
|
|
9
|
+
systemTotalBytes: number;
|
|
10
|
+
};
|
|
11
|
+
eventLoopDelayMs: number;
|
|
12
|
+
uptimeSeconds: number;
|
|
13
|
+
}
|
|
14
|
+
type IncidentSource = "uncaughtException" | "unhandledRejection" | "memoryLeak" | "manual";
|
|
15
|
+
interface Incident {
|
|
16
|
+
id: string;
|
|
17
|
+
source: IncidentSource;
|
|
18
|
+
message: string;
|
|
19
|
+
stack?: string;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
context?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
interface HealthReportDetail {
|
|
24
|
+
healthScore: number;
|
|
25
|
+
performanceScore: number;
|
|
26
|
+
uptimeSeconds: number;
|
|
27
|
+
lastSync: string;
|
|
28
|
+
latestMetrics: MetricsSnapshot | null;
|
|
29
|
+
recentIncidents: Incident[];
|
|
30
|
+
}
|
|
31
|
+
interface WatcherOptions {
|
|
32
|
+
/** Sampling interval in milliseconds. Default 5000. */
|
|
33
|
+
intervalMs?: number;
|
|
34
|
+
/** Number of metric samples kept for trend analysis (e.g. memory leak detection). Default 60. */
|
|
35
|
+
historySize?: number;
|
|
36
|
+
/** Install process-level crash handlers (uncaughtException/unhandledRejection). Default true. */
|
|
37
|
+
captureCrashes?: boolean;
|
|
38
|
+
onIncident?: (incident: Incident) => void;
|
|
39
|
+
onMetrics?: (snapshot: MetricsSnapshot) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare class Watcher {
|
|
43
|
+
private readonly intervalMs;
|
|
44
|
+
private readonly historySize;
|
|
45
|
+
private readonly captureCrashes;
|
|
46
|
+
private readonly onIncidentCallback?;
|
|
47
|
+
private readonly onMetricsCallback?;
|
|
48
|
+
private sampler;
|
|
49
|
+
private timer;
|
|
50
|
+
private history;
|
|
51
|
+
private incidents;
|
|
52
|
+
private leakIncidentRaised;
|
|
53
|
+
private readonly handleUncaughtException;
|
|
54
|
+
private readonly handleUnhandledRejection;
|
|
55
|
+
constructor(options?: WatcherOptions);
|
|
56
|
+
start(): void;
|
|
57
|
+
stop(): void;
|
|
58
|
+
/** Takes and records a single sample immediately (also used by start()'s interval). */
|
|
59
|
+
tick(): MetricsSnapshot;
|
|
60
|
+
createIncident(source: IncidentSource, message: string, context?: Record<string, unknown>): Incident;
|
|
61
|
+
health(): HealthReportDetail;
|
|
62
|
+
getHistory(): MetricsSnapshot[];
|
|
63
|
+
getIncidents(): Incident[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Samples CPU (as % of one core) and event-loop delay across successive calls. */
|
|
67
|
+
declare class MetricsSampler {
|
|
68
|
+
private lastCpuUsage;
|
|
69
|
+
private lastSampleTime;
|
|
70
|
+
private readonly eventLoopMonitor;
|
|
71
|
+
constructor();
|
|
72
|
+
sample(): MetricsSnapshot;
|
|
73
|
+
dispose(): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 0-100 score: penalizes high CPU and event-loop delay. */
|
|
77
|
+
declare function computePerformanceScore(snapshot: MetricsSnapshot): number;
|
|
78
|
+
/** 0-100 score: performance plus a penalty for recent incident volume. */
|
|
79
|
+
declare function computeHealthScore(performanceScore: number, recentIncidentCount: number): number;
|
|
80
|
+
/**
|
|
81
|
+
* Detects a sustained upward trend in heap usage across the sample history —
|
|
82
|
+
* a lightweight proxy for a memory leak (not a definitive diagnosis).
|
|
83
|
+
*/
|
|
84
|
+
declare function detectMemoryLeakTrend(history: MetricsSnapshot[]): boolean;
|
|
85
|
+
|
|
86
|
+
export { type HealthReportDetail, type Incident, type IncidentSource, MetricsSampler, type MetricsSnapshot, Watcher, type WatcherOptions, computeHealthScore, computePerformanceScore, detectMemoryLeakTrend };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { totalmem, freemem } from 'os';
|
|
3
|
+
import { monitorEventLoopDelay } from 'perf_hooks';
|
|
4
|
+
|
|
5
|
+
// src/watcher.ts
|
|
6
|
+
var MetricsSampler = class {
|
|
7
|
+
lastCpuUsage = process.cpuUsage();
|
|
8
|
+
lastSampleTime = process.hrtime.bigint();
|
|
9
|
+
eventLoopMonitor = monitorEventLoopDelay({ resolution: 20 });
|
|
10
|
+
constructor() {
|
|
11
|
+
this.eventLoopMonitor.enable();
|
|
12
|
+
}
|
|
13
|
+
sample() {
|
|
14
|
+
const now = process.hrtime.bigint();
|
|
15
|
+
const elapsedMs = Number(now - this.lastSampleTime) / 1e6;
|
|
16
|
+
const cpuUsage = process.cpuUsage(this.lastCpuUsage);
|
|
17
|
+
const cpuMs = (cpuUsage.user + cpuUsage.system) / 1e3;
|
|
18
|
+
const cpuPercent = elapsedMs > 0 ? Math.min(100, cpuMs / elapsedMs * 100) : 0;
|
|
19
|
+
this.lastCpuUsage = process.cpuUsage();
|
|
20
|
+
this.lastSampleTime = now;
|
|
21
|
+
const mem = process.memoryUsage();
|
|
22
|
+
const eventLoopDelayMs = this.eventLoopMonitor.mean / 1e6 || 0;
|
|
23
|
+
this.eventLoopMonitor.reset();
|
|
24
|
+
return {
|
|
25
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26
|
+
cpuPercent: Number(cpuPercent.toFixed(2)),
|
|
27
|
+
memory: {
|
|
28
|
+
rssBytes: mem.rss,
|
|
29
|
+
heapUsedBytes: mem.heapUsed,
|
|
30
|
+
heapTotalBytes: mem.heapTotal,
|
|
31
|
+
systemFreeBytes: freemem(),
|
|
32
|
+
systemTotalBytes: totalmem()
|
|
33
|
+
},
|
|
34
|
+
eventLoopDelayMs: Number(eventLoopDelayMs.toFixed(2)),
|
|
35
|
+
uptimeSeconds: Math.floor(process.uptime())
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
dispose() {
|
|
39
|
+
this.eventLoopMonitor.disable();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/scoring.ts
|
|
44
|
+
function computePerformanceScore(snapshot) {
|
|
45
|
+
const cpuPenalty = snapshot.cpuPercent;
|
|
46
|
+
const eventLoopPenalty = Math.min(50, snapshot.eventLoopDelayMs);
|
|
47
|
+
const score = 100 - cpuPenalty * 0.5 - eventLoopPenalty;
|
|
48
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
49
|
+
}
|
|
50
|
+
function computeHealthScore(performanceScore, recentIncidentCount) {
|
|
51
|
+
const incidentPenalty = Math.min(60, recentIncidentCount * 15);
|
|
52
|
+
return Math.max(0, Math.min(100, performanceScore - incidentPenalty));
|
|
53
|
+
}
|
|
54
|
+
function detectMemoryLeakTrend(history) {
|
|
55
|
+
if (history.length < 6) return false;
|
|
56
|
+
const heapValues = history.map((s) => s.memory.heapUsedBytes);
|
|
57
|
+
const midpoint = Math.floor(heapValues.length / 2);
|
|
58
|
+
const firstHalfAvg = average(heapValues.slice(0, midpoint));
|
|
59
|
+
const secondHalfAvg = average(heapValues.slice(midpoint));
|
|
60
|
+
if (firstHalfAvg === 0) return false;
|
|
61
|
+
const growth = (secondHalfAvg - firstHalfAvg) / firstHalfAvg;
|
|
62
|
+
return growth > 0.2;
|
|
63
|
+
}
|
|
64
|
+
function average(values) {
|
|
65
|
+
if (values.length === 0) return 0;
|
|
66
|
+
return values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/watcher.ts
|
|
70
|
+
var INCIDENT_WINDOW_MS = 5 * 60 * 1e3;
|
|
71
|
+
var Watcher = class {
|
|
72
|
+
intervalMs;
|
|
73
|
+
historySize;
|
|
74
|
+
captureCrashes;
|
|
75
|
+
onIncidentCallback;
|
|
76
|
+
onMetricsCallback;
|
|
77
|
+
sampler = null;
|
|
78
|
+
timer = null;
|
|
79
|
+
history = [];
|
|
80
|
+
incidents = [];
|
|
81
|
+
leakIncidentRaised = false;
|
|
82
|
+
handleUncaughtException = (error) => {
|
|
83
|
+
this.createIncident("uncaughtException", error.message, { stack: error.stack });
|
|
84
|
+
};
|
|
85
|
+
handleUnhandledRejection = (reason) => {
|
|
86
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
87
|
+
const stack = reason instanceof Error ? reason.stack : void 0;
|
|
88
|
+
this.createIncident("unhandledRejection", message, { stack });
|
|
89
|
+
};
|
|
90
|
+
constructor(options = {}) {
|
|
91
|
+
this.intervalMs = options.intervalMs ?? 5e3;
|
|
92
|
+
this.historySize = options.historySize ?? 60;
|
|
93
|
+
this.captureCrashes = options.captureCrashes ?? true;
|
|
94
|
+
this.onIncidentCallback = options.onIncident;
|
|
95
|
+
this.onMetricsCallback = options.onMetrics;
|
|
96
|
+
}
|
|
97
|
+
start() {
|
|
98
|
+
if (this.timer) return;
|
|
99
|
+
this.sampler = new MetricsSampler();
|
|
100
|
+
this.timer = setInterval(() => this.tick(), this.intervalMs);
|
|
101
|
+
this.timer.unref?.();
|
|
102
|
+
if (this.captureCrashes) {
|
|
103
|
+
process.on("uncaughtException", this.handleUncaughtException);
|
|
104
|
+
process.on("unhandledRejection", this.handleUnhandledRejection);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
stop() {
|
|
108
|
+
if (this.timer) {
|
|
109
|
+
clearInterval(this.timer);
|
|
110
|
+
this.timer = null;
|
|
111
|
+
}
|
|
112
|
+
this.sampler?.dispose();
|
|
113
|
+
this.sampler = null;
|
|
114
|
+
if (this.captureCrashes) {
|
|
115
|
+
process.off("uncaughtException", this.handleUncaughtException);
|
|
116
|
+
process.off("unhandledRejection", this.handleUnhandledRejection);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Takes and records a single sample immediately (also used by start()'s interval). */
|
|
120
|
+
tick() {
|
|
121
|
+
const sampler = this.sampler ?? (this.sampler = new MetricsSampler());
|
|
122
|
+
const snapshot = sampler.sample();
|
|
123
|
+
this.history.push(snapshot);
|
|
124
|
+
if (this.history.length > this.historySize) this.history.shift();
|
|
125
|
+
if (detectMemoryLeakTrend(this.history) && !this.leakIncidentRaised) {
|
|
126
|
+
this.leakIncidentRaised = true;
|
|
127
|
+
this.createIncident("memoryLeak", "Sustained heap growth detected across recent samples");
|
|
128
|
+
}
|
|
129
|
+
this.onMetricsCallback?.(snapshot);
|
|
130
|
+
return snapshot;
|
|
131
|
+
}
|
|
132
|
+
createIncident(source, message, context) {
|
|
133
|
+
const incident = {
|
|
134
|
+
id: randomUUID(),
|
|
135
|
+
source,
|
|
136
|
+
message,
|
|
137
|
+
stack: context?.stack,
|
|
138
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
139
|
+
context
|
|
140
|
+
};
|
|
141
|
+
this.incidents.push(incident);
|
|
142
|
+
this.onIncidentCallback?.(incident);
|
|
143
|
+
return incident;
|
|
144
|
+
}
|
|
145
|
+
health() {
|
|
146
|
+
const latestMetrics = this.history.at(-1) ?? null;
|
|
147
|
+
const recentIncidents = this.incidents.filter(
|
|
148
|
+
(incident) => Date.now() - new Date(incident.timestamp).getTime() < INCIDENT_WINDOW_MS
|
|
149
|
+
);
|
|
150
|
+
const performanceScore = latestMetrics ? computePerformanceScore(latestMetrics) : 100;
|
|
151
|
+
const healthScore = computeHealthScore(performanceScore, recentIncidents.length);
|
|
152
|
+
return {
|
|
153
|
+
healthScore,
|
|
154
|
+
performanceScore,
|
|
155
|
+
uptimeSeconds: latestMetrics?.uptimeSeconds ?? Math.floor(process.uptime()),
|
|
156
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
157
|
+
latestMetrics,
|
|
158
|
+
recentIncidents
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
getHistory() {
|
|
162
|
+
return [...this.history];
|
|
163
|
+
}
|
|
164
|
+
getIncidents() {
|
|
165
|
+
return [...this.incidents];
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export { MetricsSampler, Watcher, computeHealthScore, computePerformanceScore, detectMemoryLeakTrend };
|
|
170
|
+
//# sourceMappingURL=index.js.map
|
|
171
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/metrics.ts","../src/scoring.ts","../src/watcher.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { freemem, totalmem } from \"node:os\";\nimport { monitorEventLoopDelay } from \"node:perf_hooks\";\nimport type { MetricsSnapshot } from \"./types.js\";\n\n/** Samples CPU (as % of one core) and event-loop delay across successive calls. */\nexport class MetricsSampler {\n private lastCpuUsage = process.cpuUsage();\n private lastSampleTime = process.hrtime.bigint();\n private readonly eventLoopMonitor = monitorEventLoopDelay({ resolution: 20 });\n\n constructor() {\n this.eventLoopMonitor.enable();\n }\n\n sample(): MetricsSnapshot {\n const now = process.hrtime.bigint();\n const elapsedMs = Number(now - this.lastSampleTime) / 1e6;\n const cpuUsage = process.cpuUsage(this.lastCpuUsage);\n const cpuMs = (cpuUsage.user + cpuUsage.system) / 1000;\n const cpuPercent = elapsedMs > 0 ? Math.min(100, (cpuMs / elapsedMs) * 100) : 0;\n\n this.lastCpuUsage = process.cpuUsage();\n this.lastSampleTime = now;\n\n const mem = process.memoryUsage();\n const eventLoopDelayMs = this.eventLoopMonitor.mean / 1e6 || 0;\n this.eventLoopMonitor.reset();\n\n return {\n timestamp: new Date().toISOString(),\n cpuPercent: Number(cpuPercent.toFixed(2)),\n memory: {\n rssBytes: mem.rss,\n heapUsedBytes: mem.heapUsed,\n heapTotalBytes: mem.heapTotal,\n systemFreeBytes: freemem(),\n systemTotalBytes: totalmem(),\n },\n eventLoopDelayMs: Number(eventLoopDelayMs.toFixed(2)),\n uptimeSeconds: Math.floor(process.uptime()),\n };\n }\n\n dispose(): void {\n this.eventLoopMonitor.disable();\n }\n}\n","import type { MetricsSnapshot } from \"./types.js\";\n\n/** 0-100 score: penalizes high CPU and event-loop delay. */\nexport function computePerformanceScore(snapshot: MetricsSnapshot): number {\n const cpuPenalty = snapshot.cpuPercent;\n const eventLoopPenalty = Math.min(50, snapshot.eventLoopDelayMs);\n const score = 100 - cpuPenalty * 0.5 - eventLoopPenalty;\n return Math.max(0, Math.min(100, Math.round(score)));\n}\n\n/** 0-100 score: performance plus a penalty for recent incident volume. */\nexport function computeHealthScore(\n performanceScore: number,\n recentIncidentCount: number,\n): number {\n const incidentPenalty = Math.min(60, recentIncidentCount * 15);\n return Math.max(0, Math.min(100, performanceScore - incidentPenalty));\n}\n\n/**\n * Detects a sustained upward trend in heap usage across the sample history —\n * a lightweight proxy for a memory leak (not a definitive diagnosis).\n */\nexport function detectMemoryLeakTrend(history: MetricsSnapshot[]): boolean {\n if (history.length < 6) return false;\n\n const heapValues = history.map((s) => s.memory.heapUsedBytes);\n const midpoint = Math.floor(heapValues.length / 2);\n const firstHalfAvg = average(heapValues.slice(0, midpoint));\n const secondHalfAvg = average(heapValues.slice(midpoint));\n\n if (firstHalfAvg === 0) return false;\n const growth = (secondHalfAvg - firstHalfAvg) / firstHalfAvg;\n return growth > 0.2;\n}\n\nfunction average(values: number[]): number {\n if (values.length === 0) return 0;\n return values.reduce((sum, v) => sum + v, 0) / values.length;\n}\n","import { randomUUID } from \"node:crypto\";\nimport { MetricsSampler } from \"./metrics.js\";\nimport { computeHealthScore, computePerformanceScore, detectMemoryLeakTrend } from \"./scoring.js\";\nimport type {\n HealthReportDetail,\n Incident,\n IncidentSource,\n MetricsSnapshot,\n WatcherOptions,\n} from \"./types.js\";\n\nconst INCIDENT_WINDOW_MS = 5 * 60 * 1000;\n\nexport class Watcher {\n private readonly intervalMs: number;\n private readonly historySize: number;\n private readonly captureCrashes: boolean;\n private readonly onIncidentCallback?: (incident: Incident) => void;\n private readonly onMetricsCallback?: (snapshot: MetricsSnapshot) => void;\n\n private sampler: MetricsSampler | null = null;\n private timer: ReturnType<typeof setInterval> | null = null;\n private history: MetricsSnapshot[] = [];\n private incidents: Incident[] = [];\n private leakIncidentRaised = false;\n\n private readonly handleUncaughtException = (error: Error) => {\n this.createIncident(\"uncaughtException\", error.message, { stack: error.stack });\n };\n\n private readonly handleUnhandledRejection = (reason: unknown) => {\n const message = reason instanceof Error ? reason.message : String(reason);\n const stack = reason instanceof Error ? reason.stack : undefined;\n this.createIncident(\"unhandledRejection\", message, { stack });\n };\n\n constructor(options: WatcherOptions = {}) {\n this.intervalMs = options.intervalMs ?? 5000;\n this.historySize = options.historySize ?? 60;\n this.captureCrashes = options.captureCrashes ?? true;\n this.onIncidentCallback = options.onIncident;\n this.onMetricsCallback = options.onMetrics;\n }\n\n start(): void {\n if (this.timer) return;\n\n this.sampler = new MetricsSampler();\n this.timer = setInterval(() => this.tick(), this.intervalMs);\n this.timer.unref?.();\n\n if (this.captureCrashes) {\n process.on(\"uncaughtException\", this.handleUncaughtException);\n process.on(\"unhandledRejection\", this.handleUnhandledRejection);\n }\n }\n\n stop(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n this.sampler?.dispose();\n this.sampler = null;\n\n if (this.captureCrashes) {\n process.off(\"uncaughtException\", this.handleUncaughtException);\n process.off(\"unhandledRejection\", this.handleUnhandledRejection);\n }\n }\n\n /** Takes and records a single sample immediately (also used by start()'s interval). */\n tick(): MetricsSnapshot {\n const sampler = this.sampler ?? (this.sampler = new MetricsSampler());\n const snapshot = sampler.sample();\n\n this.history.push(snapshot);\n if (this.history.length > this.historySize) this.history.shift();\n\n if (detectMemoryLeakTrend(this.history) && !this.leakIncidentRaised) {\n this.leakIncidentRaised = true;\n this.createIncident(\"memoryLeak\", \"Sustained heap growth detected across recent samples\");\n }\n\n this.onMetricsCallback?.(snapshot);\n return snapshot;\n }\n\n createIncident(\n source: IncidentSource,\n message: string,\n context?: Record<string, unknown>,\n ): Incident {\n const incident: Incident = {\n id: randomUUID(),\n source,\n message,\n stack: context?.stack as string | undefined,\n timestamp: new Date().toISOString(),\n context,\n };\n\n this.incidents.push(incident);\n this.onIncidentCallback?.(incident);\n return incident;\n }\n\n health(): HealthReportDetail {\n const latestMetrics = this.history.at(-1) ?? null;\n const recentIncidents = this.incidents.filter(\n (incident) => Date.now() - new Date(incident.timestamp).getTime() < INCIDENT_WINDOW_MS,\n );\n\n const performanceScore = latestMetrics ? computePerformanceScore(latestMetrics) : 100;\n const healthScore = computeHealthScore(performanceScore, recentIncidents.length);\n\n return {\n healthScore,\n performanceScore,\n uptimeSeconds: latestMetrics?.uptimeSeconds ?? Math.floor(process.uptime()),\n lastSync: new Date().toISOString(),\n latestMetrics,\n recentIncidents,\n };\n }\n\n getHistory(): MetricsSnapshot[] {\n return [...this.history];\n }\n\n getIncidents(): Incident[] {\n return [...this.incidents];\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@appbuildersph/watcher",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Application watcher for the App Builders PH SDK — CPU/RAM/health monitoring, crash capture, and incident creation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@appbuildersph/shared": "0.0.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^8.3.5",
|
|
24
|
+
"typescript": "^5.6.3",
|
|
25
|
+
"vitest": "^2.1.4"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"dev": "tsup --watch",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run"
|
|
32
|
+
}
|
|
33
|
+
}
|