UNPKG

@himorishige/noren-devtools

Version:

Development and testing tools for Noren PII detection library

214 lines (213 loc) 6.76 kB
/** * P3-0: Metrics collection infrastructure for performance and accuracy monitoring * Web Standards only - no Node.js-specific APIs */ /** * In-memory metrics collector for testing and development */ export class InMemoryMetricsCollector { metrics = []; maxEntries; constructor(maxEntries = 10000) { this.maxEntries = maxEntries; } recordMetric(entry) { this.metrics.push({ ...entry, timestamp: entry.timestamp || Date.now(), }); // Prevent unbounded growth if (this.metrics.length > this.maxEntries) { this.metrics = this.metrics.slice(-this.maxEntries); } } recordPerformance(operation, metric, labels = {}) { this.recordMetric({ timestamp: Date.now(), name: 'noren.performance.duration_ms', value: metric.duration_ms, labels: { ...labels, operation }, metadata: { memory_delta_bytes: metric.memory_delta_bytes, cpu_time_ms: metric.cpu_time_ms, }, }); } recordAccuracy(operation, metric, labels = {}) { this.recordMetric({ timestamp: Date.now(), name: 'noren.accuracy.hits_detected', value: metric.hits_detected, labels: { ...labels, operation }, }); if (metric.false_positives !== undefined) { this.recordMetric({ timestamp: Date.now(), name: 'noren.accuracy.false_positives', value: metric.false_positives, labels: { ...labels, operation }, }); } if (metric.false_negatives !== undefined) { this.recordMetric({ timestamp: Date.now(), name: 'noren.accuracy.false_negatives', value: metric.false_negatives, labels: { ...labels, operation }, }); } } async flush() { // In-memory collector doesn't need to flush, but could implement // periodic cleanup or export functionality here return Promise.resolve(); } // Testing and debugging utilities getMetrics() { return [...this.metrics]; } getMetricsByName(name) { return this.metrics.filter((m) => m.name === name); } getMetricsByOperation(operation) { return this.metrics.filter((m) => m.labels?.operation === operation); } clear() { this.metrics = []; } getMetricsSummary() { const summary = {}; for (const metric of this.metrics) { if (!summary[metric.name]) { summary[metric.name] = { count: 0, total: 0, min: Infinity, max: -Infinity }; } const s = summary[metric.name]; s.count++; s.total += metric.value; s.min = Math.min(s.min, metric.value); s.max = Math.max(s.max, metric.value); } // Convert totals to averages const result = {}; for (const [name, data] of Object.entries(summary)) { result[name] = { count: data.count, avg: data.total / data.count, min: data.min, max: data.max, }; } return result; } } /** * No-op metrics collector for production when metrics are disabled */ export class NoOpMetricsCollector { recordMetric(_entry) { // No-op } recordPerformance(_operation, _metric, _labels) { // No-op } recordAccuracy(_operation, _metric, _labels) { // No-op } async flush() { // No-op return Promise.resolve(); } } /** * Global metrics collector instance */ let globalMetricsCollector = new NoOpMetricsCollector(); /** * Set the global metrics collector */ export function setMetricsCollector(collector) { globalMetricsCollector = collector; } /** * Get the current metrics collector */ export function getMetricsCollector() { return globalMetricsCollector; } /** * Type-safe helper to get memory usage from performance API */ function getMemoryUsage() { try { // Check if memory API is available (Chrome/Edge) const perfWithMemory = performance; const hasMemoryAPI = 'memory' in performance && typeof perfWithMemory.memory?.usedJSHeapSize === 'number'; return hasMemoryAPI ? perfWithMemory.memory?.usedJSHeapSize : undefined; } catch { // Gracefully handle any access errors return undefined; } } /** * Utility function to measure performance of an operation */ export async function measurePerformance(operation, fn, labels) { const startTime = performance.now(); const startMemory = getMemoryUsage(); try { const result = await fn(); const endTime = performance.now(); const endMemory = getMemoryUsage(); const metric = { duration_ms: endTime - startTime, memory_delta_bytes: endMemory && startMemory ? endMemory - startMemory : undefined, }; globalMetricsCollector.recordPerformance(operation, metric, labels); return result; } catch (error) { const endTime = performance.now(); const metric = { duration_ms: endTime - startTime, }; globalMetricsCollector.recordPerformance(operation, metric, { ...labels, error: 'true', error_type: error instanceof Error ? error.name : 'unknown', }); throw error; } } /** * Predefined metric definitions for Noren */ export const NOREN_METRICS = { // Performance metrics 'noren.performance.duration_ms': { name: 'noren.performance.duration_ms', description: 'Operation duration in milliseconds', aggregation: 'histogram', buckets: [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000], labels: ['operation', 'error', 'error_type'], }, // Accuracy metrics 'noren.accuracy.hits_detected': { name: 'noren.accuracy.hits_detected', description: 'Number of PII hits detected', aggregation: 'sum', labels: ['operation', 'pii_type', 'plugin'], }, 'noren.accuracy.false_positives': { name: 'noren.accuracy.false_positives', description: 'Number of false positive detections', aggregation: 'sum', labels: ['operation', 'pii_type', 'plugin'], }, 'noren.accuracy.false_negatives': { name: 'noren.accuracy.false_negatives', description: 'Number of false negative (missed) detections', aggregation: 'sum', labels: ['operation', 'pii_type', 'plugin'], }, };