@himorishige/noren-devtools
Version:
Development and testing tools for Noren PII detection library
214 lines (213 loc) • 6.76 kB
JavaScript
/**
* 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'],
},
};