mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-05 20:32:27 +00:00
Merge pull request #1458 from PR0M3TH3AN/telemetry-aggregation-privacy-12055566479602134867
Telemetry Aggregation with Privacy Preservation
This commit is contained in:
4 files changed
+162
-2641
No files matched your search
@@ -1,229 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Navigate up from scripts/agent/ to root
|
||||
const ROOT_DIR = path.resolve(__dirname, '../../');
|
||||
const ARTIFACTS_DIR = path.join(ROOT_DIR, 'artifacts');
|
||||
const REPORTS_DIR = path.join(ROOT_DIR, 'ai/reports');
|
||||
const AGGREGATE_FILE = path.join(ARTIFACTS_DIR, 'error-aggregates.json');
|
||||
|
||||
// Ensure reports dir exists
|
||||
if (!fs.existsSync(REPORTS_DIR)) {
|
||||
fs.mkdirSync(REPORTS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips PII from text.
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
function sanitize(text) {
|
||||
if (!text) return '';
|
||||
|
||||
// Emails
|
||||
text = text.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL]');
|
||||
|
||||
// IPv4 - simple matching
|
||||
text = text.replace(/\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/g, '[IP]');
|
||||
|
||||
// Nostr Bech32 Keys (nsec, npub, etc.)
|
||||
text = text.replace(/\b(nsec|npub|nprofile|nevent|naddr|nrelay|ncryptsec)1[a-z0-9]+\b/g, '[KEY]');
|
||||
|
||||
// Hex Keys (64 chars) - e.g. private keys or signatures
|
||||
text = text.replace(/\b[a-f0-9]{64}\b/g, '[HEX_KEY]');
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a fingerprint for the error based on stack trace.
|
||||
* @param {string} stack
|
||||
* @returns {string}
|
||||
*/
|
||||
function getFingerprint(stack) {
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(stack);
|
||||
return hash.digest('hex').substring(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggests an owner based on the stack trace content.
|
||||
* @param {string} stack
|
||||
* @returns {string}
|
||||
*/
|
||||
function suggestOwner(stack) {
|
||||
const lowerStack = stack.toLowerCase();
|
||||
|
||||
if (lowerStack.includes('js/nostr') || lowerStack.includes('nip')) return 'Protocol Team';
|
||||
if (lowerStack.includes('js/ui') || lowerStack.includes('components') || lowerStack.includes('css')) return 'Frontend Team';
|
||||
if (lowerStack.includes('auth') || lowerStack.includes('crypto')) return 'Security Team';
|
||||
if (lowerStack.includes('tests') || lowerStack.includes('spec')) return 'QA Team';
|
||||
if (lowerStack.includes('storage') || lowerStack.includes('db')) return 'Storage Team';
|
||||
if (lowerStack.includes('agent') || lowerStack.includes('scripts')) return 'DevOps/Agent Team';
|
||||
|
||||
return 'Unassigned';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses log content into a list of error objects.
|
||||
* @param {string} content
|
||||
* @param {string} filename
|
||||
* @returns {Array}
|
||||
*/
|
||||
function parseLogFile(content, filename) {
|
||||
const errors = [];
|
||||
const lines = content.split('\n');
|
||||
|
||||
let currentError = null;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Heuristics for error detection
|
||||
const isStackLine = /^\s+at /.test(line);
|
||||
// Detect "Error:", "Exception", or "FAIL" (test output)
|
||||
const isErrorStart = (/Error:|Exception|FAIL/.test(line)) && !isStackLine;
|
||||
|
||||
if (isErrorStart) {
|
||||
if (currentError) {
|
||||
errors.push(currentError);
|
||||
}
|
||||
currentError = {
|
||||
message: line.trim(),
|
||||
stack: [line.trim()],
|
||||
file: filename
|
||||
};
|
||||
} else if (isStackLine && currentError) {
|
||||
currentError.stack.push(line.trim());
|
||||
} else if (currentError && line.trim() !== '') {
|
||||
// If we see a non-empty line:
|
||||
// If it looks like a timestamped log line "[2023...]", it's probably a new log entry, so close the error.
|
||||
if (/^\[.*?\]/.test(line)) {
|
||||
errors.push(currentError);
|
||||
currentError = null;
|
||||
// If this line itself is an error start, we'll catch it in the next iteration
|
||||
// effectively, but since we are iterating line by line, we need to handle it.
|
||||
// Actually, if this line IS an error start, the `isErrorStart` check would have caught it if we structure the loop right.
|
||||
// But `isErrorStart` was checked at the top.
|
||||
// So if we are "in an error" and see a timestamp, we close the error.
|
||||
// But wait, what if the timestamp line IS the error line?
|
||||
// e.g. [2023...] ERROR: something
|
||||
// In that case `isErrorStart` would be true (if it contains Error:).
|
||||
// So this block handles lines that are NOT `isErrorStart` but ARE part of the previous error.
|
||||
// If it's a timestamp line and NOT an error start, it's just a regular log, so close the previous error.
|
||||
} else {
|
||||
// Just text, assume it's part of the error message/stack unless we already had stack frames and this stopped being one.
|
||||
const hasStackFrames = currentError.stack.some(l => l.startsWith('at '));
|
||||
if (hasStackFrames && !isStackLine) {
|
||||
// We had stack frames, and now we don't. Probably end of error.
|
||||
errors.push(currentError);
|
||||
currentError = null;
|
||||
} else {
|
||||
// Append to message/stack
|
||||
currentError.stack.push(line.trim());
|
||||
if (!hasStackFrames) {
|
||||
currentError.message += ' ' + line.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentError) {
|
||||
errors.push(currentError);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(ARTIFACTS_DIR)) {
|
||||
console.log(`Artifacts directory not found at ${ARTIFACTS_DIR}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const aggregates = {};
|
||||
const files = fs.readdirSync(ARTIFACTS_DIR);
|
||||
|
||||
console.log(`Scanning ${files.length} files in ${ARTIFACTS_DIR}...`);
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.json') && file !== 'error-aggregates.json') {
|
||||
// TODO: Handle JSON logs if schema is known. For now, skipping or treating as text if needed.
|
||||
// If it's the load report, we might want to skip or parse differently.
|
||||
continue;
|
||||
}
|
||||
if (file === 'error-aggregates.json') continue;
|
||||
|
||||
// Skip directories
|
||||
const filePath = path.join(ARTIFACTS_DIR, file);
|
||||
if (fs.statSync(filePath).isDirectory()) continue;
|
||||
|
||||
console.log(`Processing ${file}...`);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const errors = parseLogFile(content, file);
|
||||
|
||||
for (const err of errors) {
|
||||
const sanitizedStack = sanitize(err.stack.join('\n'));
|
||||
const sanitizedMsg = sanitize(err.message);
|
||||
const fingerprint = getFingerprint(sanitizedStack);
|
||||
|
||||
if (!aggregates[fingerprint]) {
|
||||
aggregates[fingerprint] = {
|
||||
fingerprint,
|
||||
message: sanitizedMsg,
|
||||
stack: sanitizedStack,
|
||||
count: 0,
|
||||
sources: new Set(),
|
||||
owner: suggestOwner(sanitizedStack)
|
||||
};
|
||||
}
|
||||
|
||||
aggregates[fingerprint].count++;
|
||||
aggregates[fingerprint].sources.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Set to Array for JSON
|
||||
const resultList = Object.values(aggregates).map(a => ({
|
||||
...a,
|
||||
sources: Array.from(a.sources)
|
||||
})).sort((a, b) => b.count - a.count);
|
||||
|
||||
console.log(`Found ${resultList.length} unique errors.`);
|
||||
|
||||
// Save JSON
|
||||
fs.writeFileSync(AGGREGATE_FILE, JSON.stringify(resultList, null, 2));
|
||||
console.log(`Saved aggregate data to ${AGGREGATE_FILE}`);
|
||||
|
||||
// Generate Markdown
|
||||
const dateStr = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
||||
const reportPath = path.join(REPORTS_DIR, `telemetry-${dateStr}.md`);
|
||||
|
||||
let mdContent = `# Telemetry Report ${dateStr}\n\n`;
|
||||
mdContent += `**Total Unique Errors:** ${resultList.length}\n`;
|
||||
mdContent += `**Total Occurrences:** ${resultList.reduce((sum, i) => sum + i.count, 0)}\n\n`;
|
||||
|
||||
mdContent += `## Top Errors\n\n`;
|
||||
|
||||
resultList.slice(0, 10).forEach((err, idx) => {
|
||||
mdContent += `### ${idx + 1}. ${err.message.substring(0, 100)}${err.message.length > 100 ? '...' : ''}\n\n`;
|
||||
mdContent += `- **Count:** ${err.count}\n`;
|
||||
mdContent += `- **Suggested Owner:** ${err.owner}\n`;
|
||||
mdContent += `- **Fingerprint:** \`${err.fingerprint}\`\n`;
|
||||
mdContent += `- **Sources:** ${err.sources.join(', ')}\n`;
|
||||
mdContent += `\n\`\`\`text\n${err.stack}\n\`\`\`\n\n`;
|
||||
});
|
||||
|
||||
fs.writeFileSync(reportPath, mdContent);
|
||||
console.log(`Report generated at ${reportPath}`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,286 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const ROOT_DIR = path.resolve(__dirname, '../../');
|
||||
|
||||
const ARTIFACTS_DIR = path.join(ROOT_DIR, 'artifacts');
|
||||
const REPORTS_DIR = path.join(ROOT_DIR, 'ai/reports');
|
||||
|
||||
const LOG_FILES = [
|
||||
'test_output.log',
|
||||
'npm_output.log',
|
||||
'server.log',
|
||||
'python_server.log'
|
||||
];
|
||||
|
||||
// Privacy Sanitization Patterns
|
||||
const PATTERNS = {
|
||||
ipv4: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
|
||||
ipv6: /\b([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}\b/g,
|
||||
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
||||
hexKey: /\b[0-9a-fA-F]{64}\b/g,
|
||||
bech32: /\b(npub|nsec|note|nevent|nprofile|naddr)1[0-9a-z]{50,}\b/g,
|
||||
// Simplify paths to remove user names or absolute paths
|
||||
absPath: /(?:\/[a-zA-Z0-9_\-.]+)+\/bitvid\//g, // Try to find root of repo if possible
|
||||
genericPath: /(\/[a-zA-Z0-9_\-.]+){3,}/g, // Catch long paths
|
||||
};
|
||||
|
||||
function sanitize(text) {
|
||||
if (!text) return text;
|
||||
let sanitized = text;
|
||||
|
||||
sanitized = sanitized.replace(PATTERNS.ipv4, '<IPV4>');
|
||||
// sanitized = sanitized.replace(PATTERNS.ipv6, '<IPV6>'); // IPv6 regex is tricky, leaving it out for now to avoid false positives on simple text
|
||||
sanitized = sanitized.replace(PATTERNS.email, '<EMAIL>');
|
||||
sanitized = sanitized.replace(PATTERNS.hexKey, '<HEX_KEY>');
|
||||
sanitized = sanitized.replace(PATTERNS.bech32, '<BECH32>');
|
||||
|
||||
// Custom path sanitization
|
||||
// Assuming the repo is mounted at /app in the sandbox, or we want to normalize it
|
||||
sanitized = sanitized.replace(/\/app\//g, '<REPO>/');
|
||||
|
||||
// Try to mask home directories
|
||||
sanitized = sanitized.replace(/\/home\/[a-zA-Z0-9_\-.]+\//g, '<HOME>/');
|
||||
|
||||
// Use defined patterns for other paths (e.g. if the repo is checked out elsewhere)
|
||||
sanitized = sanitized.replace(PATTERNS.absPath, '<REPO>/');
|
||||
|
||||
// Generic path sanitization (careful to avoid breaking URLs too much, though strict PII policy prefers over-sanitization)
|
||||
// We only apply this if it looks like a long absolute path (starts with /)
|
||||
sanitized = sanitized.replace(PATTERNS.genericPath, (match) => {
|
||||
// Avoid sanitizing if it's already sanitized
|
||||
if (match.includes('<REPO>') || match.includes('<HOME>')) return match;
|
||||
return '/.../';
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
function parseTestLogs(content) {
|
||||
const errors = [];
|
||||
const lines = content.split('\n');
|
||||
|
||||
let currentError = null;
|
||||
let captureStack = false;
|
||||
|
||||
for (const line of lines) {
|
||||
// Detect TAP failure
|
||||
if (line.match(/^not ok \d+/)) {
|
||||
if (currentError) {
|
||||
errors.push(currentError);
|
||||
}
|
||||
currentError = {
|
||||
message: line.trim(),
|
||||
stack: [],
|
||||
source: 'test_output'
|
||||
};
|
||||
captureStack = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Detect general Error
|
||||
if (line.includes('Error:') || line.includes('Exception:')) {
|
||||
// If we are already capturing a stack for a TAP failure, this is likely the detail
|
||||
if (currentError && captureStack) {
|
||||
currentError.message += ' | ' + line.trim();
|
||||
} else {
|
||||
if (currentError) {
|
||||
errors.push(currentError);
|
||||
}
|
||||
currentError = {
|
||||
message: line.trim(),
|
||||
stack: [line.trim()],
|
||||
source: 'unknown'
|
||||
};
|
||||
captureStack = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (captureStack) {
|
||||
// Stack trace lines usually start with " at " or are indented
|
||||
if (line.match(/^\s+at /) || (line.match(/^\s+/) && currentError)) {
|
||||
currentError.stack.push(line.trim());
|
||||
} else if (line.match(/^TAP version/) || line.match(/^→ Running/) || line.trim() === '') {
|
||||
// End of error block
|
||||
if (currentError) {
|
||||
errors.push(currentError);
|
||||
currentError = null;
|
||||
captureStack = false;
|
||||
}
|
||||
} else {
|
||||
// Continue capturing if it looks like part of the error report (e.g. TAP diagnostics)
|
||||
if (line.match(/^\s+/)) {
|
||||
currentError.stack.push(line.trim());
|
||||
} else {
|
||||
if (currentError) {
|
||||
errors.push(currentError);
|
||||
currentError = null;
|
||||
captureStack = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentError) {
|
||||
errors.push(currentError);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function processFiles() {
|
||||
const allErrors = [];
|
||||
|
||||
for (const file of LOG_FILES) {
|
||||
const filePath = path.join(ROOT_DIR, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.log(`Processing ${file}...`);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// We can add more specific parsers here
|
||||
if (file === 'test_output.log') {
|
||||
allErrors.push(...parseTestLogs(content));
|
||||
} else {
|
||||
// Generic parsing for other logs: just look for Error: blocks
|
||||
const lines = content.split('\n');
|
||||
let currentError = null;
|
||||
for (const line of lines) {
|
||||
if (line.includes('Error:')) {
|
||||
if (currentError) allErrors.push(currentError);
|
||||
currentError = {
|
||||
message: line.trim(),
|
||||
stack: [line.trim()],
|
||||
source: file
|
||||
};
|
||||
} else if (currentError) {
|
||||
if (line.match(/^\s+at /) || line.match(/^\s+/)) {
|
||||
currentError.stack.push(line.trim());
|
||||
} else {
|
||||
allErrors.push(currentError);
|
||||
currentError = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentError) allErrors.push(currentError);
|
||||
}
|
||||
} else {
|
||||
console.log(`Skipping ${file} (not found)`);
|
||||
}
|
||||
}
|
||||
|
||||
return allErrors;
|
||||
}
|
||||
|
||||
function aggregateErrors(errors) {
|
||||
const groups = {};
|
||||
|
||||
for (const error of errors) {
|
||||
// Sanitize before grouping
|
||||
const sanitizedMsg = sanitize(error.message);
|
||||
const sanitizedStack = error.stack.map(sanitize).join('\n');
|
||||
|
||||
// Create a fingerprint. We use the stack trace if available, otherwise the message.
|
||||
// Truncating stack to first few lines can help group similar errors with slightly different line numbers if code shifts,
|
||||
// but for now let's use the whole stack but maybe ignore line numbers?
|
||||
// Actually, exact stack match (sanitized) is a good start.
|
||||
|
||||
// To make it more robust, let's remove line numbers from the fingerprint?
|
||||
// e.g. (file.js:123:45) -> (file.js)
|
||||
// For now, let's stick to sanitized stack as fingerprint.
|
||||
|
||||
const fingerprint = sanitizedStack || sanitizedMsg;
|
||||
|
||||
if (!groups[fingerprint]) {
|
||||
groups[fingerprint] = {
|
||||
message: sanitizedMsg,
|
||||
stack: sanitizedStack,
|
||||
count: 0,
|
||||
sources: new Set(),
|
||||
originalSample: error
|
||||
};
|
||||
}
|
||||
|
||||
groups[fingerprint].count++;
|
||||
groups[fingerprint].sources.add(error.source);
|
||||
}
|
||||
|
||||
return Object.values(groups).sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
function generateReport(aggregatedErrors) {
|
||||
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
const reportFile = path.join(REPORTS_DIR, `telemetry-${date}.md`);
|
||||
const jsonFile = path.join(ARTIFACTS_DIR, 'error-aggregates.json');
|
||||
|
||||
// JSON Output
|
||||
if (!fs.existsSync(ARTIFACTS_DIR)) {
|
||||
fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Convert sets to arrays for JSON
|
||||
const jsonOutput = aggregatedErrors.map(e => ({
|
||||
...e,
|
||||
sources: Array.from(e.sources)
|
||||
}));
|
||||
|
||||
fs.writeFileSync(jsonFile, JSON.stringify(jsonOutput, null, 2));
|
||||
console.log(`Wrote JSON artifact to ${jsonFile}`);
|
||||
|
||||
// Markdown Output
|
||||
let md = `# Telemetry Report - ${new Date().toISOString().split('T')[0]}\n\n`;
|
||||
md += `**Total Unique Errors:** ${aggregatedErrors.length}\n\n`;
|
||||
md += `| Count | Message | Sources | Suggested Owner |\n`;
|
||||
md += `|-------|---------|---------|-----------------|\n`;
|
||||
|
||||
const top10 = aggregatedErrors.slice(0, 10);
|
||||
|
||||
for (const error of top10) {
|
||||
// Truncate message for table
|
||||
let shortMsg = error.message.replace(/\|/g, '\\|').substring(0, 80);
|
||||
if (error.message.length > 80) shortMsg += '...';
|
||||
|
||||
const sources = Array.from(error.sources).join(', ');
|
||||
|
||||
// Guess owner based on stack
|
||||
let owner = 'TBD';
|
||||
if (error.stack.includes('nostr')) owner = 'Nostr Team';
|
||||
if (error.stack.includes('torrent') || error.stack.includes('webtorrent')) owner = 'P2P Team';
|
||||
if (error.stack.includes('ui/') || error.stack.includes('components/')) owner = 'Frontend Team';
|
||||
if (error.stack.includes('test')) owner = 'QA';
|
||||
|
||||
md += `| ${error.count} | \`${shortMsg}\` | ${sources} | ${owner} |\n`;
|
||||
}
|
||||
|
||||
md += `\n## Details (Top 10)\n\n`;
|
||||
|
||||
for (const [index, error] of top10.entries()) {
|
||||
md += `### ${index + 1}. ${error.message}\n`;
|
||||
md += `- **Count:** ${error.count}\n`;
|
||||
md += `- **Sources:** ${Array.from(error.sources).join(', ')}\n`;
|
||||
md += `- **Stack Trace:**\n`;
|
||||
md += `\`\`\`\n${error.stack}\n\`\`\`\n\n`;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(REPORTS_DIR)) {
|
||||
fs.mkdirSync(REPORTS_DIR, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(reportFile, md);
|
||||
console.log(`Wrote Markdown report to ${reportFile}`);
|
||||
}
|
||||
|
||||
// Main execution
|
||||
try {
|
||||
const errors = processFiles();
|
||||
console.log(`Found ${errors.length} raw errors.`);
|
||||
const aggregated = aggregateErrors(errors);
|
||||
console.log(`Aggregated into ${aggregated.length} unique issues.`);
|
||||
generateReport(aggregated);
|
||||
} catch (err) {
|
||||
console.error('Fatal error in telemetry script:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,26 +1,24 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
// Configuration
|
||||
const ARTIFACTS_DIR = 'artifacts';
|
||||
const REPORTS_DIR = 'ai/reports';
|
||||
const TEST_LOG = 'test_output.log';
|
||||
const LOG_FILES = ['test_output.log', 'server.log', 'python_server.log', 'npm_output.log'];
|
||||
|
||||
// Regex patterns for PII sanitization
|
||||
const PII_PATTERNS = [
|
||||
{ name: 'IP', regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: '[IP]' },
|
||||
{ name: 'EMAIL', regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, replacement: '[EMAIL]' },
|
||||
{ name: 'IPV4', regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: '[IP]' },
|
||||
{ name: 'IPV6', regex: /\b([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}\b/g, replacement: '[IPV6]' },
|
||||
// Hex keys (64 chars) - common in Nostr (private/public keys, event IDs, signatures)
|
||||
// We want to mask potential keys but maybe keep event IDs if we are sure they are IDs?
|
||||
// For safety, we mask all 64-char hex strings that look like keys/signatures.
|
||||
// However, stack traces might contain IDs that are useful for debugging.
|
||||
// The prompt says "Strip IPs, keys, emails... Keep only stack traces and counts."
|
||||
// It's better to be safe than sorry with keys.
|
||||
// We will mask 64-char hex strings.
|
||||
{ name: 'HEX_KEY', regex: /\b[a-fA-F0-9]{64}\b/g, replacement: '[HEX_KEY]' },
|
||||
// Nostr Bech32 keys (nsec, npub, nprofile, etc.)
|
||||
{ name: 'NSEC', regex: /\bnsec1[a-z0-9]{50,}\b/g, replacement: '[NSEC_KEY]' },
|
||||
{ name: 'NPUB', regex: /\bnpub1[a-z0-9]{50,}\b/g, replacement: '[NPUB_KEY]' },
|
||||
{ name: 'BECH32', regex: /\b(npub|nsec|note|nevent|nprofile|naddr|nrelay|ncryptsec)1[a-z0-9]{50,}\b/g, replacement: '[BECH32_KEY]' },
|
||||
// Generic path sanitization to hide user names
|
||||
{ name: 'HOME_PATH', regex: /\/home\/[a-zA-Z0-9_\-.]+\//g, replacement: '$HOME/' },
|
||||
{ name: 'REPO_PATH', regex: /\/app\//g, replacement: '$REPO/' } // Assuming sandbox mount point or common container path
|
||||
];
|
||||
|
||||
function sanitize(text) {
|
||||
@@ -32,30 +30,46 @@ function sanitize(text) {
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
function getFingerprint(stack) {
|
||||
if (!stack) return 'unknown';
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(stack);
|
||||
return hash.digest('hex').substring(0, 8);
|
||||
}
|
||||
|
||||
function suggestOwner(stack) {
|
||||
if (!stack) return 'Unassigned';
|
||||
const lowerStack = stack.toLowerCase();
|
||||
|
||||
if (lowerStack.includes('js/nostr') || lowerStack.includes('nip')) return 'Protocol Team';
|
||||
if (lowerStack.includes('js/ui') || lowerStack.includes('components') || lowerStack.includes('css')) return 'Frontend Team';
|
||||
if (lowerStack.includes('auth') || lowerStack.includes('crypto')) return 'Security Team';
|
||||
if (lowerStack.includes('tests') || lowerStack.includes('spec') || lowerStack.includes('playwright')) return 'QA Team';
|
||||
if (lowerStack.includes('storage') || lowerStack.includes('db') || lowerStack.includes('cache')) return 'Storage Team';
|
||||
if (lowerStack.includes('agent') || lowerStack.includes('scripts')) return 'DevOps/Agent Team';
|
||||
if (lowerStack.includes('webtorrent') || lowerStack.includes('torrent')) return 'P2P Team';
|
||||
|
||||
return 'Unassigned';
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Collectors
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function collectUnitTests() {
|
||||
const errors = [];
|
||||
if (!fs.existsSync(TEST_LOG)) {
|
||||
console.warn(`Unit test log not found at ${TEST_LOG}`);
|
||||
return errors;
|
||||
}
|
||||
const logFile = 'test_output.log';
|
||||
if (!fs.existsSync(logFile)) return errors;
|
||||
|
||||
const content = fs.readFileSync(TEST_LOG, 'utf8');
|
||||
const content = fs.readFileSync(logFile, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
let currentError = null;
|
||||
let insideErrorBlock = false;
|
||||
|
||||
// TAP Parsing Logic (simplified)
|
||||
// We look for "not ok <number> - <title>"
|
||||
// Then capture indentation blocks as details/stack
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Start of a failure
|
||||
// Start of a TAP failure
|
||||
if (line.match(/^not ok \d+ - /)) {
|
||||
if (currentError) {
|
||||
errors.push(currentError);
|
||||
@@ -71,33 +85,20 @@ function collectUnitTests() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// End of failure block (next test or summary)
|
||||
if (line.match(/^(ok \d+|# tests \d+|1\.\.\d+)/) || (insideErrorBlock && !line.startsWith(' '))) {
|
||||
if (currentError && insideErrorBlock) {
|
||||
// The indentation check is a bit flaky with TAP,
|
||||
// usually YAML blocks are indented.
|
||||
// But "ok" or "#" definitely ends it.
|
||||
|
||||
// If line doesn't start with space and it's not a TAP directive,
|
||||
// it might be console output which we might want to capture?
|
||||
// For now, let's assume TAP structure:
|
||||
// not ok 1 - test
|
||||
// ---
|
||||
// error: ...
|
||||
// ...
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
if (currentError && insideErrorBlock) {
|
||||
// Check if we exited the indented block
|
||||
if (line.trim() !== '' && !line.startsWith(' ') && !line.startsWith('\t')) {
|
||||
insideErrorBlock = false;
|
||||
errors.push(currentError);
|
||||
currentError = null;
|
||||
} else {
|
||||
// End of failure block detection (heuristic)
|
||||
// TAP ok line, or start of a new test section, or unexpected unindented line that looks like a header
|
||||
if (insideErrorBlock) {
|
||||
if (line.match(/^(ok \d+|# tests \d+|1\.\.\d+|TAP version)/)) {
|
||||
insideErrorBlock = false;
|
||||
errors.push(currentError);
|
||||
currentError = null;
|
||||
} else {
|
||||
// Capture detail lines
|
||||
// If line is empty or indented, it's likely part of the error
|
||||
// If it's unindented but not a TAP command, it might still be part of it (console logs)
|
||||
// We'll be greedy here.
|
||||
currentError.details.push(sanitize(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,22 +106,66 @@ function collectUnitTests() {
|
||||
errors.push(currentError);
|
||||
}
|
||||
|
||||
// Post-process to extract stack trace for fingerprinting
|
||||
// Post-process for stack/fingerprint
|
||||
errors.forEach(e => {
|
||||
const fullText = e.details.join('\n');
|
||||
// Simple fingerprint: file path + error message
|
||||
// Try to extract "stack: |-" block
|
||||
const stackMatch = fullText.match(/stack: \|-([\s\S]+?)(?:\n\s*\w+:|$)/);
|
||||
// Try to extract stack trace
|
||||
const stackMatch = fullText.match(/stack: \|-([\s\S]+?)(?:\n\s*\w+:|$)/) || fullText.match(/Error:([\s\S]+)/);
|
||||
if (stackMatch) {
|
||||
e.stack = stackMatch[1].trim();
|
||||
} else {
|
||||
// Fallback to title
|
||||
e.stack = e.title;
|
||||
e.stack = fullText.trim() || e.title;
|
||||
}
|
||||
e.fingerprint = getFingerprint(e.stack);
|
||||
e.owner = suggestOwner(e.stack);
|
||||
});
|
||||
|
||||
// Fingerprint: remove line numbers from stack
|
||||
// Also prepend title to ensure different tests with similar stacks (common in runners) are distinct
|
||||
e.fingerprint = e.title + '::' + e.stack.replace(/:\d+:\d+/g, ':x:x').replace(/\d+/g, 'N');
|
||||
return errors;
|
||||
}
|
||||
|
||||
function collectGenericLogs(filepath, sourceName) {
|
||||
const errors = [];
|
||||
if (!fs.existsSync(filepath)) return errors;
|
||||
|
||||
const content = fs.readFileSync(filepath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
let currentError = null;
|
||||
|
||||
for (const line of lines) {
|
||||
// Simple heuristic for generic logs: look for "Error:" or "Exception:"
|
||||
// and capture following lines that look like stack traces (start with 'at ' or are indented)
|
||||
|
||||
const isErrorStart = /Error:|Exception:|FAIL|CRITICAL/.test(line);
|
||||
const isStackLine = /^\s+at /.test(line) || /^\s+/.test(line); // Indented lines often follow errors
|
||||
|
||||
if (isErrorStart) {
|
||||
if (currentError) {
|
||||
errors.push(currentError);
|
||||
}
|
||||
currentError = {
|
||||
source: sourceName,
|
||||
title: sanitize(line.trim()),
|
||||
details: [sanitize(line.trim())],
|
||||
severity: 'High'
|
||||
};
|
||||
} else if (currentError && isStackLine) {
|
||||
currentError.details.push(sanitize(line.trim()));
|
||||
} else if (currentError) {
|
||||
// End of error block
|
||||
errors.push(currentError);
|
||||
currentError = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentError) {
|
||||
errors.push(currentError);
|
||||
}
|
||||
|
||||
// Post-process
|
||||
errors.forEach(e => {
|
||||
e.stack = e.details.join('\n');
|
||||
e.fingerprint = getFingerprint(e.stack);
|
||||
e.owner = suggestOwner(e.stack);
|
||||
});
|
||||
|
||||
return errors;
|
||||
@@ -138,23 +183,38 @@ function collectSmokeTests() {
|
||||
if (content.stats && content.stats.failures > 0) {
|
||||
const failedSteps = content.stats.details.filter(d => !d.success);
|
||||
for (const step of failedSteps) {
|
||||
const sanitizedError = sanitize(step.error);
|
||||
errors.push({
|
||||
source: 'smoke-test',
|
||||
title: sanitize(`Smoke Test Failed: ${step.name}`),
|
||||
details: [sanitize(step.error)],
|
||||
stack: sanitize(step.error),
|
||||
fingerprint: sanitize(step.name), // Group by step name
|
||||
severity: 'Critical'
|
||||
details: [sanitizedError],
|
||||
stack: sanitizedError,
|
||||
fingerprint: getFingerprint(sanitizedError + step.name),
|
||||
severity: 'Critical',
|
||||
owner: suggestOwner(sanitizedError) || 'QA Team'
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to parse smoke report ${file}:`, err);
|
||||
console.warn(`Failed to parse smoke report ${file}:`, err.message);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function collectAgentLogs() {
|
||||
const errors = [];
|
||||
if (!fs.existsSync(ARTIFACTS_DIR)) return errors;
|
||||
|
||||
// Look for any .log files in artifacts that aren't the main ones we already checked (if any)
|
||||
const files = fs.readdirSync(ARTIFACTS_DIR).filter(f => f.endsWith('.log'));
|
||||
|
||||
for (const file of files) {
|
||||
errors.push(...collectGenericLogs(path.join(ARTIFACTS_DIR, file), `agent-log:${file}`));
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Aggregation
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -163,7 +223,7 @@ function aggregate(errors) {
|
||||
const grouped = {};
|
||||
|
||||
for (const err of errors) {
|
||||
const key = err.fingerprint || err.title;
|
||||
const key = err.fingerprint;
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = {
|
||||
fingerprint: key,
|
||||
@@ -171,13 +231,16 @@ function aggregate(errors) {
|
||||
count: 0,
|
||||
sources: new Set(),
|
||||
severity: err.severity,
|
||||
exampleStack: err.stack
|
||||
stack: err.stack,
|
||||
owner: err.owner
|
||||
};
|
||||
}
|
||||
grouped[key].count++;
|
||||
grouped[key].sources.add(err.source);
|
||||
// Escalating severity if we see critical
|
||||
if (err.severity === 'Critical') grouped[key].severity = 'Critical';
|
||||
|
||||
// Update title if new one is longer/better (optional, keeping first for now)
|
||||
}
|
||||
|
||||
return Object.values(grouped).sort((a, b) => {
|
||||
@@ -209,14 +272,17 @@ function generateReport(aggregates) {
|
||||
md += `**Generated:** ${new Date().toISOString()}\n\n`;
|
||||
|
||||
md += `## Top 10 Priority Issues\n\n`;
|
||||
md += `| Priority | Count | Issue | Sources |\n`;
|
||||
md += `| :--- | :---: | :--- | :--- |\n`;
|
||||
md += `| Priority | Count | Issue | Owner | Sources |\n`;
|
||||
md += `| :--- | :---: | :--- | :--- | :--- |\n`;
|
||||
|
||||
const top10 = aggregates.slice(0, 10);
|
||||
|
||||
for (const item of top10) {
|
||||
const titleShort = item.title.length > 80 ? item.title.substring(0, 77) + '...' : item.title;
|
||||
md += `| **${item.severity}** | ${item.count} | ${titleShort} | ${Array.from(item.sources).join(', ')} |\n`;
|
||||
// Truncate title
|
||||
const titleShort = item.title.length > 60 ? item.title.substring(0, 57) + '...' : item.title;
|
||||
// Escape pipes for Markdown table
|
||||
const titleSafe = titleShort.replace(/\|/g, '\\|');
|
||||
md += `| **${item.severity}** | ${item.count} | ${titleSafe} | ${item.owner} | ${Array.from(item.sources).join(', ')} |\n`;
|
||||
}
|
||||
|
||||
md += `\n## Detailed Breakdown\n\n`;
|
||||
@@ -225,10 +291,11 @@ function generateReport(aggregates) {
|
||||
md += `### [${item.severity}] ${item.title}\n`;
|
||||
md += `- **Occurrences:** ${item.count}\n`;
|
||||
md += `- **Sources:** ${Array.from(item.sources).join(', ')}\n`;
|
||||
md += `- **Suggested Owner:** TBD (Automated Triage)\n`;
|
||||
md += `- **Suggested Owner:** ${item.owner}\n`;
|
||||
md += `- **Fingerprint:** \`${item.fingerprint}\`\n`;
|
||||
md += `\n**Stack Trace / Details:**\n`;
|
||||
md += "```\n";
|
||||
md += item.exampleStack || "No stack trace available";
|
||||
md += item.stack || "No stack trace available";
|
||||
md += "\n```\n\n";
|
||||
}
|
||||
|
||||
@@ -241,6 +308,9 @@ function generateReport(aggregates) {
|
||||
}
|
||||
|
||||
function generateJSONArtifact(aggregates) {
|
||||
if (!fs.existsSync(ARTIFACTS_DIR)) {
|
||||
fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
|
||||
}
|
||||
const filepath = path.join(ARTIFACTS_DIR, 'error-aggregates.json');
|
||||
// Convert Set to Array for JSON
|
||||
const data = aggregates.map(a => ({
|
||||
@@ -259,15 +329,33 @@ async function main() {
|
||||
console.log('Starting Telemetry Aggregation...');
|
||||
|
||||
// 1. Collect
|
||||
const unitTestErrors = collectUnitTests();
|
||||
const smokeTestErrors = collectSmokeTests();
|
||||
console.log(`Collected ${unitTestErrors.length} unit test errors.`);
|
||||
console.log(`Collected ${smokeTestErrors.length} smoke test errors.`);
|
||||
const errors = [];
|
||||
|
||||
const allErrors = [...unitTestErrors, ...smokeTestErrors];
|
||||
// Unit Tests
|
||||
const unitTestErrors = collectUnitTests();
|
||||
console.log(`Collected ${unitTestErrors.length} unit test errors.`);
|
||||
errors.push(...unitTestErrors);
|
||||
|
||||
// Server Logs
|
||||
for (const logFile of LOG_FILES) {
|
||||
if (logFile === 'test_output.log') continue; // Handled by collectUnitTests
|
||||
const genericErrors = collectGenericLogs(logFile, logFile);
|
||||
console.log(`Collected ${genericErrors.length} errors from ${logFile}.`);
|
||||
errors.push(...genericErrors);
|
||||
}
|
||||
|
||||
// Smoke Tests
|
||||
const smokeTestErrors = collectSmokeTests();
|
||||
console.log(`Collected ${smokeTestErrors.length} smoke test errors.`);
|
||||
errors.push(...smokeTestErrors);
|
||||
|
||||
// Agent Logs
|
||||
const agentErrors = collectAgentLogs();
|
||||
console.log(`Collected ${agentErrors.length} agent log errors.`);
|
||||
errors.push(...agentErrors);
|
||||
|
||||
// 2. Aggregate
|
||||
const aggregates = aggregate(allErrors);
|
||||
const aggregates = aggregate(errors);
|
||||
console.log(`Aggregated into ${aggregates.length} unique issues.`);
|
||||
|
||||
// 3. Report
|
||||
|
||||
-2052
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user