npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ceon-oy/monitor-sdk

v1.4.2

Published

Client SDK for Ceon Monitor - Error tracking, health monitoring, security events, and vulnerability scanning

Readme

Ceon Monitor SDK

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.

Table of Contents

Installation

# From npm (recommended)
npm install @ceon-oy/monitor-sdk

# From GitHub
npm install github:ceon-oy/ceon-monitor-sdk

Or add to your package.json:

{
  "dependencies": {
    "@ceon-oy/monitor-sdk": "^1.0.0"
  }
}

Related

Quick Start

import { MonitorClient } from '@ceon-oy/monitor-sdk';

// Initialize the client
const monitor = new MonitorClient({
  apiKey: process.env.CEON_MONITOR_API_KEY!,
  endpoint: 'https://monitor.example.com',
  environment: process.env.NODE_ENV,
  trackDependencies: true,  // Auto-sync package.json
});

// Capture an error
try {
  await riskyOperation();
} catch (error) {
  await monitor.captureError(error as Error, {
    route: '/api/users',
    method: 'POST',
    statusCode: 500,
  });
}

// Capture a message
await monitor.captureMessage('User signed up', 'INFO', {
  metadata: { userId: '123', plan: 'premium' },
});

// Run vulnerability audit
await monitor.auditDependencies();

// Flush and close before shutdown
await monitor.close();

Configuration

interface MonitorClientConfig {
  apiKey: string;                    // Your project API key (required)
  endpoint: string;                  // Monitor service URL (required)
  environment?: string;              // Environment name (default: 'production')
  batchSize?: number;                // Errors to batch before sending (default: 10)
  flushIntervalMs?: number;          // Auto-flush interval in ms (default: 5000)
  trackDependencies?: boolean;       // Auto-sync package.json (default: false)
  dependencySources?: {              // Multiple package.json sources
    path: string;
    environment: string;
  }[];
  excludePatterns?: string[];        // Glob patterns to exclude (e.g., '@types/*')
  autoAudit?: boolean;               // Enable automatic vulnerability scanning (default: false)
  auditPaths?: {                     // Multiple directories for vulnerability scanning
    path: string;                    // Directory path (e.g., '.', '../client')
    environment: string;             // Environment label (e.g., 'server', 'client')
  }[];
  auditTimeoutMs?: number;           // Timeout for npm audit command (default: 60000, max: 300000)
  registryTimeoutMs?: number;        // Timeout for npm registry requests (default: 5000, max: 30000)
  healthCheckEnabled?: boolean;      // Enable SDK-based health check polling (default: false)
  healthCheckFetchIntervalMs?: number; // Interval to fetch health endpoints in ms (default: 60000)
}

Basic Configuration

const monitor = new MonitorClient({
  apiKey: process.env.CEON_MONITOR_API_KEY!,
  endpoint: 'https://monitor.example.com',
  environment: 'production',
});

Multi-Environment Dependency Tracking

For projects with separate server and client folders:

const monitor = new MonitorClient({
  apiKey: process.env.CEON_MONITOR_API_KEY!,
  endpoint: 'https://monitor.example.com',
  environment: 'server',
  trackDependencies: true,
  dependencySources: [
    { path: './package.json', environment: 'server' },
    { path: '../client/package.json', environment: 'client' },
  ],
  excludePatterns: ['@types/*'],  // Filter out TypeScript type definitions
});

Initialization Patterns by Runtime

The SDK initialization pattern depends on your application's runtime environment:

Long-Running Servers (Express, Fastify, plain Node.js)

Traditional Node.js servers run as persistent processes. Use explicit initialization and graceful shutdown:

// lib/monitor.ts
import { MonitorClient } from '@ceon-oy/monitor-sdk';

let monitor: MonitorClient | null = null;

export function initializeMonitor() {
  if (!monitor) {
    monitor = new MonitorClient({
      apiKey: process.env.CEON_MONITOR_API_KEY!,
      endpoint: process.env.CEON_MONITOR_ENDPOINT!,
      environment: process.env.NODE_ENV,
      trackDependencies: true,
      autoAudit: true,
    });
  }
  return monitor;
}

export function getMonitor() {
  if (!monitor) {
    throw new Error('Monitor not initialized. Call initializeMonitor() first.');
  }
  return monitor;
}

// index.ts
import { initializeMonitor, getMonitor } from './lib/monitor';

const monitor = initializeMonitor();

// Graceful shutdown - ensures queued errors are sent before exit
process.on('SIGTERM', async () => {
  console.log('Shutting down gracefully...');
  await getMonitor().close();
  process.exit(0);
});

process.on('SIGINT', async () => {
  console.log('Shutting down gracefully...');
  await getMonitor().close();
  process.exit(0);
});

Why? Long-running servers need:

  • Single instance to avoid memory leaks
  • Graceful shutdown to flush queued errors before exit
  • Process signal handlers for clean termination

Next.js / Serverless / Edge Functions

Serverless environments handle lifecycle automatically. Create the client directly:

// lib/monitor.ts
import { MonitorClient } from '@ceon-oy/monitor-sdk';

export const monitor = new MonitorClient({
  apiKey: process.env.CEON_MONITOR_API_KEY!,
  endpoint: process.env.CEON_MONITOR_ENDPOINT!,
  environment: process.env.NODE_ENV,
  batchSize: 1,  // Send immediately (no batching for short-lived functions)
});

Why no explicit shutdown?

  • Each request is isolated and short-lived
  • The platform handles process lifecycle
  • No persistent process to clean up
  • Set batchSize: 1 to send errors immediately (don't wait for batch)

For API routes that need guaranteed delivery:

// app/api/something/route.ts
import { monitor } from '@/lib/monitor';

export async function POST(request: Request) {
  try {
    // ... your logic
  } catch (error) {
    monitor.captureError(error as Error);
    await monitor.flush();  // Ensure error is sent before response
  }
}

Summary Table

| Environment | Initialize Pattern | Graceful Shutdown | batchSize | |-------------|-------------------|-------------------|-----------| | Express/Fastify/Node.js | initializeMonitor() + singleton | Yes (SIGTERM/SIGINT handlers) | Default (10) | | Next.js (App Router) | Direct instantiation | No | 1 (recommended) | | Next.js (API Routes) | Direct instantiation | No, but call flush() | 1 (recommended) | | Vercel/AWS Lambda | Direct instantiation | No, but call flush() | 1 (required) | | Docker/Kubernetes | initializeMonitor() + singleton | Yes (for graceful pod termination) | Default (10) |

Features

Error Capture

Capture an Error

try {
  // ... your code
} catch (error) {
  await monitor.captureError(error as Error, {
    severity: 'ERROR',        // DEBUG, INFO, WARNING, ERROR, CRITICAL
    route: '/api/users',      // API route or page path
    method: 'POST',           // HTTP method
    statusCode: 500,          // HTTP status code
    userAgent: req.headers['user-agent'],
    ip: req.ip,
    requestId: req.id,
    metadata: {               // Any additional data
      userId: '123',
      action: 'create_user',
    },
  });
}

Capture a Message

await monitor.captureMessage('Payment processed', 'INFO', {
  route: '/api/payments',
  metadata: { orderId: '456', amount: 99.99 },
});

Technology Tracking

Automatic Tracking

Enable trackDependencies: true to automatically sync your package.json dependencies:

const monitor = new MonitorClient({
  apiKey: process.env.CEON_MONITOR_API_KEY!,
  endpoint: 'https://monitor.example.com',
  trackDependencies: true,
});

Manual Reporting

await monitor.reportTechnologies([
  { name: 'node', version: '20.10.0', type: 'runtime' },
  { name: 'express', version: '4.18.2', type: 'framework' },
  { name: 'prisma', version: '5.0.0', type: 'database' },
]);

Vulnerability Auditing

Scan your dependencies for security vulnerabilities and report them to Ceon Monitor.

// Basic audit
const result = await monitor.auditDependencies();

if (result) {
  console.log(`Scan complete: ${result.processed} vulnerabilities found`);
  console.log(`Critical: ${result.summary.critical}, High: ${result.summary.high}`);
}

// Audit a specific project directory
await monitor.auditDependencies({
  projectPath: '/path/to/project',
  environment: 'production',
});

Supported Package Managers

The SDK automatically detects your package manager based on lock files:

| Package Manager | Detection | Audit Command | |-----------------|-----------|---------------| | npm | package-lock.json (default) | npm audit --json | | yarn | yarn.lock | yarn audit --json | | pnpm | pnpm-lock.yaml | npm audit --json (compatibility) |

No configuration needed - the SDK auto-detects and uses the appropriate audit command.

Automatic Auditing (Recommended)

Enable autoAudit: true to let the SDK automatically scan for vulnerabilities based on the interval configured in the Ceon Monitor dashboard:

const monitor = new MonitorClient({
  apiKey: process.env.CEON_MONITOR_API_KEY!,
  endpoint: 'https://monitor.example.com',
  autoAudit: true,  // Enable automatic vulnerability scanning
});

With autoAudit enabled:

  • SDK fetches the scan interval from the server on startup
  • Runs an initial vulnerability scan
  • Schedules recurring scans based on server configuration (e.g., every 24 hours)
  • Responds to on-demand "Run Scan" requests from the dashboard (polled every 5 minutes)

Configure the scan interval in the Ceon Monitor dashboard under the project's Vulnerabilities page.

Multi-Directory Vulnerability Auditing

For projects with separate server and client directories (or monorepos), use auditPaths to scan multiple directories:

const monitor = new MonitorClient({
  apiKey: process.env.CEON_MONITOR_API_KEY!,
  endpoint: 'https://monitor.example.com',
  autoAudit: true,
  auditPaths: [
    { path: '.', environment: 'server' },
    { path: '../client', environment: 'client' },
  ],
  // Also track dependencies from both
  trackDependencies: true,
  dependencySources: [
    { path: './package.json', environment: 'server' },
    { path: '../client/package.json', environment: 'client' },
  ],
});

With auditPaths configured:

  • Each directory is scanned independently using npm audit
  • Results are tagged with the environment label (e.g., 'server', 'client')
  • All vulnerabilities appear in a single project dashboard
  • Use the environment filter in the dashboard to view specific environments

You can also manually trigger a multi-directory audit:

const result = await monitor.auditMultiplePaths();
if (result) {
  console.log('Audit results by environment:');
  for (const env of result.results) {
    console.log(`  ${env.environment}: ${env.processed} vulnerabilities`);
  }
  console.log('Total:', result.totalSummary);
}

Manual Scheduled Auditing

If you prefer manual control over scheduling:

// Run on app startup
monitor.auditDependencies();

// Run daily via cron
import cron from 'node-cron';

cron.schedule('0 3 * * *', async () => {
  await monitor.auditDependencies();
});

// Or using setInterval
setInterval(async () => {
  await monitor.auditDependencies();
}, 24 * 60 * 60 * 1000); // Daily

SDK-Based Health Checks

The SDK can poll health endpoints that are inside your private network (VPN, internal services) and report the results to Ceon Monitor. This is useful for monitoring services that are not publicly accessible.

Enable SDK Polling

const monitor = new MonitorClient({
  apiKey: process.env.CEON_MONITOR_API_KEY!,
  endpoint: 'https://monitor.example.com',
  environment: 'production',

  // Enable SDK-based health check polling
  healthCheckEnabled: true,

  // Optional: customize fetch interval (default: 60000ms)
  healthCheckFetchIntervalMs: 60000,
});

With healthCheckEnabled: true, the SDK will:

  1. Fetch assigned health endpoints from the server every 60 seconds (configurable)
  2. Poll each endpoint locally at its configured interval
  3. Submit results back to the server in batches
  4. Automatically handle endpoint additions/removals

How It Works

  1. Configure in Dashboard: Create health endpoints in the Ceon Monitor dashboard and set their polling mode to "SDK"
  2. SDK Fetches Endpoints: The SDK periodically fetches the list of endpoints assigned for SDK polling
  3. Local Polling: The SDK polls each endpoint from your server (inside your VPN/private network)
  4. Report Results: Health check results are batched and submitted to Ceon Monitor

Use Cases

  • Internal APIs: Monitor APIs that are only accessible within your VPN
  • Database Health: Check database connection health from inside your network
  • Microservices: Monitor internal service-to-service communication
  • Private Infrastructure: Any endpoint that's not publicly accessible

Configuration Options

interface MonitorClientConfig {
  // ... other options

  /** Enable SDK-based health check polling (default: false) */
  healthCheckEnabled?: boolean;

  /** Interval to fetch health endpoints from server in ms (default: 60000) */
  healthCheckFetchIntervalMs?: number;
}

Manual Health Check Methods

You can also manually perform health checks:

// Fetch endpoints assigned for SDK polling
const endpoints = await monitor.fetchHealthEndpoints();

// Submit health check results
const results = [
  {
    endpointId: 'clxyz123',
    status: 'HEALTHY',
    statusCode: 200,
    responseTimeMs: 150,
  },
];
await monitor.submitHealthResults(results);

System Metrics

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.

How It Works

When the MonitorClient is created, it fetches your project settings from the server. If the project has Enable Metric Collection turned on, the SDK:

  1. Reads metricsIntervalSeconds from project settings (configured in the dashboard)
  2. Reads metricsDiskPaths from project settings (e.g. ["/", "/data"])
  3. Starts collecting metrics at that interval automatically
  4. POSTs metrics to the server — no manual calls needed

What Is Collected

| Metric | Method | Notes | |--------|--------|-------| | CPU usage % | Dual os.cpus() sample with 500 ms gap | Accurate active-cycle average | | RAM total | os.totalmem() | Bytes | | RAM used | os.totalmem() - os.freemem() | Bytes | | RAM % | usedMemory / totalMemory * 100 | | | Disk total / used / free per path | fs.statfs(path) | Node.js 18.15+ built-in, zero external deps |

Hostname is automatically set via os.hostname() and included with every report.

Enable in Dashboard

  1. Open the Ceon Monitor dashboard
  2. Go to Projects and click Edit on your project
  3. Enable the System Metrics widget
  4. Click Enable Metric Collection
  5. Set the collection interval (e.g. every 60 seconds)
  6. Add disk paths to monitor (e.g. / for root, /data for a data volume)
  7. Configure CPU / RAM / disk alert thresholds
  8. Save

The SDK will pick up the new settings on its next startup.

Notifications

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.

Node.js Version Requirement

fs.statfs() (used for disk monitoring) requires Node.js 18.15 or later. No external packages are needed.

Manual Metric Submission

You can also submit a metric reading manually:

await monitor.submitSystemMetric({
  hostname: os.hostname(),
  cpuPercent: 72.5,
  memoryTotal: 8 * 1024 ** 3,   // 8 GB in bytes
  memoryUsed:  5 * 1024 ** 3,   // 5 GB in bytes
  memoryPercent: 62.5,
  disks: [
    {
      path: '/',
      total: 100 * 1024 ** 3,   // 100 GB
      used:   60 * 1024 ** 3,   // 60 GB
      free:   40 * 1024 ** 3,   // 40 GB
      usedPercent: 60,
    },
  ],
});

Security Events

Report Security Event

await monitor.reportSecurityEvent({
  eventType: 'FAILED_LOGIN',
  category: 'AUTHENTICATION',
  severity: 'MEDIUM',
  ip: '192.168.1.1',
  identifier: '[email protected]',
  metadata: { attempts: 3 },
});

Check for Brute Force

const result = await monitor.checkBruteForce({
  ip: '192.168.1.1',
  identifier: '[email protected]',
  threshold: 5,
  windowMinutes: 15,
});

if (result.blocked) {
  // Block the request
  throw new Error('Too many attempts. Please try again later.');
}

Framework Examples

Express.js

import express from 'express';
import { MonitorClient } from '@ceon-oy/monitor-sdk';

const app = express();
const monitor = new MonitorClient({
  apiKey: process.env.CEON_MONITOR_API_KEY!,
  endpoint: process.env.CEON_MONITOR_ENDPOINT!,
  environment: process.env.NODE_ENV,
  trackDependencies: true,
});

// Your routes
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok' });
});

// Error handling middleware
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  monitor.captureError(err, {
    route: req.path,
    method: req.method,
    statusCode: 500,
    userAgent: req.get('user-agent'),
    ip: req.ip,
  });
  res.status(500).json({ error: 'Internal server error' });
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  await monitor.close();
  process.exit(0);
});

app.listen(3001, () => console.log('Server running on port 3001'));

Next.js

1. Create a monitor utility (lib/monitor.ts):

import { MonitorClient } from '@ceon-oy/monitor-sdk';

let monitor: MonitorClient | null = null;

export function getMonitor(): MonitorClient {
  if (!monitor) {
    monitor = new MonitorClient({
      apiKey: process.env.CEON_MONITOR_API_KEY!,
      endpoint: process.env.CEON_MONITOR_ENDPOINT || 'https://your-monitor-server.com',
      environment: process.env.NODE_ENV || 'development',
      trackDependencies: true,
    });
  }
  return monitor;
}

2. Use in API routes (app/api/example/route.ts):

import { NextRequest, NextResponse } from 'next/server';
import { getMonitor } from '@/lib/monitor';

export async function POST(request: NextRequest) {
  const monitor = getMonitor();

  try {
    const body = await request.json();
    // Your logic here
    return NextResponse.json({ success: true });
  } catch (error) {
    await monitor.captureError(error as Error, {
      route: '/api/example',
      method: 'POST',
      statusCode: 500,
    });
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
  }
}

3. Global error handling (app/error.tsx):

'use client';

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    fetch('/api/log-error', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        message: error.message,
        stack: error.stack,
        digest: error.digest,
      }),
    });
  }, [error]);

  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

4. Create error logging API route (app/api/log-error/route.ts):

import { NextRequest, NextResponse } from 'next/server';
import { getMonitor } from '@/lib/monitor';

export async function POST(request: NextRequest) {
  const monitor = getMonitor();
  const { message, stack, digest } = await request.json();

  await monitor.captureMessage(message, 'ERROR', {
    metadata: { stack, digest, source: 'client' },
  });

  return NextResponse.json({ logged: true });
}

React (Monolithic)

For a React project with Express backend in the same folder:

my-app/
├── package.json
├── src/
│   ├── client/          # React frontend
│   └── server/          # Express backend

Server (src/server/index.ts):

import express from 'express';
import path from 'path';
import { MonitorClient } from '@ceon-oy/monitor-sdk';

const app = express();
app.use(express.json());

const monitor = new MonitorClient({
  apiKey: process.env.CEON_MONITOR_API_KEY!,
  endpoint: process.env.CEON_MONITOR_ENDPOINT!,
  environment: process.env.NODE_ENV || 'development',
  trackDependencies: true,
});

// Client error logging endpoint
app.post('/api/log-client-error', async (req, res) => {
  const { message, stack, componentStack } = req.body;
  await monitor.captureMessage(message, 'ERROR', {
    metadata: { stack, componentStack, source: 'client' },
  });
  res.json({ logged: true });
});

// Global error handler
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  monitor.captureError(err, {
    route: req.path,
    method: req.method,
    statusCode: 500,
  });
  res.status(500).json({ error: 'Internal server error' });
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  await monitor.close();
  process.exit(0);
});

app.listen(3001, () => console.log('Server running on port 3001'));

React Error Boundary (src/client/ErrorBoundary.tsx):

import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  children: ReactNode;
}

interface State {
  hasError: boolean;
}

class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(): State {
    return { hasError: true };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    fetch('/api/log-client-error', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        message: error.message,
        stack: error.stack,
        componentStack: errorInfo.componentStack,
      }),
    });
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

React (Separate Server/Client)

For projects with separate server and client folders:

my-project/
├── server/
│   ├── package.json
│   └── src/index.ts
└── client/
    ├── package.json
    └── src/

Server (server/src/index.ts):

import express from 'express';
import cors from 'cors';
import { MonitorClient } from '@ceon-oy/monitor-sdk';

const app = express();
app.use(cors());
app.use(express.json());

// Multi-environment dependency tracking
const monitor = new MonitorClient({
  apiKey: process.env.CEON_MONITOR_API_KEY!,
  endpoint: process.env.CEON_MONITOR_ENDPOINT!,
  environment: process.env.NODE_ENV || 'development',
  trackDependencies: true,
  dependencySources: [
    { path: './package.json', environment: 'server' },
    { path: '../client/package.json', environment: 'client' },
  ],
  excludePatterns: ['@types/*'],
});

// Endpoint for client-side error logging
app.post('/api/log-error', async (req, res) => {
  const { message, stack, componentStack, url, userAgent } = req.body;

  await monitor.captureMessage(message, 'ERROR', {
    route: url,
    metadata: { stack, componentStack, userAgent, source: 'client' },
  });

  res.json({ logged: true });
});

// Global error handler
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  monitor.captureError(err, {
    route: req.path,
    method: req.method,
    statusCode: 500,
    userAgent: req.get('user-agent'),
    ip: req.ip,
  });
  res.status(500).json({ error: 'Internal server error' });
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  await monitor.close();
  process.exit(0);
});

app.listen(3001, () => console.log('Server running on port 3001'));

React Error Boundary (client/src/components/ErrorBoundary.tsx):

import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  children: ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    fetch(`${process.env.REACT_APP_API_URL}/api/log-error`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        message: error.message,
        stack: error.stack,
        componentStack: errorInfo.componentStack,
        url: window.location.href,
        userAgent: navigator.userAgent,
      }),
    }).catch(console.error);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h1>Something went wrong</h1>
          <button onClick={() => window.location.reload()}>Reload Page</button>
        </div>
      );
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

Global error handlers (client/src/index.tsx):

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import ErrorBoundary from './components/ErrorBoundary';

// Global error handlers
window.onerror = (message, source, lineno, colno, error) => {
  fetch(`${process.env.REACT_APP_API_URL}/api/log-error`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      message: String(message),
      stack: error?.stack,
      url: window.location.href,
      userAgent: navigator.userAgent,
    }),
  }).catch(console.error);
};

window.onunhandledrejection = (event) => {
  fetch(`${process.env.REACT_APP_API_URL}/api/log-error`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      message: event.reason?.message || 'Unhandled Promise Rejection',
      stack: event.reason?.stack,
      url: window.location.href,
      userAgent: navigator.userAgent,
    }),
  }).catch(console.error);
};

const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
  <React.StrictMode>
    <ErrorBoundary>
      <App />
    </ErrorBoundary>
  </React.StrictMode>
);

Real-World Example: Full-Stack Monitoring

This example shows how the Ceon Hours project monitors both server and client code from a single SDK installation on the server.

Project Structure:

ceon-projects/
├── ceon-hours-api/          # Express server (SDK installed here)
│   ├── package.json
│   └── lib/config/monitor-setup.js
└── ceon-hours-web-app/      # React client (no SDK needed)
    └── package.json

1. Monitor Setup (ceon-hours-api/lib/config/monitor-setup.js):

const { MonitorClient } = require("@ceon-oy/monitor-sdk");

let monitor = null;

function initializeMonitor() {
  if (process.env.CEON_MONITOR_API_KEY) {
    monitor = new MonitorClient({
      apiKey: process.env.CEON_MONITOR_API_KEY,
      endpoint: process.env.CEON_MONITOR_ENDPOINT || "https://ceonmonitor.com",
      environment: "server",
      trackDependencies: true,
      autoAudit: true,
      // Track dependencies from both server AND client
      dependencySources: [
        { path: "./package.json", environment: "server" },
        { path: "../ceon-hours-web-app/package.json", environment: "client" },
      ],
      // Audit vulnerabilities in both
      auditPaths: [
        { path: ".", environment: "server" },
        { path: "../ceon-hours-web-app", environment: "client" },
      ],
    });
    console.log("[CeonMonitor] SDK initialized");
  }
  return monitor;
}

function getMonitor() {
  return monitor;
}

module.exports = { initializeMonitor, getMonitor };

2. Initialize in Server Entry (ceon-hours-api/index.js):

const { initializeMonitor, getMonitor } = require("./lib/config/monitor-setup");

// Initialize at startup
const monitor = initializeMonitor();

// Export for middleware
module.exports.getMonitor = getMonitor;

// Graceful shutdown
process.on("SIGTERM", async () => {
  console.log("[CeonMonitor] Flushing pending errors...");
  if (monitor) await monitor.flush();
  process.exit(0);
});

3. Error Handler Middleware (ceon-hours-api/lib/middleware/errorHandler.js):

const { getMonitor } = require("../config/monitor-setup");

const errorHandler = (error, req, res, next) => {
  const monitor = getMonitor();
  if (monitor && error.status >= 400) {
    monitor.captureError(error, {
      route: req.path,
      method: req.method,
      statusCode: error.status || 500,
      userAgent: req.get("user-agent"),
      ip: req.ip,
      severity: error.status >= 500 ? "ERROR" : "WARNING",
    }).catch(console.error);
  }

  res.status(error.status || 500).json({ error: error.message });
};

module.exports = errorHandler;

4. Environment Variables (.env):

# Get API key from https://ceonmonitor.com dashboard
CEON_MONITOR_API_KEY=your-project-api-key

# Endpoint options:
# - Production: https://ceonmonitor.com
# - Local dev: http://localhost:4040
# - Docker dev: http://host.docker.internal:4040
CEON_MONITOR_ENDPOINT=https://ceonmonitor.com

Important: Relative Path Requirements

The dependencySources and auditPaths use relative paths from the server's working directory. For multi-project monitoring to work:

  1. Directory structure must match - The client project must be at the expected relative path (e.g., ../ceon-hours-web-app)
  2. Both projects must be present - If the client folder doesn't exist, only server dependencies will be tracked
  3. npm must be available - Vulnerability auditing requires npm to be installed

Troubleshooting: If client dependencies aren't being tracked on other machines:

  • Verify the relative path is correct for that machine's directory structure
  • Check that the client's package.json exists at the expected path
  • The SDK will log warnings if paths are invalid

API Reference

MonitorClient

constructor(config: MonitorClientConfig)

Creates a new monitor client instance.

captureError(error: Error, context?: ErrorContext): Promise<void>

Captures an error and queues it for sending.

captureMessage(message: string, severity?: Severity, context?: ErrorContext): Promise<void>

Captures a log message with optional severity level.

reportTechnologies(technologies: Technology[]): Promise<void>

Reports technology stack information.

reportSecurityEvent(event: SecurityEvent): Promise<void>

Reports a security event.

checkBruteForce(params: BruteForceParams): Promise<BruteForceResult>

Checks for brute force attacks.

auditDependencies(options?: AuditOptions): Promise<AuditResult | null>

Runs npm audit and sends results to the server.

auditMultiplePaths(): Promise<MultiAuditSummary | null>

Runs npm audit on all directories configured in auditPaths and returns a combined summary.

submitSystemMetric(metric: SystemMetric): Promise<void>

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.

flush(): Promise<void>

Immediately sends all queued errors.

close(): Promise<void>

Flushes remaining errors and stops the client.

Batching Behavior

The SDK batches errors to reduce network overhead:

  1. Errors are queued locally
  2. Queue is flushed when:
    • batchSize errors are queued (default: 10)
    • flushIntervalMs milliseconds pass (default: 5000ms)
    • flush() is called manually
    • close() is called

For serverless/edge environments where the process may terminate quickly:

  • Set batchSize: 1 to send immediately
  • Call await monitor.flush() at the end of each request

Building

npm install
npm run build    # Build for production
npm run dev      # Watch mode for development

Types

type Severity = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL';

interface ErrorContext {
  severity?: Severity;
  route?: string;
  method?: string;
  statusCode?: number;
  userAgent?: string;
  ip?: string;
  requestId?: string;
  metadata?: Record<string, unknown>;
}

interface Technology {
  name: string;
  version: string;
  type: 'runtime' | 'framework' | 'library' | 'database' | 'tool' | 'other';
  environment?: string;
}

interface SecurityEvent {
  eventType: string;
  category: 'AUTHENTICATION' | 'AUTHORIZATION' | 'RATE_LIMIT' | 'SUSPICIOUS_ACTIVITY' | 'DATA_ACCESS';
  severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
  ip?: string;
  identifier?: string;
  metadata?: Record<string, unknown>;
}

interface AuditPath {
  path: string;        // Directory path (e.g., '.', '../client')
  environment: string; // Environment label (e.g., 'server', 'client')
}

interface DiskMetric {
  path: string;        // Mount path (e.g. '/', '/data')
  total: number;       // Total bytes
  used: number;        // Used bytes
  free: number;        // Free bytes
  usedPercent: number; // 0–100
}

interface SystemMetric {
  hostname: string;
  cpuPercent: number;
  memoryTotal: number;
  memoryUsed: number;
  memoryPercent: number;
  disks: DiskMetric[];
}

interface MultiAuditSummary {
  results: Array<{
    environment: string;
    scanId: string;
    processed: number;
    resolved: number;
    summary: { critical: number; high: number; moderate: number; low: number; info: number };
  }>;
  totalSummary: { critical: number; high: number; moderate: number; low: number; info: number };
}

License

MIT