besper-frontend-site-dev-main
Version:
Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment
1,311 lines (1,205 loc) • 122 kB
JavaScript
/**
* BSP Logger Component
* A comprehensive logging and debugging interface with network tracing capabilities
* Converted from Liquid template to pure JavaScript with internationalization support
*/
// Logger translations for internationalization
const loggerTranslations = {
en: {
report_bug: 'Report a Bug',
intro_text:
"We'd like to help you resolve any issues you're experiencing. To provide the best support, we can include technical logs with your bug report.",
error_detected_subtitle:
'We detected an error and would appreciate if you could send us a report',
error_detected_explanation:
'Including a copy of the network trace for this page helps us diagnose the issue faster. Network traces contain information about page loading and API calls - no personal data like passwords or private content is included.',
manual_report_subtitle:
"Help us improve by reporting issues you've encountered",
what_data: 'What data would be included?',
data_may_contain: 'The logs may contain',
data_browser: 'Browser console messages and errors',
data_js: 'JavaScript execution logs',
data_network: 'Network request information',
data_timestamps: 'Timestamps of website interactions',
no_personal: 'No personal information',
no_personal_desc:
'like passwords, email content, or private data is collected.',
your_choices: 'Your choices:',
include_logs: 'Include technical logs',
include_logs_desc: 'This helps our team diagnose the issue faster',
contact_me: 'Support may contact me',
contact_me_desc: 'Allow our team to reach out if they need more details',
help_resolve: 'To help us resolve your issue faster:',
describe_what: 'Describe what you were trying to do',
describe_when: 'when the problem occurred',
mention_expected: 'Mention what you expected',
vs_happened: 'vs. what actually happened',
include_screenshots: 'Include screenshots',
screenshots_desc: 'if possible (you can attach them to the email)',
note_browser: 'Note your browser type',
browser_types: '(Chrome, Firefox, Safari, etc.)',
email_review:
'Your email client will open with a pre-filled message. You can review everything before sending and make any changes you want.',
cancel: 'Cancel',
open_email: 'Open Email Client',
email_subject: 'BSP Logger: Issue Report from Website',
email_greeting: 'Dear support team,',
email_intro: 'I encountered an issue on your website.',
email_problem_desc: 'PROBLEM DESCRIPTION:',
email_problem_placeholder:
'(Please describe the issue here and feel free to add screenshots)',
email_what_trying: 'WHAT I WAS TRYING TO DO:',
email_what_placeholder:
'(Describe what you were doing when the problem occurred)',
email_expected: 'WHAT I EXPECTED:',
email_expected_placeholder: '(Describe what should have happened)',
email_browser_info: 'BROWSER INFORMATION:',
email_browser_prefix: 'Browser:',
email_timestamp_prefix: 'Timestamp:',
email_contact_yes:
'[SUCCESS] You may contact me if you need additional information.',
email_contact_no: '[ERROR] Please do not contact me regarding this report.',
email_closing: 'Best regards,',
email_logs_header: '--- Technical Logs ---',
init_success: 'BSP Logger initialized successfully',
logs_cleared: 'Logs cleared',
logs_copied: 'Logs copied to clipboard',
copy_failed: 'Failed to copy logs',
opening_email: 'Opening email client for bug report',
no_logs_available: 'No logs available to include.',
additional_logs: 'and {count} more log entries',
show_network_traces: 'Show Network Traces',
show_logs: 'Show Logs',
network_traces_title: 'Network Activity Monitor',
no_network_activity: 'No network activity recorded',
start_recording: 'Start recording network activity',
},
de: {
report_bug: 'Fehler melden',
intro_text:
'Wir möchten Ihnen bei der Lösung von Problemen helfen, die Sie erleben. Um den bestmöglichen Support zu bieten, können wir technische Logs in Ihren Fehlerbericht einbeziehen.',
error_detected_subtitle:
'Wir haben einen Fehler erkannt und würden uns freuen, wenn Sie uns einen Bericht senden könnten',
error_detected_explanation:
'Das Einbeziehen einer Kopie der Netzwerk-Trace für diese Seite hilft uns, das Problem schneller zu diagnostizieren. Netzwerk-Traces enthalten Informationen über das Laden der Seite und API-Aufrufe - keine persönlichen Daten wie Passwörter oder private Inhalte sind enthalten.',
manual_report_subtitle:
'Helfen Sie uns bei der Verbesserung, indem Sie Probleme melden, die Sie erlebt haben',
what_data: 'Welche Daten würden einbezogen?',
data_may_contain: 'Die Logs können enthalten',
data_browser: 'Browser-Konsolen-Nachrichten und Fehler',
data_js: 'JavaScript-Ausführungsprotokoll',
data_network: 'Netzwerk-Anfrage-Informationen',
data_timestamps: 'Zeitstempel von Website-Interaktionen',
no_personal: 'Keine persönlichen Informationen',
no_personal_desc:
'wie Passwörter, E-Mail-Inhalte oder private Daten werden gesammelt.',
your_choices: 'Ihre Wahlmöglichkeiten:',
include_logs: 'Technische Logs einbeziehen',
include_logs_desc:
'Dies hilft unserem Team, das Problem schneller zu diagnostizieren',
contact_me: 'Support darf mich kontaktieren',
contact_me_desc:
'Erlauben Sie unserem Team, sich zu melden, wenn weitere Details benötigt werden',
help_resolve: 'Um Ihr Problem schneller zu lösen:',
describe_what: 'Beschreiben Sie, was Sie zu tun versucht haben',
describe_when: 'als das Problem auftrat',
mention_expected: 'Erwähnen Sie, was Sie erwartet haben',
vs_happened: 'vs. was tatsächlich passiert ist',
include_screenshots: 'Fügen Sie Screenshots hinzu',
screenshots_desc: 'wenn möglich (Sie können sie an die E-Mail anhängen)',
note_browser: 'Notieren Sie Ihren Browser-Typ',
browser_types: '(Chrome, Firefox, Safari, etc.)',
email_review:
'Ihr E-Mail-Client wird mit einer vorausgefüllten Nachricht geöffnet. Sie können alles vor dem Senden überprüfen und beliebige Änderungen vornehmen.',
cancel: 'Abbrechen',
open_email: 'E-Mail-Client öffnen',
email_subject: 'BSP Logger: Fehlerbericht von der Website',
email_greeting: 'Sehr geehrtes Support-Team,',
email_intro: 'ich habe ein Problem auf Ihrer Website festgestellt.',
email_problem_desc: 'BESCHREIBUNG DES PROBLEMS:',
email_problem_placeholder:
'(Bitte beschreiben Sie hier das Problem und fügen Sie ggf. Screenshots hinzu)',
email_what_trying: 'WAS ICH VERSUCHT HABE:',
email_what_placeholder:
'(Beschreiben Sie, was Sie getan haben, als das Problem auftrat)',
email_expected: 'WAS ICH ERWARTET HATTE:',
email_expected_placeholder: '(Beschreiben Sie, was hätte passieren sollen)',
email_browser_info: 'BROWSER INFORMATION:',
email_browser_prefix: 'Browser:',
email_timestamp_prefix: 'Zeitpunkt:',
email_contact_yes:
'[SUCCESS] Sie können mich gerne kontaktieren, falls Sie weitere Informationen benötigen.',
email_contact_no:
'[ERROR] Bitte kontaktieren Sie mich nicht bezüglich dieses Reports.',
email_closing: 'Mit freundlichen Grüßen',
email_logs_header: '--- Technische Logs ---',
init_success: 'BSP Logger erfolgreich initialisiert',
logs_cleared: 'Logs gelöscht',
logs_copied: 'Logs in die Zwischenablage kopiert',
copy_failed: 'Kopieren der Logs fehlgeschlagen',
opening_email: 'E-Mail-Client für Fehlerbericht wird geöffnet',
no_logs_available: 'Keine Logs zum Einbeziehen verfügbar.',
additional_logs: 'und {count} weitere Log-Einträge',
show_network_traces: 'Netzwerk-Traces anzeigen',
show_logs: 'Logs anzeigen',
network_traces_title: 'Netzwerk-Aktivitäts-Monitor',
no_network_activity: 'Keine Netzwerk-Aktivität aufgezeichnet',
start_recording: 'Netzwerk-Aktivität aufzeichnen starten',
},
};
/**
* BSP Logger Class
*/
export class BSPLogger {
constructor(language = 'en', options = {}) {
this.language = language.split('-')[0].toLowerCase(); // Extract main language code
this.translations =
loggerTranslations[this.language] || loggerTranslations.en;
this.options = {
debug: false,
maxEntries: 100,
maxChartPoints: 120,
maxLivePoints: 120,
...options,
};
this.elements = {};
this.state = {
isOpen: false,
currentFilter: 'all',
logs: [],
chartData: [],
lastLogTime: 0,
liveLineData: [],
lastBucketTime: null,
hasUnacknowledgedErrors: false,
showNetworkTraces: false, // New: Toggle between logs and network traces
networkTraces: [], // New: Store network activity
isRecording: false, // New: Recording state
currentTraceFilter: 'all', // New: Current network trace filter
};
this.originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
};
}
/**
* Get translation for a key
*/
t(key, replacements = {}) {
let translation = this.translations[key] || key;
// Handle replacements like {count}
Object.keys(replacements).forEach(replaceKey => {
translation = translation.replace(
`{${replaceKey}}`,
replacements[replaceKey]
);
});
return translation;
}
/**
* Initialize the logger
*/
init() {
try {
console.log('[BSP Logger] [INIT] Initializing logger...');
this.createLoggerHTML();
this.setupEventListeners();
this.captureExistingLogs();
this.interceptConsoleMethods();
this.setupErrorHandling();
this.setupPerformanceObserver();
this.setupGlobalMessageInterceptor();
this.setupNetworkTracing(); // New: Network tracing setup
this.startChartUpdates();
this.addLogEntry(this.t('init_success'), 'info');
// Verify the logger is visible in the DOM and position above footer
setTimeout(() => {
const container = document.getElementById('bsp_logger_container');
if (container) {
console.log(
'[BSP Logger] [SUCCESS] Logger container successfully added to DOM'
);
// Position above footer in document flow
container.style.position = 'static';
container.style.margin = '0';
container.style.zIndex = '9999';
container.style.display = 'block';
container.style.visibility = 'visible';
container.style.width = '100%';
container.style.pointerEvents = 'auto';
container.style.clear = 'both';
container.style.border = 'none';
container.style.outline = 'none';
console.log(
'[BSP Logger] [SUCCESS] Logger positioned above footer in document flow'
);
} else {
console.error(
'[BSP Logger] [ERROR] Logger container not found in DOM!'
);
// Try to recreate if missing
console.log(
'[BSP Logger] [LOADING] Attempting to recreate logger...'
);
this.createLoggerHTML();
}
}, 200);
} catch (error) {
console.error('[BSP Logger] [ERROR] Initialization failed:', error);
throw error;
}
}
/**
* Create the logger HTML structure
*/
createLoggerHTML() {
// Create container
const container = document.createElement('div');
container.id = 'bsp_logger_container';
container.innerHTML = this.getLoggerHTML();
// Add CSS
const style = document.createElement('style');
style.textContent = this.getLoggerCSS();
document.head.appendChild(style);
// Add to page - position before any footer elements
this.insertLoggerInCorrectPosition(container);
// Store element references
this.elements = {
container,
header: container.querySelector('#bsp_logger_header'),
content: container.querySelector('#bsp_logger_content'),
miniChart: container.querySelector('#bsp_logger_mini_chart'),
liveSvg: container.querySelector('#bsp_logger_live_svg'),
chevron: container.querySelector('.bsp_logger_chevron_icon'),
bugIcon: container.querySelector('#bsp_logger_bug_icon'),
modalOverlay: container.querySelector('#bsp_logger_modal_overlay'),
modalCancel: container.querySelector('#bsp_modal_cancel'),
modalSend: container.querySelector('#bsp_modal_send'),
networkToggle: container.querySelector('#bsp_logger_network_toggle'),
networkTraceContainer: container.querySelector(
'#bsp_logger_network_trace'
),
networkTraceBody: container.querySelector('#bsp_logger_trace_body'),
networkTraceBodyWrapper: container.querySelector(
'.bsp_logger_trace_body_wrapper'
),
// New trace controls
clearTracesBtn: container.querySelector('#bsp_logger_clear_traces'),
exportHarBtn: container.querySelector('#bsp_logger_export_har'),
traceCount: container.querySelector('#bsp_logger_trace_count'),
};
// Load D3 for charts
this.loadD3();
}
/**
* Insert logger in the correct position above footer elements
*/
insertLoggerInCorrectPosition(container) {
// Look for common footer selectors
const footerSelectors = [
'footer',
'.footer',
'#footer',
'.page-footer',
'.site-footer',
'[role="contentinfo"]',
'.row.sectionBlockLayout', // BSP specific footer layout
'.help-company-legal', // BSP specific footer sections
];
let insertionPoint = null;
// Find the first footer element
for (const selector of footerSelectors) {
const footerElement = document.querySelector(selector);
if (footerElement) {
insertionPoint = footerElement;
break;
}
}
if (insertionPoint) {
// Insert logger before the footer
insertionPoint.parentNode.insertBefore(container, insertionPoint);
console.log('[BSP Logger] 📍 Logger positioned above footer element');
} else {
// Fallback: append to body
document.body.appendChild(container);
console.log(
'[BSP Logger] 📍 Logger positioned at end of body (no footer found)'
);
}
}
/**
* Get the logger HTML template
*/
getLoggerHTML() {
return `
<div class="bsp_logger_header" id="bsp_logger_header">
<div class="bsp_logger_header_left">
<svg class="bsp_logger_terminal_icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<polyline points="4,17 10,11 4,5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
<svg class="bsp_logger_chevron_icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</div>
<div class="bsp_logger_header_center">
<div class="bsp_logger_mini_chart bsp_logger_expanded_only" id="bsp_logger_mini_chart" title="Log activity" style="display: none;">
<div class="bsp_logger_live_line">
<svg class="bsp_logger_live_svg" id="bsp_logger_live_svg"></svg>
</div>
</div>
</div>
<div class="bsp_logger_header_right">
<!-- Network traces toggle - only visible when expanded -->
<button class="bsp_logger_network_toggle bsp_logger_expanded_only" id="bsp_logger_network_toggle" title="${this.t('show_network_traces')}">
<svg class="bsp_logger_network_icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</button>
<!-- Copy button - only visible when expanded -->
<button class="bsp_logger_copy_btn bsp_logger_expanded_only" id="bsp_logger_copy_all" title="Copy all logs">
<svg class="bsp_logger_copy_icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
</svg>
</button>
<!-- Report button - professional design with animated error indicator -->
<button class="bsp_logger_bug_btn" id="bsp_logger_bug_icon" title="${this.t('report_bug')}">
<svg class="bsp_logger_bug_icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<rect width="8" height="14" x="8" y="6" rx="4"></rect>
<path d="m19 7-3 2"></path>
<path d="m5 7 3 2"></path>
<path d="m19 19-3-2"></path>
<path d="m5 19 3-2"></path>
<path d="M20 13h-4"></path>
<path d="M4 13h4"></path>
<path d="m10 4 1 2"></path>
<path d="m14 4-1 2"></path>
</svg>
</button>
</div>
</div>
<!-- Log content (default view) -->
<div class="bsp_logger_content" id="bsp_logger_content">
</div>
<!-- Network traces container (new) -->
<div class="bsp_logger_network_trace" id="bsp_logger_network_trace" style="display: none;">
<!-- Network trace controls with simplified filter buttons -->
<div class="bsp_logger_trace_controls">
<div class="bsp_logger_trace_filters">
<button class="bsp_logger_trace_filter_btn bsp_logger_active" data-trace-filter="all">All</button>
<button class="bsp_logger_trace_filter_btn" data-trace-filter="fetch">XHR</button>
<button class="bsp_logger_trace_filter_btn" data-trace-filter="doc">Doc</button>
<button class="bsp_logger_trace_filter_btn" data-trace-filter="css">CSS</button>
<button class="bsp_logger_trace_filter_btn" data-trace-filter="js">JS</button>
<button class="bsp_logger_trace_filter_btn" data-trace-filter="img">Img</button>
<button class="bsp_logger_trace_filter_btn" data-trace-filter="other">Other</button>
</div>
<div class="bsp_logger_trace_actions">
<button class="bsp_logger_action_btn" id="bsp_logger_clear_traces" title="Clear network traces">
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
Clear
</button>
<button class="bsp_logger_action_btn" id="bsp_logger_export_har" title="Export HAR file">
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Export
</button>
<div class="bsp_logger_trace_count_display" id="bsp_logger_trace_count">
0 requests
</div>
</div>
</div>
<div class="bsp_logger_trace_header">
<div class="bsp_logger_trace_col">Request</div>
<div class="bsp_logger_trace_col">Status</div>
<div class="bsp_logger_trace_col">Type</div>
<div class="bsp_logger_trace_col">Size</div>
<div class="bsp_logger_trace_col">Time</div>
<div class="bsp_logger_trace_col-waterfall">Waterfall</div>
</div>
<div class="bsp_logger_trace_body_wrapper">
<div class="bsp_logger_trace_body" id="bsp_logger_trace_body">
<div class="bsp_logger_trace_placeholder">
<div style="font-size: 14px; font-weight: bold;">${this.t('no_network_activity')}</div>
<div class="bsp_logger_trace_subtitle">${this.t('start_recording')}</div>
</div>
</div>
</div>
</div>
<!-- Modal and tooltip -->
<div class="bsp_logger_d3_tooltip" id="bsp_logger_tooltip"></div>
${this.getBugReportModalHTML()}
`;
}
/**
* Get the bug report modal HTML - Enhanced Professional Design
*/
getBugReportModalHTML() {
const isErrorDetected = this.state.hasUnacknowledgedErrors;
const subtitle = isErrorDetected
? this.t('error_detected_subtitle')
: this.t('manual_report_subtitle');
return `
<div class="bsp_npm_site_modal_overlay" id="bsp_logger_modal_overlay">
<div class="bsp_npm_site_modal">
<div class="bsp_npm_site_modal_header">
<div class="bsp_npm_site_modal_header_content">
<div class="bsp_npm_site_modal_icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="#4299e1" stroke="#4299e1">
<rect width="8" height="14" x="8" y="6" rx="4"></rect>
<path d="m19 7-3 2"></path>
<path d="m5 7 3 2"></path>
<path d="m19 19-3-2"></path>
<path d="m5 19 3-2"></path>
<path d="M20 13h-4"></path>
<path d="M4 13h4"></path>
<path d="m10 4 1 2"></path>
<path d="m14 4-1 2"></path>
</svg>
</div>
<div class="bsp_npm_site_modal_title_section">
<h2 class="bsp_npm_site_modal_title">${this.t('report_bug')}</h2>
<p class="bsp_npm_site_modal_subtitle">${subtitle}</p>
${
isErrorDetected
? `
<div class="bsp_npm_site_error_detection_notice">
<div class="bsp_npm_site_error_notice_icon">[WARN]</div>
<div class="bsp_npm_site_error_notice_text">
${this.t('error_detected_explanation')}
</div>
</div>
`
: ''
}
</div>
</div>
<button class="bsp_npm_site_modal_close_btn" id="bsp_modal_close">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="bsp_npm_site_modal_body">
<form class="bsp_npm_site_bug_report_form" id="bsp_bug_report_form">
<!-- Issue Description -->
<div class="bsp_npm_site_form_section">
<h3 class="bsp_npm_site_section_title">
<span class="bsp_npm_site_section_icon">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path>
</svg>
</span>
Describe the Issue
</h3>
<div class="bsp_npm_site_form_group">
<label for="bsp_bug_message" class="bsp_npm_site_form_label">What happened? <span class="bsp_npm_site_required">*</span></label>
<textarea
id="bsp_bug_message"
name="message"
class="bsp_npm_site_form_textarea"
placeholder="Please describe the issue you encountered in detail..."
rows="4"
required
></textarea>
</div>
<div class="bsp_npm_site_form_row">
<div class="bsp_npm_site_form_group bsp_npm_site_form_group_half">
<label for="bsp_expected_behavior" class="bsp_npm_site_form_label">Expected Behavior</label>
<input
type="text"
id="bsp_expected_behavior"
name="expected_behavior"
class="bsp_npm_site_form_input"
placeholder="What should have happened?"
/>
</div>
<div class="bsp_npm_site_form_group bsp_npm_site_form_group_half">
<label for="bsp_actual_behavior" class="bsp_npm_site_form_label">Actual Behavior</label>
<input
type="text"
id="bsp_actual_behavior"
name="actual_behavior"
class="bsp_npm_site_form_input"
placeholder="What actually happened?"
/>
</div>
</div>
<div class="bsp_npm_site_form_group">
<label for="bsp_steps_reproduce" class="bsp_npm_site_form_label">Steps to Reproduce</label>
<textarea
id="bsp_steps_reproduce"
name="steps_to_reproduce"
class="bsp_npm_site_form_textarea"
placeholder="1. Go to... 2. Click on... 3. See error"
rows="3"
></textarea>
</div>
</div>
<!-- Contact Information -->
<div class="bsp_npm_site_form_section">
<h3 class="bsp_npm_site_section_title">
<span class="bsp_npm_site_section_icon">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
</span>
Contact Information
</h3>
<div class="bsp_npm_site_form_group">
<label for="bsp_user_email" class="bsp_npm_site_form_label">Email Address</label>
<input
type="email"
id="bsp_user_email"
name="user_email"
class="bsp_npm_site_form_input"
placeholder="your.email@example.com"
/>
<div class="bsp_npm_site_form_help">Optional - only if you want us to follow up</div>
</div>
<div class="bsp_npm_site_checkbox_group">
<div class="bsp_npm_site_checkbox_item">
<input type="checkbox" id="bsp_allow_contact" name="allow_contact" class="bsp_npm_site_checkbox">
<label for="bsp_allow_contact" class="bsp_npm_site_checkbox_label">
<span class="bsp_npm_site_checkbox_custom"></span>
<span class="bsp_npm_site_checkbox_text">
<strong>Allow support team to contact me</strong>
<br><small>We may reach out if we need additional information</small>
</span>
</label>
</div>
</div>
</div>
<!-- Technical Details -->
<div class="bsp_npm_site_form_section">
<h3 class="bsp_npm_site_section_title">
<span class="bsp_npm_site_section_icon">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
</span>
Technical Details
</h3>
<div class="bsp_npm_site_form_row">
<div class="bsp_npm_site_form_group bsp_npm_site_form_group_half">
<label for="bsp_severity" class="bsp_npm_site_form_label">Severity</label>
<select id="bsp_severity" name="severity" class="bsp_npm_site_form_select">
<option value="low">Low - Minor inconvenience</option>
<option value="medium" selected>Medium - Affects functionality</option>
<option value="high">High - Major impact</option>
<option value="critical">Critical - Blocks usage</option>
</select>
</div>
<div class="bsp_npm_site_form_group bsp_npm_site_form_group_half">
<label for="bsp_browser_info" class="bsp_npm_site_form_label">Browser Info</label>
<input
type="text"
id="bsp_browser_info"
name="browser_info"
class="bsp_npm_site_form_input"
readonly
value=""
/>
</div>
</div>
<div class="bsp_npm_site_checkbox_group">
<div class="bsp_npm_site_checkbox_item">
<input type="checkbox" id="bsp_include_har" name="include_har" class="bsp_npm_site_checkbox" checked>
<label for="bsp_include_har" class="bsp_npm_site_checkbox_label">
<span class="bsp_npm_site_checkbox_custom"></span>
<span class="bsp_npm_site_checkbox_text">
<strong>Include network trace data (recommended)</strong>
<br><small>This records how the page loads and communicates with our servers. It's completely safe and contains no passwords, personal data, or private content - just technical information that helps us reproduce and fix issues faster.</small>
</span>
</label>
</div>
</div>
</div>
<!-- Privacy Notice -->
<div class="bsp_npm_site_privacy_notice">
<div class="bsp_npm_site_privacy_icon">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
</div>
<div class="bsp_npm_site_privacy_content">
<h4>Privacy & Data Collection</h4>
<ul>
<li><strong>No personal data</strong> like passwords or private content is collected</li>
<li>Technical logs help us reproduce and fix the issue faster</li>
<li>Data is only used for debugging and improving our service</li>
</ul>
</div>
</div>
</form>
</div>
<div class="bsp_npm_site_modal_footer">
<button type="button" class="bsp_npm_site_modal_btn bsp_npm_site_modal_btn_secondary" id="bsp_modal_cancel">
Cancel
</button>
<button type="submit" form="bsp_bug_report_form" class="bsp_npm_site_modal_btn bsp_npm_site_modal_btn_primary" id="bsp_modal_send">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
</svg>
Submit Bug Report
</button>
</div>
</div>
</div>
`;
}
/**
* Setup event listeners
*/
setupEventListeners() {
this.elements.header.addEventListener('click', e => {
if (
!e.target.closest('.bsp_logger_copy_btn') &&
!e.target.closest('.bsp_logger_bug_btn') &&
!e.target.closest('.bsp_logger_network_toggle')
) {
this.toggleLogger();
}
});
// Network toggle
this.elements.networkToggle.addEventListener('click', e => {
e.stopPropagation();
this.toggleNetworkView();
});
// Network trace controls
this.elements.clearTracesBtn.addEventListener('click', e => {
e.stopPropagation();
this.clearNetworkTraces();
});
this.elements.exportHarBtn.addEventListener('click', e => {
e.stopPropagation();
this.exportHAR();
});
// Chrome DevTools style filter buttons
document
.querySelectorAll('.bsp_logger_trace_filter_btn')
.forEach(button => {
button.addEventListener('click', e => {
e.stopPropagation();
this.setTraceFilter(button.getAttribute('data-trace-filter'));
});
});
// Copy button
const copyBtn = this.elements.container.querySelector(
'#bsp_logger_copy_all'
);
if (copyBtn) {
copyBtn.addEventListener('click', e => {
e.stopPropagation();
this.copyAllLogs();
});
}
this.elements.bugIcon.addEventListener('click', e => {
e.stopPropagation();
this.showBugReportModal();
});
// Initial modal event listeners setup
this.setupModalEventListeners();
// Close modal with Escape key
document.addEventListener('keydown', e => {
if (
e.key === 'Escape' &&
this.elements.modalOverlay.classList.contains('bsp_logger_show')
) {
this.hideBugReportModal();
}
});
}
/**
* New: Toggle between logs and network traces view
*/
toggleNetworkView() {
this.state.showNetworkTraces = !this.state.showNetworkTraces;
if (this.state.showNetworkTraces) {
// Show network traces
this.elements.content.style.display = 'none';
this.elements.networkTraceContainer.style.display = 'flex';
this.elements.networkToggle.title = this.t('show_logs');
this.elements.networkToggle.classList.add('bsp_logger_active');
this.renderNetworkTraces();
} else {
// Show logs
this.elements.content.style.display = 'block';
this.elements.networkTraceContainer.style.display = 'none';
this.elements.networkToggle.title = this.t('show_network_traces');
this.elements.networkToggle.classList.remove('bsp_logger_active');
}
}
/**
* New: Setup network tracing
*/
setupNetworkTracing() {
const self = this;
// Override XMLHttpRequest
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (
method,
url,
_async,
_user,
_password
) {
this._bsp_method = method;
this._bsp_url = url;
this._bsp_startTime = Date.now();
return originalXHROpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (_data) {
const xhr = this;
const startTime = Date.now();
xhr.addEventListener('load', function () {
const duration = Date.now() - startTime;
const size = this.getResponseHeader('content-length') || '0';
self.addNetworkTrace({
method: xhr._bsp_method || 'GET',
url: xhr._bsp_url || 'unknown',
status: xhr.status,
statusText: xhr.statusText,
duration,
size: parseInt(size),
type: 'xhr',
timestamp: startTime,
});
});
xhr.addEventListener('error', function () {
const duration = Date.now() - startTime;
self.addNetworkTrace({
method: xhr._bsp_method || 'GET',
url: xhr._bsp_url || 'unknown',
status: 0,
statusText: 'Network Error',
duration,
size: 0,
type: 'xhr',
timestamp: startTime,
error: true,
});
});
return originalXHRSend.apply(this, arguments);
};
// Override fetch
if (window.fetch) {
const originalFetch = window.fetch;
window.fetch = function (resource, options = {}) {
const startTime = Date.now();
const method = options.method || 'GET';
const url = typeof resource === 'string' ? resource : resource.url;
return originalFetch
.apply(this, arguments)
.then(response => {
const duration = Date.now() - startTime;
const size = response.headers.get('content-length') || '0';
self.addNetworkTrace({
method,
url,
status: response.status,
statusText: response.statusText,
duration,
size: parseInt(size),
type: 'fetch',
timestamp: startTime,
error: !response.ok,
});
return response;
})
.catch(error => {
const duration = Date.now() - startTime;
self.addNetworkTrace({
method,
url,
status: 0,
statusText: error.message,
duration,
size: 0,
type: 'fetch',
timestamp: startTime,
error: true,
});
throw error;
});
};
}
}
/**
* New: Add network trace entry
*/
addNetworkTrace(trace) {
this.state.networkTraces.unshift(trace);
// Limit the number of traces stored
if (this.state.networkTraces.length > this.options.maxEntries) {
this.state.networkTraces = this.state.networkTraces.slice(
0,
this.options.maxEntries
);
}
// If currently showing network traces, re-render
if (this.state.showNetworkTraces && this.state.isOpen) {
this.renderNetworkTraces();
}
}
/**
* New: Render network traces
*/
renderNetworkTraces() {
const traceBody = this.elements.networkTraceBody;
const filteredTraces = this.getFilteredTraces();
if (this.state.networkTraces.length === 0) {
traceBody.innerHTML = `
<div class="bsp_logger_trace_placeholder">
<div style="font-size: 14px; font-weight: bold;">${this.t('no_network_activity')}</div>
<div class="bsp_logger_trace_subtitle">${this.t('start_recording')}</div>
</div>
`;
this.updateTraceCount();
return;
}
if (filteredTraces.length === 0) {
traceBody.innerHTML = `
<div class="bsp_logger_trace_placeholder">
<div style="font-size: 14px; font-weight: bold;">No matching requests</div>
<div class="bsp_logger_trace_subtitle">Try changing the filter</div>
</div>
`;
this.updateTraceCount();
return;
}
// Calculate waterfall scale
const maxDuration = Math.max(...filteredTraces.map(t => t.duration));
const scale = Math.max(maxDuration, 1000); // Minimum 1 second scale
traceBody.innerHTML = filteredTraces
.map(trace => {
const waterfallWidth = (trace.duration / scale) * 100;
const statusClass = trace.error || trace.status >= 400 ? 'error' : '';
const barClass = trace.duration > 1000 ? 'slow' : '';
return `
<div class="bsp_logger_trace_row">
<div class="bsp_logger_trace_name" title="${trace.url}">${this.truncateUrl(trace.url)}</div>
<div class="bsp_logger_trace_status ${statusClass}">${trace.status}</div>
<div class="bsp_logger_trace_type">${trace.type}</div>
<div class="bsp_logger_trace_size">${this.formatSize(trace.size)}</div>
<div class="bsp_logger_trace_time">${trace.duration}ms</div>
<div class="bsp_logger_trace_waterfall">
<div class="bsp_logger_trace_bar ${barClass}" style="width: ${waterfallWidth}%"></div>
</div>
</div>
`;
})
.join('');
this.updateTraceCount();
}
/**
* Helper: Truncate URL for display
*/
truncateUrl(url) {
if (url.length <= 50) return url;
try {
const urlObj = new URL(url);
const path = urlObj.pathname + urlObj.search;
if (path.length <= 40) {
return urlObj.hostname + path;
}
return urlObj.hostname + '...' + path.slice(-30);
} catch (e) {
return url.length > 50 ? url.slice(0, 47) + '...' : url;
}
}
/**
* Helper: Format file size
*/
formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
/**
* New: Clear network traces
*/
clearNetworkTraces() {
this.state.networkTraces = [];
this.renderNetworkTraces();
this.updateTraceCount();
}
/**
* New: Export HAR file
*/
exportHAR() {
if (this.state.networkTraces.length === 0) {
alert('No network traces to export');
return;
}
try {
const har = this.generateHAR();
const blob = new Blob([JSON.stringify(har, null, 2)], {
type: 'application/json',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `network-traces-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.har`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.addLogEntry(
`Exported HAR file with ${this.state.networkTraces.length} requests`,
'info'
);
} catch (error) {
this.addLogEntry(`Failed to export HAR: ${error.message}`, 'error');
}
}
/**
* New: Generate HAR format data
*/
generateHAR() {
const version = '1.2';
const creator = {
name: 'BSP Logger',
version: '1.0.0',
};
const entries = this.state.networkTraces.map(trace => {
return {
startedDateTime: new Date(trace.timestamp).toISOString(),
time: trace.duration,
request: {
method: trace.method,
url: trace.url,
httpVersion: 'HTTP/1.1',
headers: [],
queryString: [],
cookies: [],
headersSize: -1,
bodySize: -1,
},
response: {
status: trace.status,
statusText: trace.statusText,
httpVersion: 'HTTP/1.1',
headers: [],
cookies: [],
content: {
size: trace.size,
mimeType: this.getMimeTypeFromUrl(trace.url),
},
headersSize: -1,
bodySize: trace.size,
},
cache: {},
timings: {
blocked: 0,
dns: 0,
connect: 0,
send: 0,
wait: trace.duration,
receive: 0,
ssl: -1,
},
};
});
return {
log: {
version,
creator,
pages: [
{
startedDateTime: new Date().toISOString(),
id: 'page_1',
title: document.title,
pageTimings: {
onContentLoad: -1,
onLoad: -1,
},
},
],
entries,
},
};
}
/**
* New: Set trace filter (Chrome DevTools style)
*/
setTraceFilter(filterType) {
this.state.currentTraceFilter = filterType;
// Update button states
document.querySelectorAll('.bsp_logger_trace_filter_btn').forEach(btn => {
if (btn.getAttribute('data-trace-filter') === filterType) {
btn.classList.add('bsp_logger_active');
} else {
btn.classList.remove('bsp_logger_active');
}
});
this.renderNetworkTraces();
}
/**
* New: Filter network traces
*/
filterNetworkTraces(filterType) {
this.setTraceFilter(filterType);
}
/**
* New: Update trace count display
*/
updateTraceCount() {
const total = this.state.networkTraces.length;
const visible = this.getFilteredTraces().length;
if (
this.state.currentTraceFilter &&
this.state.currentTraceFilter !== 'all'
) {
this.elements.traceCount.textContent = `${visible} of ${total} requests`;
} else {
this.elements.traceCount.textContent = `${total} requests`;
}
}
/**
* New: Get filtered traces based on current filter
*/
getFilteredTraces() {
const filter = this.state.currentTraceFilter || 'all';
if (filter === 'all') {
return this.state.networkTraces;
}
return this.state.networkTraces.filter(trace => {
switch (filter) {
case 'fetch':
case 'xhr':
return trace.type === 'xhr' || trace.type === 'fetch';
case 'doc':
return (
this.getMimeTypeFromUrl(trace.url).includes('html') ||
trace.url.includes('.html') ||
!trace.url.includes('.')
);
case 'css':
return (
this.getMimeTypeFromUrl(trace.url).includes('css') ||
trace.url.includes('.css')
);
case 'js':
return (
this.getMimeTypeFromUrl(trace.url).includes('javascript') ||
trace.url.includes('.js') ||
trace.url.includes('.mjs')
);
case 'font':
return (
this.getMimeTypeFromUrl(trace.url).includes('font') ||
/\.(woff|woff2|ttf|eot|otf)$/i.test(trace.url)
);
case 'img':
return (
this.getMimeTypeFromUrl(trace.url).includes('image') ||
/\.(jpg|jpeg|png|gif|svg|webp|ico|bmp)$/i.test(trace.url)
);
case 'media':
return (
/\.(mp4|webm|ogg|mp3|wav|flac|aac|mov|avi|mkv)$/i.test(trace.url) ||
this.getMimeTypeFromUrl(trace.url).includes('video') ||
this.getMimeTypeFromUrl(trace.url).includes('audio')
);
case 'manifest':
return (
/\.(json|xml|webmanifest)$/i.test(trace.url) ||
trace.url.includes('manifest')
);
case 'other':
// Everything that doesn't match the other categories
return !this.isKnownResourceType(trace);
case 'error':
return trace.error || trace.status >= 400;
default:
return true;
}
});
}
/**
* Helper: Check if resource type is known
*/
isKnownResourceType(trace) {
const url = trace.url;
const mimeType = this.getMimeTypeFromUrl(url);
// Check if it matches any known categories
return (
// XHR/Fetch
trace.type === 'xhr' ||
trace.type === 'fetch' ||
// Documents
mimeType.includes('html') ||
url.includes('.html') ||
!url.includes('.') ||
// CSS
mimeType.includes('css') ||
url.includes('.css') ||
// JavaScript
mimeType.includes('javascript') ||
url.includes('.js') ||
url.includes('.mjs') ||
// Fonts
mimeType.includes('font') ||
/\.(woff|woff2|ttf|eot|otf)$/i.test(url) ||
// Images
mimeType.includes('image') ||
/\.(jpg|jpeg|png|gif|svg|webp|ico|bmp)$/i.test(url) ||
// Media
/\.(mp4|webm|ogg|mp3|wav|flac|aac|mov|avi|mkv)$/i.test(url) ||
mimeType.includes('video') ||
mimeType.includes('audio') ||
// Manifest
/\.(json|xml|webmanifest)$/i.test(url) ||
url.includes('manifest')
);
}
/**
* Helper: Get MIME type from URL
*/
getMimeTypeFromUrl(url) {
const extension = url.split('.').pop().toLowerCase();
const mimeTypes = {
js: 'application/javascript',
css: 'text/css',
html: 'text/html',
json: 'application/json',
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
svg: 'image/svg+xml',
woff: 'font/woff',
woff2: 'font/woff2',
ttf: 'font/ttf',
};
return mimeTypes[extension] || 'application/octet-stream';
}
/**
* Toggle logger open/closed
*/
toggleLogger() {
this.state.isOpen = !this.state.isOpen;
if (this.state.isOpen) {
this.elements.content.classList.add('bsp_logger_open');
this.elements.networkTraceContainer.classList.add('bsp_logger_open');
this.elements.chevron.classList.add('bsp_logger_open');
this.elements.header.classList.add('bsp_logger_expanded');
// Show expanded-only buttons and mini chart
const expandedOnlyButtons = this.elements.container.querySelectorAll(
'.bsp_logger_expanded_only'
);
expandedOnlyButtons.forEach(btn => (btn.style.display = 'flex'));
// Show mini chart when expanded
this.elements.miniChart.style.display = 'block';
} else {
this.elements.content.classList.remove('bsp_logger_o