@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 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"]}
@@ -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 };
@@ -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
+ }