@ceon-oy/monitor-sdk 1.2.1 → 1.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Ceon Monitor SDK
2
2
 
3
- Lightweight client SDK for integrating with the Ceon Monitor service. Provides error reporting, technology tracking, vulnerability auditing, and security event monitoring.
3
+ Lightweight client SDK for integrating with the Ceon Monitor service. Provides error reporting, technology tracking, vulnerability auditing, system metrics collection (CPU, RAM, disk), and security event monitoring.
4
4
 
5
5
  ## Table of Contents
6
6
 
@@ -12,6 +12,7 @@ Lightweight client SDK for integrating with the Ceon Monitor service. Provides e
12
12
  - [Technology Tracking](#technology-tracking)
13
13
  - [Vulnerability Auditing](#vulnerability-auditing)
14
14
  - [SDK-Based Health Checks](#sdk-based-health-checks)
15
+ - [System Metrics](#system-metrics)
15
16
  - [Security Events](#security-events)
16
17
  - [Framework Examples](#framework-examples)
17
18
  - [Express.js](#expressjs)
@@ -490,6 +491,75 @@ const results = [
490
491
  await monitor.submitHealthResults(results);
491
492
  ```
492
493
 
494
+ ### System Metrics
495
+
496
+ The SDK automatically collects and reports server resource usage — CPU, RAM, and disk — to Ceon Monitor. No SDK configuration flag is needed; the feature activates based on the project settings configured in the Ceon Monitor dashboard.
497
+
498
+ #### How It Works
499
+
500
+ When the `MonitorClient` is created, it fetches your project settings from the server. If the project has **Enable Metric Collection** turned on, the SDK:
501
+
502
+ 1. Reads `metricsIntervalSeconds` from project settings (configured in the dashboard)
503
+ 2. Reads `metricsDiskPaths` from project settings (e.g. `["/", "/data"]`)
504
+ 3. Starts collecting metrics at that interval automatically
505
+ 4. POSTs metrics to the server — no manual calls needed
506
+
507
+ #### What Is Collected
508
+
509
+ | Metric | Method | Notes |
510
+ |--------|--------|-------|
511
+ | CPU usage % | Dual `os.cpus()` sample with 500 ms gap | Accurate active-cycle average |
512
+ | RAM total | `os.totalmem()` | Bytes |
513
+ | RAM used | `os.totalmem() - os.freemem()` | Bytes |
514
+ | RAM % | `usedMemory / totalMemory * 100` | |
515
+ | Disk total / used / free per path | `fs.statfs(path)` | Node.js 18.15+ built-in, zero external deps |
516
+
517
+ Hostname is automatically set via `os.hostname()` and included with every report.
518
+
519
+ #### Enable in Dashboard
520
+
521
+ 1. Open the **Ceon Monitor** dashboard
522
+ 2. Go to **Projects** and click **Edit** on your project
523
+ 3. Enable the **System Metrics** widget
524
+ 4. Click **Enable Metric Collection**
525
+ 5. Set the collection interval (e.g. every 60 seconds)
526
+ 6. Add disk paths to monitor (e.g. `/` for root, `/data` for a data volume)
527
+ 7. Configure CPU / RAM / disk alert thresholds
528
+ 8. Save
529
+
530
+ The SDK will pick up the new settings on its next startup.
531
+
532
+ #### Notifications
533
+
534
+ When a metric exceeds a configured threshold, Ceon Monitor sends a `SYSTEM_METRIC_CRITICAL` notification to any user who has subscribed to **System Metric Alerts** for that project. Subscribe in **Settings → Subscriptions**.
535
+
536
+ #### Node.js Version Requirement
537
+
538
+ `fs.statfs()` (used for disk monitoring) requires **Node.js 18.15 or later**. No external packages are needed.
539
+
540
+ #### Manual Metric Submission
541
+
542
+ You can also submit a metric reading manually:
543
+
544
+ ```typescript
545
+ await monitor.submitSystemMetric({
546
+ hostname: os.hostname(),
547
+ cpuPercent: 72.5,
548
+ memoryTotal: 8 * 1024 ** 3, // 8 GB in bytes
549
+ memoryUsed: 5 * 1024 ** 3, // 5 GB in bytes
550
+ memoryPercent: 62.5,
551
+ disks: [
552
+ {
553
+ path: '/',
554
+ total: 100 * 1024 ** 3, // 100 GB
555
+ used: 60 * 1024 ** 3, // 60 GB
556
+ free: 40 * 1024 ** 3, // 40 GB
557
+ usedPercent: 60,
558
+ },
559
+ ],
560
+ });
561
+ ```
562
+
493
563
  ### Security Events
494
564
 
495
565
  #### Report Security Event
@@ -1092,6 +1162,10 @@ Runs npm audit and sends results to the server.
1092
1162
 
1093
1163
  Runs npm audit on all directories configured in `auditPaths` and returns a combined summary.
1094
1164
 
1165
+ #### `submitSystemMetric(metric: SystemMetric): Promise<void>`
1166
+
1167
+ Manually submits a system metric reading. Under normal usage this is called automatically by the SDK's internal collection loop — use this only if you need custom collection logic.
1168
+
1095
1169
  #### `flush(): Promise<void>`
1096
1170
 
1097
1171
  Immediately sends all queued errors.
@@ -1160,6 +1234,23 @@ interface AuditPath {
1160
1234
  environment: string; // Environment label (e.g., 'server', 'client')
1161
1235
  }
1162
1236
 
1237
+ interface DiskMetric {
1238
+ path: string; // Mount path (e.g. '/', '/data')
1239
+ total: number; // Total bytes
1240
+ used: number; // Used bytes
1241
+ free: number; // Free bytes
1242
+ usedPercent: number; // 0–100
1243
+ }
1244
+
1245
+ interface SystemMetric {
1246
+ hostname: string;
1247
+ cpuPercent: number;
1248
+ memoryTotal: number;
1249
+ memoryUsed: number;
1250
+ memoryPercent: number;
1251
+ disks: DiskMetric[];
1252
+ }
1253
+
1163
1254
  interface MultiAuditSummary {
1164
1255
  results: Array<{
1165
1256
  environment: string;
package/dist/index.d.mts CHANGED
@@ -53,7 +53,7 @@ interface MonitorClientConfig {
53
53
  allowInsecureHttp?: boolean;
54
54
  /** Custom npm registry URL for fetching latest versions (default: https://registry.npmjs.org) */
55
55
  npmRegistryUrl?: string;
56
- /** Enable SDK-based health check polling (default: false) */
56
+ /** Enable SDK-based health check polling (default: true) */
57
57
  healthCheckEnabled?: boolean;
58
58
  /** Interval to fetch health endpoints from server in ms (default: 60000) */
59
59
  healthCheckFetchIntervalMs?: number;
@@ -166,6 +166,7 @@ interface SdkHealthEndpoint {
166
166
  intervalMs: number;
167
167
  timeoutMs: number;
168
168
  expectedStatus: number;
169
+ allowInsecureTls?: boolean;
169
170
  }
170
171
  interface SdkHealthResult {
171
172
  endpointId: string;
@@ -226,6 +227,12 @@ declare class MonitorClient {
226
227
  private healthCheckTimers;
227
228
  private healthCheckResultsQueue;
228
229
  private healthCheckFlushTimer;
230
+ private sdkErrorQueue;
231
+ private sdkErrorFlushTimer;
232
+ private sdkErrorsInCurrentWindow;
233
+ private sdkErrorWindowResetTimer;
234
+ private metricsCollectionTimer;
235
+ private metricsSettingsCheckTimer;
229
236
  constructor(config: MonitorClientConfig);
230
237
  /**
231
238
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -253,6 +260,15 @@ declare class MonitorClient {
253
260
  flush(): Promise<void>;
254
261
  private getErrorKey;
255
262
  close(): Promise<void>;
263
+ /**
264
+ * Queue an SDK error to be reported to the server's system errors page.
265
+ * Fire-and-forget — never throws. If reporting itself fails, logs to console only.
266
+ * Throttled to max 20 errors per minute to prevent flooding.
267
+ */
268
+ private reportError;
269
+ private startSdkErrorFlushTimer;
270
+ private stopSdkErrorFlushTimer;
271
+ private flushSdkErrors;
256
272
  private stopAuditIntervalTimer;
257
273
  /**
258
274
  * Fetch project settings from the monitoring server.
@@ -263,6 +279,12 @@ declare class MonitorClient {
263
279
  vulnerabilityScanIntervalHours: number;
264
280
  scanRequestedAt: string | null;
265
281
  techScanRequestedAt: string | null;
282
+ metricsEnabled?: boolean;
283
+ metricsIntervalSeconds?: number;
284
+ metricsDiskPaths?: string[];
285
+ metricsCpuThreshold?: number;
286
+ metricsRamThreshold?: number;
287
+ metricsDiskThreshold?: number;
266
288
  } | null>;
267
289
  /**
268
290
  * Setup automatic vulnerability scanning based on server-configured interval.
@@ -465,6 +487,47 @@ declare class MonitorClient {
465
487
  * Stop all health check timers
466
488
  */
467
489
  private stopHealthCheckTimers;
490
+ /**
491
+ * Start system metrics collection based on server-configured settings.
492
+ * Only runs if metricsEnabled is true in project settings.
493
+ * Collects CPU, RAM, and disk usage using built-in Node.js modules.
494
+ */
495
+ setupSystemMetricsCollection(): Promise<void>;
496
+ private startMetricsCollection;
497
+ private stopMetricsCollection;
498
+ /**
499
+ * Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
500
+ * and submit to the monitoring server.
501
+ *
502
+ * Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
503
+ * Zero dependencies — all built-in Node.js 18.15+
504
+ */
505
+ private collectAndSubmitMetrics;
506
+ /**
507
+ * Measure CPU utilization by sampling cpus() twice with a 500ms gap.
508
+ * Returns a percentage (0–100).
509
+ */
510
+ private measureCpuPercent;
511
+ /**
512
+ * Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
513
+ */
514
+ private collectDiskMetrics;
515
+ /**
516
+ * Submit a system metric snapshot to the monitoring server.
517
+ */
518
+ submitSystemMetric(metric: {
519
+ hostname: string;
520
+ cpuPercent: number;
521
+ memoryTotal: number;
522
+ memoryUsed: number;
523
+ memoryPercent: number;
524
+ disks: Array<{
525
+ path: string;
526
+ total: number;
527
+ used: number;
528
+ percent: number;
529
+ }>;
530
+ }): Promise<void>;
468
531
  }
469
532
 
470
533
  export { type AuditPath, type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, type HealthStatus, MonitorClient, type MonitorClientConfig, type MultiAuditSummary, type SdkHealthEndpoint, type SdkHealthEndpointsResponse, type SdkHealthResult, type SdkHealthResultsResponse, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
package/dist/index.d.ts CHANGED
@@ -53,7 +53,7 @@ interface MonitorClientConfig {
53
53
  allowInsecureHttp?: boolean;
54
54
  /** Custom npm registry URL for fetching latest versions (default: https://registry.npmjs.org) */
55
55
  npmRegistryUrl?: string;
56
- /** Enable SDK-based health check polling (default: false) */
56
+ /** Enable SDK-based health check polling (default: true) */
57
57
  healthCheckEnabled?: boolean;
58
58
  /** Interval to fetch health endpoints from server in ms (default: 60000) */
59
59
  healthCheckFetchIntervalMs?: number;
@@ -166,6 +166,7 @@ interface SdkHealthEndpoint {
166
166
  intervalMs: number;
167
167
  timeoutMs: number;
168
168
  expectedStatus: number;
169
+ allowInsecureTls?: boolean;
169
170
  }
170
171
  interface SdkHealthResult {
171
172
  endpointId: string;
@@ -226,6 +227,12 @@ declare class MonitorClient {
226
227
  private healthCheckTimers;
227
228
  private healthCheckResultsQueue;
228
229
  private healthCheckFlushTimer;
230
+ private sdkErrorQueue;
231
+ private sdkErrorFlushTimer;
232
+ private sdkErrorsInCurrentWindow;
233
+ private sdkErrorWindowResetTimer;
234
+ private metricsCollectionTimer;
235
+ private metricsSettingsCheckTimer;
229
236
  constructor(config: MonitorClientConfig);
230
237
  /**
231
238
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -253,6 +260,15 @@ declare class MonitorClient {
253
260
  flush(): Promise<void>;
254
261
  private getErrorKey;
255
262
  close(): Promise<void>;
263
+ /**
264
+ * Queue an SDK error to be reported to the server's system errors page.
265
+ * Fire-and-forget — never throws. If reporting itself fails, logs to console only.
266
+ * Throttled to max 20 errors per minute to prevent flooding.
267
+ */
268
+ private reportError;
269
+ private startSdkErrorFlushTimer;
270
+ private stopSdkErrorFlushTimer;
271
+ private flushSdkErrors;
256
272
  private stopAuditIntervalTimer;
257
273
  /**
258
274
  * Fetch project settings from the monitoring server.
@@ -263,6 +279,12 @@ declare class MonitorClient {
263
279
  vulnerabilityScanIntervalHours: number;
264
280
  scanRequestedAt: string | null;
265
281
  techScanRequestedAt: string | null;
282
+ metricsEnabled?: boolean;
283
+ metricsIntervalSeconds?: number;
284
+ metricsDiskPaths?: string[];
285
+ metricsCpuThreshold?: number;
286
+ metricsRamThreshold?: number;
287
+ metricsDiskThreshold?: number;
266
288
  } | null>;
267
289
  /**
268
290
  * Setup automatic vulnerability scanning based on server-configured interval.
@@ -465,6 +487,47 @@ declare class MonitorClient {
465
487
  * Stop all health check timers
466
488
  */
467
489
  private stopHealthCheckTimers;
490
+ /**
491
+ * Start system metrics collection based on server-configured settings.
492
+ * Only runs if metricsEnabled is true in project settings.
493
+ * Collects CPU, RAM, and disk usage using built-in Node.js modules.
494
+ */
495
+ setupSystemMetricsCollection(): Promise<void>;
496
+ private startMetricsCollection;
497
+ private stopMetricsCollection;
498
+ /**
499
+ * Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
500
+ * and submit to the monitoring server.
501
+ *
502
+ * Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
503
+ * Zero dependencies — all built-in Node.js 18.15+
504
+ */
505
+ private collectAndSubmitMetrics;
506
+ /**
507
+ * Measure CPU utilization by sampling cpus() twice with a 500ms gap.
508
+ * Returns a percentage (0–100).
509
+ */
510
+ private measureCpuPercent;
511
+ /**
512
+ * Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
513
+ */
514
+ private collectDiskMetrics;
515
+ /**
516
+ * Submit a system metric snapshot to the monitoring server.
517
+ */
518
+ submitSystemMetric(metric: {
519
+ hostname: string;
520
+ cpuPercent: number;
521
+ memoryTotal: number;
522
+ memoryUsed: number;
523
+ memoryPercent: number;
524
+ disks: Array<{
525
+ path: string;
526
+ total: number;
527
+ used: number;
528
+ percent: number;
529
+ }>;
530
+ }): Promise<void>;
468
531
  }
469
532
 
470
533
  export { type AuditPath, type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, type HealthStatus, MonitorClient, type MonitorClientConfig, type MultiAuditSummary, type SdkHealthEndpoint, type SdkHealthEndpointsResponse, type SdkHealthResult, type SdkHealthResultsResponse, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
package/dist/index.js CHANGED
@@ -94,6 +94,14 @@ var MonitorClient = class {
94
94
  this.healthCheckTimers = /* @__PURE__ */ new Map();
95
95
  this.healthCheckResultsQueue = [];
96
96
  this.healthCheckFlushTimer = null;
97
+ // SDK error reporting queue (errors visible on admin system errors page)
98
+ this.sdkErrorQueue = [];
99
+ this.sdkErrorFlushTimer = null;
100
+ this.sdkErrorsInCurrentWindow = 0;
101
+ this.sdkErrorWindowResetTimer = null;
102
+ // System metrics collection (on-premise only, opt-in)
103
+ this.metricsCollectionTimer = null;
104
+ this.metricsSettingsCheckTimer = null;
97
105
  if (!config.apiKey || config.apiKey.trim().length === 0) {
98
106
  throw new Error("[MonitorClient] API key is required");
99
107
  }
@@ -153,24 +161,28 @@ var MonitorClient = class {
153
161
  this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
154
162
  this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
155
163
  this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
156
- this.healthCheckEnabled = config.healthCheckEnabled || false;
164
+ this.healthCheckEnabled = config.healthCheckEnabled ?? true;
157
165
  this.healthCheckFetchIntervalMs = config.healthCheckFetchIntervalMs || CONFIG_LIMITS.HEALTH_CHECK_FETCH_INTERVAL_MS;
158
166
  this.startFlushTimer();
159
167
  if (this.trackDependencies) {
160
168
  this.syncDependencies().catch((err) => {
161
- console.error("[MonitorClient] Failed to sync dependencies:", err instanceof Error ? err.message : String(err));
169
+ this.reportError("DEPENDENCY_SYNC", "Failed to sync dependencies on startup", err);
162
170
  });
163
171
  }
164
172
  if (this.autoAudit) {
165
173
  this.setupAutoAudit().catch((err) => {
166
- console.error("[MonitorClient] Failed to setup auto audit:", err instanceof Error ? err.message : String(err));
174
+ this.reportError("AUDIT_SETUP", "Failed to setup auto audit", err);
167
175
  });
168
176
  }
169
177
  if (this.healthCheckEnabled) {
170
178
  this.setupHealthCheckPolling().catch((err) => {
171
- console.error("[MonitorClient] Failed to setup health check polling:", err instanceof Error ? err.message : String(err));
179
+ this.reportError("HEALTH_POLLING_SETUP", "Failed to setup health check polling", err);
172
180
  });
173
181
  }
182
+ this.startSdkErrorFlushTimer();
183
+ this.setupSystemMetricsCollection().catch((err) => {
184
+ this.reportError("SYSTEM_METRICS_SETUP", "Failed to setup system metrics collection", err);
185
+ });
174
186
  }
175
187
  /**
176
188
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -320,8 +332,66 @@ var MonitorClient = class {
320
332
  this.stopFlushTimer();
321
333
  this.stopAuditIntervalTimer();
322
334
  this.stopHealthCheckTimers();
335
+ this.stopSdkErrorFlushTimer();
336
+ this.stopMetricsCollection();
323
337
  await this.flush();
324
338
  await this.flushHealthResults();
339
+ await this.flushSdkErrors();
340
+ }
341
+ /**
342
+ * Queue an SDK error to be reported to the server's system errors page.
343
+ * Fire-and-forget — never throws. If reporting itself fails, logs to console only.
344
+ * Throttled to max 20 errors per minute to prevent flooding.
345
+ */
346
+ reportError(category, message, err) {
347
+ if (this.sdkErrorsInCurrentWindow >= 20) return;
348
+ this.sdkErrorsInCurrentWindow++;
349
+ const errorMessage = err instanceof Error ? `${message}: ${err.message}` : message;
350
+ const stack = err instanceof Error ? err.stack : void 0;
351
+ this.sdkErrorQueue.push({ category, message: errorMessage, stack });
352
+ if (this.sdkErrorQueue.length >= 20) {
353
+ this.flushSdkErrors().catch(() => {
354
+ });
355
+ }
356
+ }
357
+ startSdkErrorFlushTimer() {
358
+ this.sdkErrorFlushTimer = setInterval(() => {
359
+ this.flushSdkErrors().catch(() => {
360
+ });
361
+ }, 3e4);
362
+ this.sdkErrorWindowResetTimer = setInterval(() => {
363
+ this.sdkErrorsInCurrentWindow = 0;
364
+ }, 6e4);
365
+ }
366
+ stopSdkErrorFlushTimer() {
367
+ if (this.sdkErrorFlushTimer) {
368
+ clearInterval(this.sdkErrorFlushTimer);
369
+ this.sdkErrorFlushTimer = null;
370
+ }
371
+ if (this.sdkErrorWindowResetTimer) {
372
+ clearInterval(this.sdkErrorWindowResetTimer);
373
+ this.sdkErrorWindowResetTimer = null;
374
+ }
375
+ }
376
+ async flushSdkErrors() {
377
+ if (this.sdkErrorQueue.length === 0) return;
378
+ const errors = [...this.sdkErrorQueue];
379
+ this.sdkErrorQueue = [];
380
+ try {
381
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/sdk-errors`, {
382
+ method: "POST",
383
+ headers: {
384
+ "Content-Type": "application/json",
385
+ "Authorization": `Bearer ${this.apiKey}`
386
+ },
387
+ body: JSON.stringify({ errors })
388
+ });
389
+ if (!response.ok) {
390
+ console.warn(`[MonitorClient] Failed to report SDK errors: HTTP ${response.status}`);
391
+ }
392
+ } catch (err) {
393
+ console.warn("[MonitorClient] Failed to flush SDK errors to server:", err instanceof Error ? err.message : String(err));
394
+ }
325
395
  }
326
396
  stopAuditIntervalTimer() {
327
397
  if (this.auditIntervalTimer) {
@@ -383,14 +453,14 @@ var MonitorClient = class {
383
453
  const intervalMs = intervalHours * 60 * 60 * 1e3;
384
454
  this.auditIntervalTimer = setInterval(() => {
385
455
  this.runScanAndTrackTime().catch((err) => {
386
- console.error("[MonitorClient] Auto audit scan failed:", err);
456
+ this.reportError("AUDIT_SCAN", "Auto audit scan failed", err);
387
457
  });
388
458
  }, intervalMs);
389
459
  }
390
460
  console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
391
461
  this.settingsPollingTimer = setInterval(() => {
392
462
  this.checkForScanRequest().catch((err) => {
393
- console.error("[MonitorClient] Scan request check failed:", err);
463
+ this.reportError("SETTINGS_POLL", "Scan request check failed", err);
394
464
  });
395
465
  }, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
396
466
  }
@@ -411,7 +481,7 @@ var MonitorClient = class {
411
481
  const duration = Date.now() - startTime;
412
482
  console.log(`[MonitorClient] Vulnerability scan completed in ${duration}ms`);
413
483
  } catch (err) {
414
- console.error("[MonitorClient] Vulnerability scan failed:", err instanceof Error ? err.message : String(err));
484
+ this.reportError("VULNERABILITY_SCAN", "Vulnerability scan failed", err);
415
485
  }
416
486
  }
417
487
  /**
@@ -438,7 +508,7 @@ var MonitorClient = class {
438
508
  }
439
509
  }
440
510
  } catch (err) {
441
- console.error("[MonitorClient] Failed to check for scan request:", err instanceof Error ? err.message : String(err));
511
+ this.reportError("SETTINGS_POLL", "Failed to check for scan request", err);
442
512
  }
443
513
  }
444
514
  enqueue(payload) {
@@ -525,7 +595,7 @@ var MonitorClient = class {
525
595
  );
526
596
  console.log("[MonitorClient] Technology sync completed successfully");
527
597
  } catch (err) {
528
- console.error("[MonitorClient] Technology sync failed:", err instanceof Error ? err.message : String(err));
598
+ this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
529
599
  }
530
600
  }
531
601
  async performDependencySync(signal) {
@@ -1029,7 +1099,7 @@ var MonitorClient = class {
1029
1099
  const result = await response.json();
1030
1100
  return result.data;
1031
1101
  } catch (err) {
1032
- console.error("[MonitorClient] Failed to audit dependencies:", err instanceof Error ? err.message : String(err));
1102
+ this.reportError("VULNERABILITY_SCAN", "Failed to audit dependencies", err);
1033
1103
  return null;
1034
1104
  }
1035
1105
  }
@@ -1059,7 +1129,7 @@ var MonitorClient = class {
1059
1129
  }
1060
1130
  return result;
1061
1131
  } catch (err) {
1062
- console.error("[MonitorClient] Multi-path audit failed:", err instanceof Error ? err.message : String(err));
1132
+ this.reportError("VULNERABILITY_SCAN", "Multi-path audit failed", err);
1063
1133
  return null;
1064
1134
  }
1065
1135
  }
@@ -1278,6 +1348,19 @@ var MonitorClient = class {
1278
1348
  "Content-Type": "application/json"
1279
1349
  };
1280
1350
  }
1351
+ if (endpoint.allowInsecureTls) {
1352
+ try {
1353
+ const undici = await import(
1354
+ /* webpackIgnore: true */
1355
+ "undici"
1356
+ );
1357
+ const Agent = undici.Agent;
1358
+ requestOptions.dispatcher = new Agent({
1359
+ connect: { rejectUnauthorized: false }
1360
+ });
1361
+ } catch {
1362
+ }
1363
+ }
1281
1364
  const response = await this.fetchWithTimeout(
1282
1365
  endpoint.url,
1283
1366
  requestOptions,
@@ -1321,12 +1404,12 @@ var MonitorClient = class {
1321
1404
  await this.fetchAndScheduleHealthChecks();
1322
1405
  this.healthCheckFetchTimer = setInterval(() => {
1323
1406
  this.fetchAndScheduleHealthChecks().catch((err) => {
1324
- console.error("[MonitorClient] Failed to fetch and schedule health checks:", err);
1407
+ this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch and schedule health checks", err);
1325
1408
  });
1326
1409
  }, this.healthCheckFetchIntervalMs);
1327
1410
  this.healthCheckFlushTimer = setInterval(() => {
1328
1411
  this.flushHealthResults().catch((err) => {
1329
- console.error("[MonitorClient] Failed to flush health results:", err);
1412
+ this.reportError("HEALTH_RESULTS_FLUSH", "Failed to flush health results", err);
1330
1413
  });
1331
1414
  }, CONFIG_LIMITS.HEALTH_CHECK_FLUSH_INTERVAL_MS);
1332
1415
  }
@@ -1347,7 +1430,7 @@ var MonitorClient = class {
1347
1430
  this.scheduleHealthCheck(endpoint);
1348
1431
  }
1349
1432
  } catch (err) {
1350
- console.error("[MonitorClient] Failed to fetch health endpoints:", err);
1433
+ this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch health endpoints from server", err);
1351
1434
  }
1352
1435
  }
1353
1436
  /**
@@ -1356,11 +1439,11 @@ var MonitorClient = class {
1356
1439
  scheduleHealthCheck(endpoint) {
1357
1440
  const intervalMs = Math.max(CONFIG_LIMITS.HEALTH_CHECK_MIN_INTERVAL_MS, endpoint.intervalMs);
1358
1441
  this.runHealthCheck(endpoint).catch((err) => {
1359
- console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1442
+ this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
1360
1443
  });
1361
1444
  const timer = setInterval(() => {
1362
1445
  this.runHealthCheck(endpoint).catch((err) => {
1363
- console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1446
+ this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
1364
1447
  });
1365
1448
  }, intervalMs);
1366
1449
  this.healthCheckTimers.set(endpoint.id, timer);
@@ -1386,7 +1469,7 @@ var MonitorClient = class {
1386
1469
  const response = await this.submitHealthResults(results);
1387
1470
  console.log(`[MonitorClient] Submitted ${response.processed}/${response.total} health check results`);
1388
1471
  } catch (err) {
1389
- console.error("[MonitorClient] Failed to flush health results:", err);
1472
+ this.reportError("HEALTH_RESULTS_FLUSH", "Failed to submit health check results to server", err);
1390
1473
  if (this.healthCheckResultsQueue.length + results.length <= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
1391
1474
  this.healthCheckResultsQueue.unshift(...results);
1392
1475
  }
@@ -1409,6 +1492,171 @@ var MonitorClient = class {
1409
1492
  this.healthCheckTimers.delete(endpointId);
1410
1493
  }
1411
1494
  }
1495
+ // ---- System Metrics (on-premise only) ----
1496
+ /**
1497
+ * Start system metrics collection based on server-configured settings.
1498
+ * Only runs if metricsEnabled is true in project settings.
1499
+ * Collects CPU, RAM, and disk usage using built-in Node.js modules.
1500
+ */
1501
+ async setupSystemMetricsCollection() {
1502
+ const settings = await this.fetchProjectSettings();
1503
+ if (!settings?.metricsEnabled) {
1504
+ this.metricsSettingsCheckTimer = setInterval(() => {
1505
+ this.fetchProjectSettings().then((latestSettings) => {
1506
+ if (latestSettings?.metricsEnabled) {
1507
+ clearInterval(this.metricsSettingsCheckTimer);
1508
+ this.metricsSettingsCheckTimer = null;
1509
+ this.startMetricsCollection(latestSettings);
1510
+ }
1511
+ }).catch(() => {
1512
+ });
1513
+ }, 3e5);
1514
+ return;
1515
+ }
1516
+ this.startMetricsCollection(settings);
1517
+ }
1518
+ startMetricsCollection(settings) {
1519
+ const intervalMs = Math.max(3e4, (settings.metricsIntervalSeconds ?? 3600) * 1e3);
1520
+ console.log(`[MonitorClient] System metrics collection enabled (every ${intervalMs / 1e3}s)`);
1521
+ const initialDiskPaths = settings.metricsDiskPaths ?? ["/"];
1522
+ this.collectAndSubmitMetrics(initialDiskPaths).catch((err) => {
1523
+ this.reportError("SYSTEM_METRICS", "Initial system metrics collection failed", err);
1524
+ });
1525
+ this.metricsCollectionTimer = setInterval(() => {
1526
+ this.fetchProjectSettings().then((latestSettings) => {
1527
+ if (!latestSettings?.metricsEnabled) return;
1528
+ const diskPaths = latestSettings.metricsDiskPaths ?? ["/"];
1529
+ return this.collectAndSubmitMetrics(diskPaths);
1530
+ }).catch((err) => {
1531
+ this.reportError("SYSTEM_METRICS", "Scheduled system metrics collection failed", err);
1532
+ });
1533
+ }, intervalMs);
1534
+ }
1535
+ stopMetricsCollection() {
1536
+ if (this.metricsCollectionTimer) {
1537
+ clearInterval(this.metricsCollectionTimer);
1538
+ this.metricsCollectionTimer = null;
1539
+ }
1540
+ if (this.metricsSettingsCheckTimer) {
1541
+ clearInterval(this.metricsSettingsCheckTimer);
1542
+ this.metricsSettingsCheckTimer = null;
1543
+ }
1544
+ }
1545
+ /**
1546
+ * Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
1547
+ * and submit to the monitoring server.
1548
+ *
1549
+ * Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
1550
+ * Zero dependencies — all built-in Node.js 18.15+
1551
+ */
1552
+ async collectAndSubmitMetrics(diskPaths) {
1553
+ if (typeof window !== "undefined" || typeof document !== "undefined") {
1554
+ return;
1555
+ }
1556
+ let os;
1557
+ let fs;
1558
+ try {
1559
+ const osModule = await import(
1560
+ /* webpackIgnore: true */
1561
+ "os"
1562
+ );
1563
+ const fsModule = await import(
1564
+ /* webpackIgnore: true */
1565
+ "fs/promises"
1566
+ );
1567
+ os = osModule;
1568
+ fs = fsModule;
1569
+ } catch {
1570
+ this.reportError("SYSTEM_METRICS", "System metrics require Node.js (not available in bundled/browser environments)");
1571
+ return;
1572
+ }
1573
+ try {
1574
+ const hostname = os.hostname();
1575
+ const cpuPercent = await this.measureCpuPercent(os);
1576
+ const memoryTotal = os.totalmem();
1577
+ const memoryFree = os.freemem();
1578
+ const memoryUsed = memoryTotal - memoryFree;
1579
+ const memoryPercent = memoryTotal > 0 ? memoryUsed / memoryTotal * 100 : 0;
1580
+ const disks = await this.collectDiskMetrics(diskPaths, fs);
1581
+ await this.submitSystemMetric({
1582
+ hostname,
1583
+ cpuPercent,
1584
+ memoryTotal,
1585
+ memoryUsed,
1586
+ memoryPercent,
1587
+ disks
1588
+ });
1589
+ } catch (err) {
1590
+ this.reportError("SYSTEM_METRICS", "Failed to collect system metrics", err);
1591
+ }
1592
+ }
1593
+ /**
1594
+ * Measure CPU utilization by sampling cpus() twice with a 500ms gap.
1595
+ * Returns a percentage (0–100).
1596
+ */
1597
+ async measureCpuPercent(os) {
1598
+ const sample1 = os.cpus();
1599
+ await new Promise((resolve) => setTimeout(resolve, 500));
1600
+ const sample2 = os.cpus();
1601
+ let totalIdle = 0;
1602
+ let totalTick = 0;
1603
+ for (let i = 0; i < sample2.length; i++) {
1604
+ const prev = sample1[i];
1605
+ const curr = sample2[i];
1606
+ const prevTotal = Object.values(prev.times).reduce((a, b) => a + b, 0);
1607
+ const currTotal = Object.values(curr.times).reduce((a, b) => a + b, 0);
1608
+ totalTick += currTotal - prevTotal;
1609
+ totalIdle += curr.times.idle - prev.times.idle;
1610
+ }
1611
+ const idlePercent = totalTick > 0 ? totalIdle / totalTick * 100 : 0;
1612
+ return Math.max(0, Math.min(100, 100 - idlePercent));
1613
+ }
1614
+ /**
1615
+ * Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
1616
+ */
1617
+ async collectDiskMetrics(paths, fs) {
1618
+ const results = [];
1619
+ for (const diskPath of paths) {
1620
+ try {
1621
+ const stat = await fs.statfs(diskPath);
1622
+ const total = stat.bsize * stat.blocks;
1623
+ const free = stat.bsize * stat.bavail;
1624
+ const used = total - free;
1625
+ const percent = total > 0 ? used / total * 100 : 0;
1626
+ results.push({
1627
+ path: diskPath,
1628
+ total: Math.round(total),
1629
+ used: Math.round(used),
1630
+ percent: Math.round(percent * 10) / 10
1631
+ // 1 decimal place
1632
+ });
1633
+ } catch {
1634
+ this.reportError("SYSTEM_METRICS", `Failed to get disk stats for path: ${diskPath}`);
1635
+ }
1636
+ }
1637
+ return results;
1638
+ }
1639
+ /**
1640
+ * Submit a system metric snapshot to the monitoring server.
1641
+ */
1642
+ async submitSystemMetric(metric) {
1643
+ try {
1644
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/system-metrics`, {
1645
+ method: "POST",
1646
+ headers: {
1647
+ "Content-Type": "application/json",
1648
+ "Authorization": `Bearer ${this.apiKey}`
1649
+ },
1650
+ body: JSON.stringify(metric)
1651
+ });
1652
+ if (!response.ok) {
1653
+ const errorText = await response.text().catch(() => "");
1654
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
1655
+ }
1656
+ } catch (err) {
1657
+ this.reportError("SYSTEM_METRICS", "Failed to submit system metric to server", err);
1658
+ }
1659
+ }
1412
1660
  };
1413
1661
  // Annotate the CommonJS export names for ESM import in node:
1414
1662
  0 && (module.exports = {
package/dist/index.mjs CHANGED
@@ -58,6 +58,14 @@ var MonitorClient = class {
58
58
  this.healthCheckTimers = /* @__PURE__ */ new Map();
59
59
  this.healthCheckResultsQueue = [];
60
60
  this.healthCheckFlushTimer = null;
61
+ // SDK error reporting queue (errors visible on admin system errors page)
62
+ this.sdkErrorQueue = [];
63
+ this.sdkErrorFlushTimer = null;
64
+ this.sdkErrorsInCurrentWindow = 0;
65
+ this.sdkErrorWindowResetTimer = null;
66
+ // System metrics collection (on-premise only, opt-in)
67
+ this.metricsCollectionTimer = null;
68
+ this.metricsSettingsCheckTimer = null;
61
69
  if (!config.apiKey || config.apiKey.trim().length === 0) {
62
70
  throw new Error("[MonitorClient] API key is required");
63
71
  }
@@ -117,24 +125,28 @@ var MonitorClient = class {
117
125
  this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
118
126
  this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
119
127
  this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
120
- this.healthCheckEnabled = config.healthCheckEnabled || false;
128
+ this.healthCheckEnabled = config.healthCheckEnabled ?? true;
121
129
  this.healthCheckFetchIntervalMs = config.healthCheckFetchIntervalMs || CONFIG_LIMITS.HEALTH_CHECK_FETCH_INTERVAL_MS;
122
130
  this.startFlushTimer();
123
131
  if (this.trackDependencies) {
124
132
  this.syncDependencies().catch((err) => {
125
- console.error("[MonitorClient] Failed to sync dependencies:", err instanceof Error ? err.message : String(err));
133
+ this.reportError("DEPENDENCY_SYNC", "Failed to sync dependencies on startup", err);
126
134
  });
127
135
  }
128
136
  if (this.autoAudit) {
129
137
  this.setupAutoAudit().catch((err) => {
130
- console.error("[MonitorClient] Failed to setup auto audit:", err instanceof Error ? err.message : String(err));
138
+ this.reportError("AUDIT_SETUP", "Failed to setup auto audit", err);
131
139
  });
132
140
  }
133
141
  if (this.healthCheckEnabled) {
134
142
  this.setupHealthCheckPolling().catch((err) => {
135
- console.error("[MonitorClient] Failed to setup health check polling:", err instanceof Error ? err.message : String(err));
143
+ this.reportError("HEALTH_POLLING_SETUP", "Failed to setup health check polling", err);
136
144
  });
137
145
  }
146
+ this.startSdkErrorFlushTimer();
147
+ this.setupSystemMetricsCollection().catch((err) => {
148
+ this.reportError("SYSTEM_METRICS_SETUP", "Failed to setup system metrics collection", err);
149
+ });
138
150
  }
139
151
  /**
140
152
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -284,8 +296,66 @@ var MonitorClient = class {
284
296
  this.stopFlushTimer();
285
297
  this.stopAuditIntervalTimer();
286
298
  this.stopHealthCheckTimers();
299
+ this.stopSdkErrorFlushTimer();
300
+ this.stopMetricsCollection();
287
301
  await this.flush();
288
302
  await this.flushHealthResults();
303
+ await this.flushSdkErrors();
304
+ }
305
+ /**
306
+ * Queue an SDK error to be reported to the server's system errors page.
307
+ * Fire-and-forget — never throws. If reporting itself fails, logs to console only.
308
+ * Throttled to max 20 errors per minute to prevent flooding.
309
+ */
310
+ reportError(category, message, err) {
311
+ if (this.sdkErrorsInCurrentWindow >= 20) return;
312
+ this.sdkErrorsInCurrentWindow++;
313
+ const errorMessage = err instanceof Error ? `${message}: ${err.message}` : message;
314
+ const stack = err instanceof Error ? err.stack : void 0;
315
+ this.sdkErrorQueue.push({ category, message: errorMessage, stack });
316
+ if (this.sdkErrorQueue.length >= 20) {
317
+ this.flushSdkErrors().catch(() => {
318
+ });
319
+ }
320
+ }
321
+ startSdkErrorFlushTimer() {
322
+ this.sdkErrorFlushTimer = setInterval(() => {
323
+ this.flushSdkErrors().catch(() => {
324
+ });
325
+ }, 3e4);
326
+ this.sdkErrorWindowResetTimer = setInterval(() => {
327
+ this.sdkErrorsInCurrentWindow = 0;
328
+ }, 6e4);
329
+ }
330
+ stopSdkErrorFlushTimer() {
331
+ if (this.sdkErrorFlushTimer) {
332
+ clearInterval(this.sdkErrorFlushTimer);
333
+ this.sdkErrorFlushTimer = null;
334
+ }
335
+ if (this.sdkErrorWindowResetTimer) {
336
+ clearInterval(this.sdkErrorWindowResetTimer);
337
+ this.sdkErrorWindowResetTimer = null;
338
+ }
339
+ }
340
+ async flushSdkErrors() {
341
+ if (this.sdkErrorQueue.length === 0) return;
342
+ const errors = [...this.sdkErrorQueue];
343
+ this.sdkErrorQueue = [];
344
+ try {
345
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/sdk-errors`, {
346
+ method: "POST",
347
+ headers: {
348
+ "Content-Type": "application/json",
349
+ "Authorization": `Bearer ${this.apiKey}`
350
+ },
351
+ body: JSON.stringify({ errors })
352
+ });
353
+ if (!response.ok) {
354
+ console.warn(`[MonitorClient] Failed to report SDK errors: HTTP ${response.status}`);
355
+ }
356
+ } catch (err) {
357
+ console.warn("[MonitorClient] Failed to flush SDK errors to server:", err instanceof Error ? err.message : String(err));
358
+ }
289
359
  }
290
360
  stopAuditIntervalTimer() {
291
361
  if (this.auditIntervalTimer) {
@@ -347,14 +417,14 @@ var MonitorClient = class {
347
417
  const intervalMs = intervalHours * 60 * 60 * 1e3;
348
418
  this.auditIntervalTimer = setInterval(() => {
349
419
  this.runScanAndTrackTime().catch((err) => {
350
- console.error("[MonitorClient] Auto audit scan failed:", err);
420
+ this.reportError("AUDIT_SCAN", "Auto audit scan failed", err);
351
421
  });
352
422
  }, intervalMs);
353
423
  }
354
424
  console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
355
425
  this.settingsPollingTimer = setInterval(() => {
356
426
  this.checkForScanRequest().catch((err) => {
357
- console.error("[MonitorClient] Scan request check failed:", err);
427
+ this.reportError("SETTINGS_POLL", "Scan request check failed", err);
358
428
  });
359
429
  }, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
360
430
  }
@@ -375,7 +445,7 @@ var MonitorClient = class {
375
445
  const duration = Date.now() - startTime;
376
446
  console.log(`[MonitorClient] Vulnerability scan completed in ${duration}ms`);
377
447
  } catch (err) {
378
- console.error("[MonitorClient] Vulnerability scan failed:", err instanceof Error ? err.message : String(err));
448
+ this.reportError("VULNERABILITY_SCAN", "Vulnerability scan failed", err);
379
449
  }
380
450
  }
381
451
  /**
@@ -402,7 +472,7 @@ var MonitorClient = class {
402
472
  }
403
473
  }
404
474
  } catch (err) {
405
- console.error("[MonitorClient] Failed to check for scan request:", err instanceof Error ? err.message : String(err));
475
+ this.reportError("SETTINGS_POLL", "Failed to check for scan request", err);
406
476
  }
407
477
  }
408
478
  enqueue(payload) {
@@ -489,7 +559,7 @@ var MonitorClient = class {
489
559
  );
490
560
  console.log("[MonitorClient] Technology sync completed successfully");
491
561
  } catch (err) {
492
- console.error("[MonitorClient] Technology sync failed:", err instanceof Error ? err.message : String(err));
562
+ this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
493
563
  }
494
564
  }
495
565
  async performDependencySync(signal) {
@@ -993,7 +1063,7 @@ var MonitorClient = class {
993
1063
  const result = await response.json();
994
1064
  return result.data;
995
1065
  } catch (err) {
996
- console.error("[MonitorClient] Failed to audit dependencies:", err instanceof Error ? err.message : String(err));
1066
+ this.reportError("VULNERABILITY_SCAN", "Failed to audit dependencies", err);
997
1067
  return null;
998
1068
  }
999
1069
  }
@@ -1023,7 +1093,7 @@ var MonitorClient = class {
1023
1093
  }
1024
1094
  return result;
1025
1095
  } catch (err) {
1026
- console.error("[MonitorClient] Multi-path audit failed:", err instanceof Error ? err.message : String(err));
1096
+ this.reportError("VULNERABILITY_SCAN", "Multi-path audit failed", err);
1027
1097
  return null;
1028
1098
  }
1029
1099
  }
@@ -1242,6 +1312,19 @@ var MonitorClient = class {
1242
1312
  "Content-Type": "application/json"
1243
1313
  };
1244
1314
  }
1315
+ if (endpoint.allowInsecureTls) {
1316
+ try {
1317
+ const undici = await import(
1318
+ /* webpackIgnore: true */
1319
+ "undici"
1320
+ );
1321
+ const Agent = undici.Agent;
1322
+ requestOptions.dispatcher = new Agent({
1323
+ connect: { rejectUnauthorized: false }
1324
+ });
1325
+ } catch {
1326
+ }
1327
+ }
1245
1328
  const response = await this.fetchWithTimeout(
1246
1329
  endpoint.url,
1247
1330
  requestOptions,
@@ -1285,12 +1368,12 @@ var MonitorClient = class {
1285
1368
  await this.fetchAndScheduleHealthChecks();
1286
1369
  this.healthCheckFetchTimer = setInterval(() => {
1287
1370
  this.fetchAndScheduleHealthChecks().catch((err) => {
1288
- console.error("[MonitorClient] Failed to fetch and schedule health checks:", err);
1371
+ this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch and schedule health checks", err);
1289
1372
  });
1290
1373
  }, this.healthCheckFetchIntervalMs);
1291
1374
  this.healthCheckFlushTimer = setInterval(() => {
1292
1375
  this.flushHealthResults().catch((err) => {
1293
- console.error("[MonitorClient] Failed to flush health results:", err);
1376
+ this.reportError("HEALTH_RESULTS_FLUSH", "Failed to flush health results", err);
1294
1377
  });
1295
1378
  }, CONFIG_LIMITS.HEALTH_CHECK_FLUSH_INTERVAL_MS);
1296
1379
  }
@@ -1311,7 +1394,7 @@ var MonitorClient = class {
1311
1394
  this.scheduleHealthCheck(endpoint);
1312
1395
  }
1313
1396
  } catch (err) {
1314
- console.error("[MonitorClient] Failed to fetch health endpoints:", err);
1397
+ this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch health endpoints from server", err);
1315
1398
  }
1316
1399
  }
1317
1400
  /**
@@ -1320,11 +1403,11 @@ var MonitorClient = class {
1320
1403
  scheduleHealthCheck(endpoint) {
1321
1404
  const intervalMs = Math.max(CONFIG_LIMITS.HEALTH_CHECK_MIN_INTERVAL_MS, endpoint.intervalMs);
1322
1405
  this.runHealthCheck(endpoint).catch((err) => {
1323
- console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1406
+ this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
1324
1407
  });
1325
1408
  const timer = setInterval(() => {
1326
1409
  this.runHealthCheck(endpoint).catch((err) => {
1327
- console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1410
+ this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
1328
1411
  });
1329
1412
  }, intervalMs);
1330
1413
  this.healthCheckTimers.set(endpoint.id, timer);
@@ -1350,7 +1433,7 @@ var MonitorClient = class {
1350
1433
  const response = await this.submitHealthResults(results);
1351
1434
  console.log(`[MonitorClient] Submitted ${response.processed}/${response.total} health check results`);
1352
1435
  } catch (err) {
1353
- console.error("[MonitorClient] Failed to flush health results:", err);
1436
+ this.reportError("HEALTH_RESULTS_FLUSH", "Failed to submit health check results to server", err);
1354
1437
  if (this.healthCheckResultsQueue.length + results.length <= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
1355
1438
  this.healthCheckResultsQueue.unshift(...results);
1356
1439
  }
@@ -1373,6 +1456,171 @@ var MonitorClient = class {
1373
1456
  this.healthCheckTimers.delete(endpointId);
1374
1457
  }
1375
1458
  }
1459
+ // ---- System Metrics (on-premise only) ----
1460
+ /**
1461
+ * Start system metrics collection based on server-configured settings.
1462
+ * Only runs if metricsEnabled is true in project settings.
1463
+ * Collects CPU, RAM, and disk usage using built-in Node.js modules.
1464
+ */
1465
+ async setupSystemMetricsCollection() {
1466
+ const settings = await this.fetchProjectSettings();
1467
+ if (!settings?.metricsEnabled) {
1468
+ this.metricsSettingsCheckTimer = setInterval(() => {
1469
+ this.fetchProjectSettings().then((latestSettings) => {
1470
+ if (latestSettings?.metricsEnabled) {
1471
+ clearInterval(this.metricsSettingsCheckTimer);
1472
+ this.metricsSettingsCheckTimer = null;
1473
+ this.startMetricsCollection(latestSettings);
1474
+ }
1475
+ }).catch(() => {
1476
+ });
1477
+ }, 3e5);
1478
+ return;
1479
+ }
1480
+ this.startMetricsCollection(settings);
1481
+ }
1482
+ startMetricsCollection(settings) {
1483
+ const intervalMs = Math.max(3e4, (settings.metricsIntervalSeconds ?? 3600) * 1e3);
1484
+ console.log(`[MonitorClient] System metrics collection enabled (every ${intervalMs / 1e3}s)`);
1485
+ const initialDiskPaths = settings.metricsDiskPaths ?? ["/"];
1486
+ this.collectAndSubmitMetrics(initialDiskPaths).catch((err) => {
1487
+ this.reportError("SYSTEM_METRICS", "Initial system metrics collection failed", err);
1488
+ });
1489
+ this.metricsCollectionTimer = setInterval(() => {
1490
+ this.fetchProjectSettings().then((latestSettings) => {
1491
+ if (!latestSettings?.metricsEnabled) return;
1492
+ const diskPaths = latestSettings.metricsDiskPaths ?? ["/"];
1493
+ return this.collectAndSubmitMetrics(diskPaths);
1494
+ }).catch((err) => {
1495
+ this.reportError("SYSTEM_METRICS", "Scheduled system metrics collection failed", err);
1496
+ });
1497
+ }, intervalMs);
1498
+ }
1499
+ stopMetricsCollection() {
1500
+ if (this.metricsCollectionTimer) {
1501
+ clearInterval(this.metricsCollectionTimer);
1502
+ this.metricsCollectionTimer = null;
1503
+ }
1504
+ if (this.metricsSettingsCheckTimer) {
1505
+ clearInterval(this.metricsSettingsCheckTimer);
1506
+ this.metricsSettingsCheckTimer = null;
1507
+ }
1508
+ }
1509
+ /**
1510
+ * Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
1511
+ * and submit to the monitoring server.
1512
+ *
1513
+ * Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
1514
+ * Zero dependencies — all built-in Node.js 18.15+
1515
+ */
1516
+ async collectAndSubmitMetrics(diskPaths) {
1517
+ if (typeof window !== "undefined" || typeof document !== "undefined") {
1518
+ return;
1519
+ }
1520
+ let os;
1521
+ let fs;
1522
+ try {
1523
+ const osModule = await import(
1524
+ /* webpackIgnore: true */
1525
+ "os"
1526
+ );
1527
+ const fsModule = await import(
1528
+ /* webpackIgnore: true */
1529
+ "fs/promises"
1530
+ );
1531
+ os = osModule;
1532
+ fs = fsModule;
1533
+ } catch {
1534
+ this.reportError("SYSTEM_METRICS", "System metrics require Node.js (not available in bundled/browser environments)");
1535
+ return;
1536
+ }
1537
+ try {
1538
+ const hostname = os.hostname();
1539
+ const cpuPercent = await this.measureCpuPercent(os);
1540
+ const memoryTotal = os.totalmem();
1541
+ const memoryFree = os.freemem();
1542
+ const memoryUsed = memoryTotal - memoryFree;
1543
+ const memoryPercent = memoryTotal > 0 ? memoryUsed / memoryTotal * 100 : 0;
1544
+ const disks = await this.collectDiskMetrics(diskPaths, fs);
1545
+ await this.submitSystemMetric({
1546
+ hostname,
1547
+ cpuPercent,
1548
+ memoryTotal,
1549
+ memoryUsed,
1550
+ memoryPercent,
1551
+ disks
1552
+ });
1553
+ } catch (err) {
1554
+ this.reportError("SYSTEM_METRICS", "Failed to collect system metrics", err);
1555
+ }
1556
+ }
1557
+ /**
1558
+ * Measure CPU utilization by sampling cpus() twice with a 500ms gap.
1559
+ * Returns a percentage (0–100).
1560
+ */
1561
+ async measureCpuPercent(os) {
1562
+ const sample1 = os.cpus();
1563
+ await new Promise((resolve) => setTimeout(resolve, 500));
1564
+ const sample2 = os.cpus();
1565
+ let totalIdle = 0;
1566
+ let totalTick = 0;
1567
+ for (let i = 0; i < sample2.length; i++) {
1568
+ const prev = sample1[i];
1569
+ const curr = sample2[i];
1570
+ const prevTotal = Object.values(prev.times).reduce((a, b) => a + b, 0);
1571
+ const currTotal = Object.values(curr.times).reduce((a, b) => a + b, 0);
1572
+ totalTick += currTotal - prevTotal;
1573
+ totalIdle += curr.times.idle - prev.times.idle;
1574
+ }
1575
+ const idlePercent = totalTick > 0 ? totalIdle / totalTick * 100 : 0;
1576
+ return Math.max(0, Math.min(100, 100 - idlePercent));
1577
+ }
1578
+ /**
1579
+ * Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
1580
+ */
1581
+ async collectDiskMetrics(paths, fs) {
1582
+ const results = [];
1583
+ for (const diskPath of paths) {
1584
+ try {
1585
+ const stat = await fs.statfs(diskPath);
1586
+ const total = stat.bsize * stat.blocks;
1587
+ const free = stat.bsize * stat.bavail;
1588
+ const used = total - free;
1589
+ const percent = total > 0 ? used / total * 100 : 0;
1590
+ results.push({
1591
+ path: diskPath,
1592
+ total: Math.round(total),
1593
+ used: Math.round(used),
1594
+ percent: Math.round(percent * 10) / 10
1595
+ // 1 decimal place
1596
+ });
1597
+ } catch {
1598
+ this.reportError("SYSTEM_METRICS", `Failed to get disk stats for path: ${diskPath}`);
1599
+ }
1600
+ }
1601
+ return results;
1602
+ }
1603
+ /**
1604
+ * Submit a system metric snapshot to the monitoring server.
1605
+ */
1606
+ async submitSystemMetric(metric) {
1607
+ try {
1608
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/system-metrics`, {
1609
+ method: "POST",
1610
+ headers: {
1611
+ "Content-Type": "application/json",
1612
+ "Authorization": `Bearer ${this.apiKey}`
1613
+ },
1614
+ body: JSON.stringify(metric)
1615
+ });
1616
+ if (!response.ok) {
1617
+ const errorText = await response.text().catch(() => "");
1618
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
1619
+ }
1620
+ } catch (err) {
1621
+ this.reportError("SYSTEM_METRICS", "Failed to submit system metric to server", err);
1622
+ }
1623
+ }
1376
1624
  };
1377
1625
  export {
1378
1626
  MonitorClient
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceon-oy/monitor-sdk",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "Client SDK for Ceon Monitor - Error tracking, health monitoring, security events, and vulnerability scanning",
5
5
  "author": "Ceon",
6
6
  "license": "MIT",