Testing & Debugging
Comprehensive testing and debugging guide for Errors and Echoes integration.
Testing Your Integration
Basic Integration Test
Verify your module is properly registered:
// Test module registration
Hooks.once('ready', () => {
// Check if API is available
if (!window.ErrorsAndEchoesAPI) {
console.warn('Errors and Echoes API not available');
return;
}
// Register your module
window.ErrorsAndEchoesAPI.register({
moduleId: 'your-module-id',
contextProvider: () => ({ test: true })
});
// Verify registration
setTimeout(() => {
const isRegistered = ModuleRegistry.isRegistered('your-module-id');
console.log('Module registered:', isRegistered);
if (isRegistered) {
console.log('✅ Integration test passed');
} else {
console.error('❌ Integration test failed');
}
}, 1000);
});
Context Provider Testing
Test your context provider function:
function testContextProvider() {
const registration = ModuleRegistry.getRegisteredModule('your-module-id');
if (!registration?.contextProvider) {
console.error('No context provider found');
return;
}
try {
const context = registration.contextProvider();
// Check context structure
console.log('Generated context:', context);
console.log('Context size:', JSON.stringify(context).length, 'bytes');
// Test for PII (see privacy guidelines)
const contextStr = JSON.stringify(context);
const piiPatterns = [
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, // Email
/\/Users\/[^\/]+/, // User paths
/user:\s*[^\s]+/i, // User names
];
let hasPII = false;
piiPatterns.forEach(pattern => {
if (pattern.test(contextStr)) {
console.error('⚠️ Potential PII detected:', pattern);
hasPII = true;
}
});
if (!hasPII) {
console.log('✅ Context provider privacy check passed');
}
return context;
} catch (error) {
console.error('❌ Context provider error:', error);
}
}
Error Filter Testing
Test your error filter function:
function testErrorFilter() {
const registration = ModuleRegistry.getRegisteredModule('your-module-id');
if (!registration?.errorFilter) {
console.log('No error filter configured');
return;
}
// Test cases
const testErrors = [
new Error('Network request failed'),
new Error('Permission denied'),
new Error('Your module specific error'),
new Error('Script error.'),
];
testErrors.forEach(error => {
error.stack = `/modules/your-module-id/script.js:1:1`;
const shouldFilter = registration.errorFilter(error);
console.log(`Error: "${error.message}" - Filtered: ${shouldFilter}`);
});
}
Manual Error Testing
Generate Test Errors
Create controlled test scenarios:
// Test manual error reporting
function testManualReporting() {
if (!window.ErrorsAndEchoesAPI?.reportError) {
console.error('Manual reporting not available');
return;
}
try {
// Create a test error
const testError = new Error('Test error from manual testing');
testError.stack = `Error: Test error
at testManualReporting (/modules/your-module-id/test.js:1:1)
at HTMLButtonElement.<anonymous> (/modules/your-module-id/test.js:5:5)`;
// Report with context
window.ErrorsAndEchoesAPI.reportError(testError, {
test: true,
operation: 'manual-test',
userAction: 'button-click',
moduleVersion: game.modules.get('your-module-id')?.version
});
console.log('✅ Manual error report sent');
} catch (error) {
console.error('❌ Manual reporting failed:', error);
}
}
// Test automatic error capturing
function testAutomaticCapture() {
// This error should be automatically captured
setTimeout(() => {
throw new Error('Test automatic error capture from your-module-id');
}, 100);
}
// Test async error capturing
async function testAsyncErrors() {
try {
await new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Test async error')), 100);
});
} catch (error) {
// This should be captured if it bubbles up
throw error;
}
}
Hook Error Testing
Test error reporting in Foundry hooks:
// Test hook error reporting
Hooks.on('updateActor', (actor, data, options, userId) => {
if (data.testError) {
// Intentional test error
throw new Error('Test hook error from your-module-id');
}
});
// Trigger the test
function testHookError() {
const actor = game.actors.contents[0];
if (actor) {
actor.update({ testError: true });
}
}
Endpoint Testing
Test Endpoint Connectivity
Test your error reporting endpoint:
async function testEndpointConnectivity() {
const testUrl = 'https://your-endpoint.com/test/your-author';
try {
const response = await fetch(testUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
test: true,
timestamp: new Date().toISOString(),
source: 'manual-test'
})
});
const result = await response.json();
if (result.success) {
console.log('✅ Endpoint connectivity test passed:', result);
} else {
console.error('❌ Endpoint test failed:', result);
}
} catch (error) {
console.error('❌ Endpoint connectivity error:', error);
}
}
Test Error Report Format
Verify your endpoint receives properly formatted reports:
async function testErrorReportFormat() {
const testReport = {
error: {
message: 'Test error message',
stack: 'Error: Test\n at test.js:1:1',
type: 'Error',
source: 'your-module-id'
},
attribution: {
moduleId: 'your-module-id',
confidence: 'high',
method: 'manual',
source: 'user-report'
},
foundry: {
version: game.version,
system: {
id: game.system.id,
version: game.system.version
}
},
meta: {
timestamp: new Date().toISOString(),
privacyLevel: 'minimal',
reporterVersion: '0.1.2'
},
client: {
sessionId: 'test-session-123',
browser: navigator.userAgent.split(' ')[0]
}
};
try {
const response = await fetch('https://your-endpoint.com/report/your-author', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(testReport)
});
const result = await response.json();
console.log('Error report test result:', result);
} catch (error) {
console.error('Error report test failed:', error);
}
}
Debug Tools
Error Reporter Status
Check the status of the error reporter:
function checkErrorReporterStatus() {
const errorReporter = game.modules.get('errors-and-echoes');
if (!errorReporter?.active) {
console.log('❌ Errors and Echoes module not active');
return;
}
console.log('✅ Errors and Echoes is active');
// Check API availability
if (window.ErrorsAndEchoesAPI) {
console.log('✅ API is available');
} else {
console.log('❌ API not available');
}
// Check consent status
const hasConsent = game.settings.get('errors-and-echoes', 'error-reporting-enabled');
console.log('User consent:', hasConsent ? '✅ Enabled' : '❌ Disabled');
// Check privacy level
const privacyLevel = game.settings.get('errors-and-echoes', 'privacy-level');
console.log('Privacy level:', privacyLevel);
// Check registered modules
const stats = ModuleRegistry.getStats();
console.log('Registration stats:', stats);
}
Registry Inspection
Inspect the module registry:
function inspectRegistry() {
console.log('=== Module Registry Inspection ===');
const allModules = ModuleRegistry.getAllRegisteredModules();
console.log(`Total registered modules: ${allModules.length}`);
allModules.forEach(module => {
console.log(`\n📦 ${module.moduleId}`);
console.log(` Registered: ${new Date(module.registeredAt).toLocaleString()}`);
console.log(` Context Provider: ${module.contextProvider ? '✅' : '❌'}`);
console.log(` Error Filter: ${module.errorFilter ? '✅' : '❌'}`);
console.log(` Custom Endpoint: ${module.endpoint ? '✅' : '❌'}`);
if (module.endpoint) {
console.log(` Endpoint URL: ${module.endpoint.url}`);
console.log(` Endpoint Enabled: ${module.endpoint.enabled ? '✅' : '❌'}`);
}
});
}
Error Attribution Testing
Test error attribution accuracy:
function testErrorAttribution() {
// Create errors with different stack traces
const testCases = [
{
name: 'Module Error',
stack: `Error: Test
at moduleFunction (/modules/your-module-id/script.js:10:5)
at onClick (/modules/your-module-id/ui.js:25:10)`
},
{
name: 'Core Error',
stack: `Error: Test
at coreFunction (/foundry.js:100:5)
at hookCall (/foundry.js:200:10)`
},
{
name: 'Other Module Error',
stack: `Error: Test
at otherFunction (/modules/other-module/script.js:50:5)`
}
];
testCases.forEach(testCase => {
const error = new Error('Test attribution error');
error.stack = testCase.stack;
// Test attribution (this would normally be internal)
const attribution = window.ErrorsAndEchoes?.ErrorAttribution.attributeToModule(error, {
source: 'javascript',
timestamp: Date.now()
});
console.log(`${testCase.name}:`, attribution);
});
}
Performance Testing
Context Provider Performance
Test context provider performance:
function testContextProviderPerformance() {
const registration = ModuleRegistry.getRegisteredModule('your-module-id');
if (!registration?.contextProvider) {
console.log('No context provider to test');
return;
}
const iterations = 1000;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
registration.contextProvider();
}
const end = performance.now();
const avgTime = (end - start) / iterations;
console.log(`Context provider performance:`);
console.log(` ${iterations} iterations in ${(end - start).toFixed(2)}ms`);
console.log(` Average: ${avgTime.toFixed(4)}ms per call`);
if (avgTime > 10) {
console.warn('⚠️ Context provider may be slow (>10ms average) - consider optimization');
} else {
console.log('✅ Context provider performance appears reasonable');
}
}
Memory Usage Testing
Monitor memory usage:
function testMemoryUsage() {
if (!performance.memory) {
console.log('Memory API not available');
return;
}
const before = performance.memory.usedJSHeapSize;
// Generate 100 test error reports
for (let i = 0; i < 100; i++) {
const testError = new Error(`Test error ${i}`);
window.ErrorsAndEchoesAPI?.reportError(testError, {
test: true,
iteration: i
});
}
// Force garbage collection if available
if (window.gc) {
window.gc();
}
setTimeout(() => {
const after = performance.memory.usedJSHeapSize;
const increase = after - before;
console.log(`Memory usage test:`);
console.log(` Before: ${(before / 1024 / 1024).toFixed(2)} MB`);
console.log(` After: ${(after / 1024 / 1024).toFixed(2)} MB`);
console.log(` Increase: ${(increase / 1024).toFixed(2)} KB`);
if (increase > 1024 * 1024) { // 1MB
console.warn('⚠️ Significant memory increase detected - may indicate leaks');
} else {
console.log('✅ Memory usage appears stable');
}
}, 2000);
}
Debugging Common Issues
API Not Available
// Debug API availability issues
function debugAPIAvailability() {
console.log('=== API Availability Debug ===');
// Check module status
const module = game.modules.get('errors-and-echoes');
console.log('Module found:', !!module);
console.log('Module active:', module?.active);
// Check user permissions
console.log('User is GM:', game.user.isGM);
// Check initialization
console.log('API available:', !!window.ErrorsAndEchoesAPI);
// Check for conflicts
const errorModules = Array.from(game.modules.values())
.filter(m => m.active && m.id.includes('error'))
.map(m => m.id);
console.log('Other error-related modules:', errorModules);
}
Registration Failures
// Debug registration issues
function debugRegistration(moduleId) {
console.log(`=== Registration Debug for ${moduleId} ===`);
try {
// Check if already registered
const existing = ModuleRegistry.getRegisteredModule(moduleId);
console.log('Existing registration:', existing);
// Test registration
window.ErrorsAndEchoesAPI.register({
moduleId: moduleId,
contextProvider: () => ({ debug: true })
});
// Verify registration
const registered = ModuleRegistry.isRegistered(moduleId);
console.log('Registration successful:', registered);
} catch (error) {
console.error('Registration error:', error);
}
}
Error Reporting Failures
// Debug error reporting issues
function debugErrorReporting() {
console.log('=== Error Reporting Debug ===');
// Check consent
const hasConsent = game.settings.get('errors-and-echoes', 'error-reporting-enabled');
console.log('Has consent:', hasConsent);
// Check network connectivity
navigator.onLine && console.log('Network online:', navigator.onLine);
// Test with minimal error
try {
window.ErrorsAndEchoesAPI.reportError(new Error('Debug test'), {
debug: true
});
console.log('Error reporting call successful');
} catch (error) {
console.error('Error reporting failed:', error);
}
}
Automated Testing
Test Suite Template
// Complete test suite for your module integration
class ErrorReportingTestSuite {
constructor(moduleId) {
this.moduleId = moduleId;
this.results = [];
}
async runAllTests() {
console.log(`🧪 Running Error Reporting Test Suite for ${this.moduleId}`);
await this.testRegistration();
await this.testContextProvider();
await this.testErrorFilter();
await this.testManualReporting();
await this.testEndpointConnectivity();
this.printResults();
}
async testRegistration() {
try {
window.ErrorsAndEchoesAPI.register({
moduleId: this.moduleId,
contextProvider: () => ({ test: true })
});
const isRegistered = ModuleRegistry.isRegistered(this.moduleId);
this.addResult('Registration', isRegistered, 'Module registration');
} catch (error) {
this.addResult('Registration', false, error.message);
}
}
async testContextProvider() {
try {
const registration = ModuleRegistry.getRegisteredModule(this.moduleId);
const context = registration?.contextProvider?.();
const hasContext = !!context;
const isObject = typeof context === 'object';
const hasNoFunc = !JSON.stringify(context).includes('function');
this.addResult('Context Provider', hasContext && isObject && hasNoFunc,
'Context provider returns valid object');
} catch (error) {
this.addResult('Context Provider', false, error.message);
}
}
async testErrorFilter() {
try {
const registration = ModuleRegistry.getRegisteredModule(this.moduleId);
if (registration?.errorFilter) {
const testError = new Error('Test');
const result = registration.errorFilter(testError);
this.addResult('Error Filter', typeof result === 'boolean',
'Error filter returns boolean');
} else {
this.addResult('Error Filter', true, 'No filter configured (optional)');
}
} catch (error) {
this.addResult('Error Filter', false, error.message);
}
}
async testManualReporting() {
try {
window.ErrorsAndEchoesAPI.reportError(new Error('Test'), { test: true });
this.addResult('Manual Reporting', true, 'Manual error reporting works');
} catch (error) {
this.addResult('Manual Reporting', false, error.message);
}
}
async testEndpointConnectivity() {
// This would test your specific endpoint
this.addResult('Endpoint', true, 'Endpoint test not implemented');
}
addResult(test, passed, message) {
this.results.push({ test, passed, message });
}
printResults() {
console.log('\n📊 Test Results:');
this.results.forEach(result => {
const icon = result.passed ? '✅' : '❌';
console.log(`${icon} ${result.test}: ${result.message}`);
});
const passed = this.results.filter(r => r.passed).length;
const total = this.results.length;
console.log(`\n🎯 ${passed}/${total} tests passed`);
}
}
// Run the test suite
new ErrorReportingTestSuite('your-module-id').runAllTests();
This comprehensive testing guide helps ensure your error reporting integration is working correctly and efficiently.