mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
- Format `css/tailwind.source.css` with Prettier. - Add allowlist entry for `VideoCard.js` media query pixel value in `scripts/check-design-tokens.mjs`. - Remove accidental artifact `js/ui/dm/jules_session_4520373547615093905.zip`. - Fix bug in `js/app.js` and `js/services/moderationActionController.js` where blocked users' videos were not correctly hidden in the UI immediately after blocking. - Fix bug in `tests/app/moderation-overrides.test.mjs` where `userBlocks` mocks were insufficient to reflect state changes.
369 lines
13 KiB
JavaScript
369 lines
13 KiB
JavaScript
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 LOG_FILES = ['test_output.log', 'server.log', 'python_server.log', 'npm_output.log'];
|
|
|
|
// Regex patterns for PII sanitization
|
|
const PII_PATTERNS = [
|
|
{ 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)
|
|
{ name: 'HEX_KEY', regex: /\b[a-fA-F0-9]{64}\b/g, replacement: '[HEX_KEY]' },
|
|
// Nostr Bech32 keys (nsec, npub, nprofile, etc.)
|
|
{ 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) {
|
|
if (!text) return text;
|
|
let sanitized = text;
|
|
for (const pattern of PII_PATTERNS) {
|
|
sanitized = sanitized.replace(pattern.regex, pattern.replacement);
|
|
}
|
|
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 = [];
|
|
const logFile = 'test_output.log';
|
|
if (!fs.existsSync(logFile)) return errors;
|
|
|
|
const content = fs.readFileSync(logFile, 'utf8');
|
|
const lines = content.split('\n');
|
|
let currentError = null;
|
|
let insideErrorBlock = false;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
|
|
// Start of a TAP failure
|
|
if (line.match(/^not ok \d+ - /)) {
|
|
if (currentError) {
|
|
errors.push(currentError);
|
|
}
|
|
const title = line.replace(/^not ok \d+ - /, '').trim();
|
|
currentError = {
|
|
source: 'unit-test',
|
|
title: sanitize(title),
|
|
details: [],
|
|
severity: 'High'
|
|
};
|
|
insideErrorBlock = true;
|
|
continue;
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currentError) {
|
|
errors.push(currentError);
|
|
}
|
|
|
|
// Post-process for stack/fingerprint
|
|
errors.forEach(e => {
|
|
const fullText = e.details.join('\n');
|
|
// 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 {
|
|
e.stack = fullText.trim() || e.title;
|
|
}
|
|
e.fingerprint = getFingerprint(e.stack);
|
|
e.owner = suggestOwner(e.stack);
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
function collectSmokeTests() {
|
|
const errors = [];
|
|
if (!fs.existsSync(ARTIFACTS_DIR)) return errors;
|
|
|
|
const files = fs.readdirSync(ARTIFACTS_DIR).filter(f => f.startsWith('smoke-report-') && f.endsWith('.json'));
|
|
|
|
for (const file of files) {
|
|
try {
|
|
const content = JSON.parse(fs.readFileSync(path.join(ARTIFACTS_DIR, file), 'utf8'));
|
|
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: [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.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
|
|
// -----------------------------------------------------------------------------
|
|
|
|
function aggregate(errors) {
|
|
const grouped = {};
|
|
|
|
for (const err of errors) {
|
|
const key = err.fingerprint;
|
|
if (!grouped[key]) {
|
|
grouped[key] = {
|
|
fingerprint: key,
|
|
title: err.title,
|
|
count: 0,
|
|
sources: new Set(),
|
|
severity: err.severity,
|
|
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) => {
|
|
// Sort by Severity (Critical > High > Medium) then Count (Desc)
|
|
const severityScore = { 'Critical': 3, 'High': 2, 'Medium': 1 };
|
|
const scoreA = severityScore[a.severity] || 0;
|
|
const scoreB = severityScore[b.severity] || 0;
|
|
|
|
if (scoreA !== scoreB) return scoreB - scoreA;
|
|
return b.count - a.count;
|
|
});
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Reporting
|
|
// -----------------------------------------------------------------------------
|
|
|
|
function generateReport(aggregates) {
|
|
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
const filename = `telemetry-${dateStr}.md`;
|
|
const filepath = path.join(REPORTS_DIR, filename);
|
|
|
|
if (!fs.existsSync(REPORTS_DIR)) {
|
|
fs.mkdirSync(REPORTS_DIR, { recursive: true });
|
|
}
|
|
|
|
let md = `# Telemetry Report - ${new Date().toISOString().slice(0,10)}\n\n`;
|
|
md += `**Total Unique Issues:** ${aggregates.length}\n`;
|
|
md += `**Generated:** ${new Date().toISOString()}\n\n`;
|
|
|
|
md += `## Top 10 Priority Issues\n\n`;
|
|
md += `| Priority | Count | Issue | Owner | Sources |\n`;
|
|
md += `| :--- | :---: | :--- | :--- | :--- |\n`;
|
|
|
|
const top10 = aggregates.slice(0, 10);
|
|
|
|
for (const item of top10) {
|
|
// 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`;
|
|
|
|
for (const item of top10) {
|
|
md += `### [${item.severity}] ${item.title}\n`;
|
|
md += `- **Occurrences:** ${item.count}\n`;
|
|
md += `- **Sources:** ${Array.from(item.sources).join(', ')}\n`;
|
|
md += `- **Suggested Owner:** ${item.owner}\n`;
|
|
md += `- **Fingerprint:** \`${item.fingerprint}\`\n`;
|
|
md += `\n**Stack Trace / Details:**\n`;
|
|
md += "```\n";
|
|
md += item.stack || "No stack trace available";
|
|
md += "\n```\n\n";
|
|
}
|
|
|
|
// Privacy note
|
|
md += `\n---\n*Privacy Notice: PII (IPs, emails, keys) has been sanitized from this report.*\n`;
|
|
|
|
fs.writeFileSync(filepath, md);
|
|
console.log(`Report generated: ${filepath}`);
|
|
return filepath;
|
|
}
|
|
|
|
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 => ({
|
|
...a,
|
|
sources: Array.from(a.sources)
|
|
}));
|
|
fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
|
|
console.log(`Artifact generated: ${filepath}`);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Main
|
|
// -----------------------------------------------------------------------------
|
|
|
|
async function main() {
|
|
console.log('Starting Telemetry Aggregation...');
|
|
|
|
// 1. Collect
|
|
const errors = [];
|
|
|
|
// 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(errors);
|
|
console.log(`Aggregated into ${aggregates.length} unique issues.`);
|
|
|
|
// 3. Report
|
|
generateJSONArtifact(aggregates);
|
|
generateReport(aggregates);
|
|
|
|
console.log('Telemetry Aggregation Complete.');
|
|
}
|
|
|
|
main();
|