UNPKG

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
/** * 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...&#10;2. Click on...&#10;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