From 9c6c8ef35163550bcdc8d47cd3389e064ce8c394 Mon Sep 17 00:00:00 2001 From: rahul-aot Date: Sat, 21 Feb 2026 22:45:45 +0530 Subject: [PATCH 1/3] feat: Implement initial TestPilot browser extension with session management, issue detection, and popup UI. --- src/background/main.ts | 9 ++- src/background/sessionManager.ts | 20 +++++-- src/background/severityEngine.ts | 2 - src/content/bridge.ts | 74 +++++++++--------------- src/content/main.ts | 98 ++++++++++++++++++++++++-------- src/core/types.ts | 4 +- src/popup/popup.css | 53 +++++++++++++++++ src/popup/popup.tsx | 57 +++++++++++++++---- 8 files changed, 220 insertions(+), 97 deletions(-) diff --git a/src/background/main.ts b/src/background/main.ts index 92a58ae..be2ca36 100644 --- a/src/background/main.ts +++ b/src/background/main.ts @@ -8,14 +8,13 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message.action === 'TELEMETRY_EVENT') { EventProcessor.processEvent(message.payload); } else if (message.action === 'GET_SESSION_STATUS') { - SessionManager.isActive().then((active) => { - sendResponse({ active }); + SessionManager.getCurrentSession().then((session) => { + sendResponse({ active: !!session, config: session?.config }); }); return true; // async } else if (message.action === 'START_SESSION') { - const { envData } = message.payload || {}; - SessionManager.startSession(envData).then((session) => { - notifyTabs('SESSION_STARTED'); + SessionManager.startSession(message.payload).then((session) => { + notifyTabs('SESSION_STARTED', { config: session.config }); sendResponse({ session }); }); return true; diff --git a/src/background/sessionManager.ts b/src/background/sessionManager.ts index 93d0d5e..31762f7 100644 --- a/src/background/sessionManager.ts +++ b/src/background/sessionManager.ts @@ -2,8 +2,9 @@ import { StorageService } from './storage.ts'; import type { Session } from '../core/types.ts'; export class SessionManager { - static async startSession(envData?: any): Promise { + static async startSession(payload?: { envData?: any, config?: any }): Promise { const sessionId = crypto.randomUUID(); + const { envData, config } = payload || {}; const session: Session = { sessionId, startTime: Date.now(), @@ -14,10 +15,21 @@ export class SessionManager { url: location.href, platform: (navigator as any).platform }, - config: { + config: config || { slowApiThreshold: 1000, - longTaskThreshold: 200, - escalationThreshold: 10 + escalationThreshold: 10, + enabledTypes: { + runtime_crash: true, + console_error: true, + console_log: false, + network_failure: true, + slow_api: true, + retry_storm: true, + resource_failure: true, + cors_failure: true, + security_risk: true, + white_screen: true + } } }; await StorageService.saveSession(session); diff --git a/src/background/severityEngine.ts b/src/background/severityEngine.ts index 8337221..2e32227 100644 --- a/src/background/severityEngine.ts +++ b/src/background/severityEngine.ts @@ -19,11 +19,9 @@ export class SeverityEngine { return 'high'; case 'slow_api': - case 'long_task': return 'medium'; case 'console_log': - case 'route_change': return 'low'; default: diff --git a/src/content/bridge.ts b/src/content/bridge.ts index d1f5b63..f903725 100644 --- a/src/content/bridge.ts +++ b/src/content/bridge.ts @@ -59,6 +59,7 @@ const startTime = Date.now(); let url = ''; let method = 'GET'; + let requestBody: any = null; try { if (typeof args[0] === 'string') { @@ -70,19 +71,30 @@ method = args[0].method; } - if (args[1] && args[1].method) { - method = args[1].method; + if (args[1]) { + if (args[1].method) method = args[1].method; + if (args[1].body) requestBody = args[1].body; } const response = await originalFetch(...args); const duration = Date.now() - startTime; if (response.status >= 400 || duration > 1000) { + let responseBody = ''; + try { + const clone = response.clone(); + responseBody = await clone.text(); + } catch (e) { + responseBody = '[Unable to read body]'; + } + emit('network_event', { url, method, status: response.status, - duration + duration, + requestPayload: requestBody ? (typeof requestBody === 'string' ? requestBody : '[Non-string body]') : null, + responseBody: responseBody.substring(0, 2000) }); } @@ -91,7 +103,8 @@ emit('network_error', { url, method, - message: (error as Error).message + message: (error as Error).message, + requestPayload: requestBody ? (typeof requestBody === 'string' ? requestBody : '[Non-string body]') : null }); throw error; } @@ -102,6 +115,7 @@ private _method?: string; private _url?: string; private _startTime?: number; + private _requestBody?: any; open(method: string, url: string | URL, ...args: any[]) { this._method = method; @@ -111,6 +125,7 @@ } send(body?: Document | XMLHttpRequestBodyInit | null) { + this._requestBody = body; this.addEventListener('load', () => { const duration = Date.now() - (this._startTime || 0); if (this.status >= 400 || duration > 1000) { @@ -118,7 +133,9 @@ url: this._url, method: this._method, status: this.status, - duration + duration, + requestPayload: this._requestBody ? (typeof this._requestBody === 'string' ? this._requestBody : '[Non-string body]') : null, + responseBody: (this.responseText || '').substring(0, 2000) }); } }); @@ -126,7 +143,8 @@ emit('network_error', { url: this._url, method: this._method, - message: 'XHR Error' + message: 'XHR Error', + requestPayload: this._requestBody ? (typeof this._requestBody === 'string' ? this._requestBody : '[Non-string body]') : null }); }); return super.send(body); @@ -152,47 +170,9 @@ }); }); - // SPA Route Change Tracking - const wrapHistory = (method: string) => { - const original = (history as any)[method]; - return function (this: History, ...args: any[]) { - const result = original.apply(this, args); - emit('route_change', { - url: window.location.href, - method, - title: args[0] - }); - return result; - }; - }; - history.pushState = wrapHistory('pushState'); - history.replaceState = wrapHistory('replaceState'); - window.addEventListener('popstate', () => { - emit('route_change', { - url: window.location.href, - method: 'popstate' - }); - }); - // Performance: Long Task Detection - if ('PerformanceObserver' in window) { - try { - const observer = new PerformanceObserver((list) => { - list.getEntries().forEach((entry) => { - if (entry.duration > 200) { // Threshold: 200ms - emit('long_task', { - duration: entry.duration, - name: entry.name, - startTime: entry.startTime - }); - } - }); - }); - observer.observe({ entryTypes: ['longtask'] }); - } catch (e) { - console.warn('[TestPilot] PerformanceObserver not supported for longtask'); - } - } + + // Broken Resource Detection window.addEventListener('error', (event) => { @@ -220,6 +200,4 @@ return originalSetItem.apply(this, [key, value]); }; - const context = window.self === window.top ? 'Main Window' : 'IFrame'; - console.log(`[TestPilot] Injected bridge active in ${context} with Advanced Monitoring`); })(); diff --git a/src/content/main.ts b/src/content/main.ts index 2157ac4..b02c46e 100644 --- a/src/content/main.ts +++ b/src/content/main.ts @@ -4,6 +4,29 @@ // Content Script Loaded +// Helper to safely send messages to background +function safeSendMessage(message: any, callback?: (response: any) => void) { + try { + if (chrome.runtime?.id) { + if (callback) { + chrome.runtime.sendMessage(message, (response) => { + if (chrome.runtime.lastError) { + // Context likely invalidated, ignore + return; + } + callback(response); + }); + } else { + chrome.runtime.sendMessage(message).catch(() => { + // Context likely invalidated, ignore + }); + } + } + } catch (e) { + // Extension context invalidated, ignore + } +} + // Always listen for bridge messages, regardless of when it's injected window.addEventListener('message', (event) => { if (event.data?.source === 'testpilot-bridge') { @@ -14,17 +37,43 @@ window.addEventListener('message', (event) => { function injectBridge() { if (document.getElementById('testpilot-bridge')) return; - const script = document.createElement('script'); - script.id = 'testpilot-bridge'; - script.src = chrome.runtime.getURL('src/bridge/bridge.js'); - (document.head || document.documentElement).appendChild(script); + try { + if (!chrome.runtime?.id) return; + const script = document.createElement('script'); + script.id = 'testpilot-bridge'; + script.src = chrome.runtime.getURL('src/bridge/bridge.js'); + (document.head || document.documentElement).appendChild(script); + } catch (e) { + // Context invalidated + } } let isMonitoring = false; +let sessionConfig: any = null; function handleBridgeEvent(type: string, data: any) { if (!isMonitoring) return; + // Filter by type if config is available + if (sessionConfig?.enabledTypes) { + // Map common internal types to IssueType if they differ + let issueType: string = type; + if (type === 'network_event' || type === 'network_error') { + const isError = type === 'network_error' || (data.status && data.status >= 400); + issueType = isError ? 'network_failure' : 'slow_api'; + } else if (type === 'runtime_error') { + issueType = 'runtime_crash'; + } else if (type === 'resource_failure') { + issueType = 'resource_failure'; + } else if (type === 'security_risk') { + issueType = 'security_risk'; + } + + if (sessionConfig.enabledTypes[issueType] === false) { + return; + } + } + let payload: any = { type, url: window.location.href, @@ -59,7 +108,7 @@ function handleBridgeEvent(type: string, data: any) { setTimeout(() => { const contentLen = document.body?.innerText?.length || 0; if (contentLen < 50) { - chrome.runtime.sendMessage({ + safeSendMessage({ action: 'TELEMETRY_EVENT', payload: { type: 'white_screen', @@ -70,46 +119,45 @@ function handleBridgeEvent(type: string, data: any) { }); } }, 1000); - } else if (type === 'long_task') { - payload.type = 'long_task'; - payload.message = `UI Thread frozen for ${Math.round(data.duration)}ms`; - payload.metadata = data; + } else if (type === 'resource_failure') { payload.type = 'resource_failure'; payload.message = `Failed to load ${data.tagName}: ${data.url}`; payload.metadata = data; - } else if (type === 'route_change') { - payload.type = 'route_change'; - payload.message = `Navigation: ${data.method} to ${data.url}`; - payload.metadata = data; } - chrome.runtime.sendMessage({ action: 'TELEMETRY_EVENT', payload }); + safeSendMessage({ action: 'TELEMETRY_EVENT', payload }); } -function enableMonitors() { +function enableMonitors(config?: any) { isMonitoring = true; + sessionConfig = config; injectBridge(); } function disableMonitors() { isMonitoring = false; + sessionConfig = null; } // 1. Check initial state -chrome.runtime.sendMessage({ action: 'GET_SESSION_STATUS' }, (response: any) => { - if (chrome.runtime.lastError) return; +safeSendMessage({ action: 'GET_SESSION_STATUS' }, (response: any) => { if (response && response.active) { - enableMonitors(); + enableMonitors(response.config); } }); // 2. Listen for Command changes -chrome.runtime.onMessage.addListener((message: any) => { - if (message.action === 'SESSION_STARTED') { - enableMonitors(); - } else if (message.action === 'SESSION_STOPPED') { - disableMonitors(); - // Session ended - check popup for report +try { + if (chrome.runtime?.id) { + chrome.runtime.onMessage.addListener((message: any) => { + if (message.action === 'SESSION_STARTED') { + enableMonitors(message.config); + } else if (message.action === 'SESSION_STOPPED') { + disableMonitors(); + } + }); } -}); +} catch (e) { + // Context invalidated +} diff --git a/src/core/types.ts b/src/core/types.ts index 34be406..2e7bfbc 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -5,11 +5,9 @@ export type IssueType = | 'network_failure' | 'slow_api' | 'retry_storm' - | 'long_task' | 'resource_failure' | 'cors_failure' | 'security_risk' - | 'route_change' | 'white_screen'; export type IssueLevel = 'critical' | 'high' | 'medium' | 'low'; @@ -45,8 +43,8 @@ export interface Session { }; config?: { slowApiThreshold: number; - longTaskThreshold: number; escalationThreshold: number; + enabledTypes: Record; }; } diff --git a/src/popup/popup.css b/src/popup/popup.css index 525ee56..4883548 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -124,6 +124,37 @@ body { box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } +.monitor-types-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + background: white; + padding: 16px; + border: 1.5px solid #e2e8f0; + border-radius: 8px; +} + +.monitor-type-item { + display: flex; + align-items: center; + gap: 8px; +} + +.monitor-type-item input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; +} + +.monitor-type-item label { + font-size: 13px; + color: #1e293b; + text-transform: capitalize; + letter-spacing: normal; + font-weight: 500; + cursor: pointer; +} + .history-state { background: #f8fafc; max-height: 480px; @@ -513,6 +544,28 @@ body { font-weight: 500; } +.issue-body-detail { + margin-top: 8px; + padding: 8px; + background: rgba(0, 0, 0, 0.03); + border-radius: 4px; + font-family: 'SF Mono', 'Monaco', monospace; + font-size: 10px; + color: #475569; + word-break: break-all; + white-space: pre-wrap; + max-height: 80px; + overflow-y: auto; +} + +.issue-body-detail strong { + color: #1e293b; + text-transform: uppercase; + font-size: 9px; + display: block; + margin-bottom: 2px; +} + .no-issues { text-align: center; color: #94a3b8; diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index eebc651..5b02720 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -16,8 +16,19 @@ function App() { const [showSettings, setShowSettings] = useState(false); const [config, setConfig] = useState({ slowApiThreshold: 1000, - longTaskThreshold: 200, - escalationThreshold: 10 + escalationThreshold: 10, + enabledTypes: { + runtime_crash: true, + console_error: true, + console_log: false, + network_failure: true, + slow_api: true, + retry_storm: true, + resource_failure: true, + cors_failure: true, + security_risk: true, + white_screen: true + } }); useEffect(() => { @@ -206,6 +217,16 @@ function App() { {issue.metadata?.duration && (
{issue.metadata.duration}ms โ€ข {issue.metadata.status || 'Error'}
)} + {issue.metadata?.requestPayload && ( +
+ Request Payload: {issue.metadata.requestPayload} +
+ )} + {issue.metadata?.responseBody && ( +
+ Response Body: {issue.metadata.responseBody} +
+ )} ))} @@ -244,14 +265,7 @@ function App() { onChange={(e) => setConfig({ ...config, slowApiThreshold: parseInt((e.target as HTMLInputElement).value) })} /> -
- - setConfig({ ...config, longTaskThreshold: parseInt((e.target as HTMLInputElement).value) })} - /> -
+
setConfig({ ...config, escalationThreshold: parseInt((e.target as HTMLInputElement).value) })} />
+ +
+ +
+ {Object.entries(config.enabledTypes).map(([type, enabled]) => ( +
+ setConfig({ + ...config, + enabledTypes: { + ...config.enabledTypes, + [type]: (e.target as HTMLInputElement).checked + } + })} + /> + +
+ ))} +
+
From 4b714f33fc5caecc5e54a77aa4a2bde73a4f93a9 Mon Sep 17 00:00:00 2001 From: rahul-aot Date: Sat, 21 Feb 2026 23:24:06 +0530 Subject: [PATCH 2/3] feat: establish core browser extension with session management, issue monitoring, and reporting features via a new popup UI. --- README.md | 225 ++++++++++++++++++++++++++++---------- src/background/main.ts | 61 ++++++++--- src/background/storage.ts | 13 +++ src/content/main.ts | 10 ++ src/popup/popup.css | 191 +++++++++++++++++++++++++++++++- src/popup/popup.tsx | 133 +++++++++++++++++----- 6 files changed, 526 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index f99f863..f63cad6 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,197 @@ # ๐Ÿงช TestPilot -**Intelligent Bug Detection & Telemetry Extension for Modern Web Apps** - -TestPilot is a powerful, production-grade Chrome extension designed to catch bugs before your users do. It acts as a black-box flight recorder for your web application, capturing console errors, network failures, slow APIs, performance metrics, and security risks in real-time. - -![TestPilot Banner](https://via.placeholder.com/800x200?text=TestPilot+Extension) - -## โœจ Key Features - -- **๐Ÿš€ Real-time Telemetry**: Captures `console.error`, `console.warn`, unhandled exceptions, and promise rejections. -- **๐ŸŒ Network Intelligence**: - - Detects **Slow APIs** (>1000ms by default) - - Identifies **Retry Storms** (rapid repeated failures) - - Captures **CORS Errors** and HTTP 4xx/5xx failures -- **โšก Performance Monitoring**: - - **Long Task Detection**: Flags UI freezes (>200ms) - - **White Screen Detection**: Alerts on potential rendering crashes -- **๐Ÿ›ก๏ธ Security Scanner**: - - Detects sensitive data leaks (JWTs, API keys, PII) in console/storage - - Monitors unsafe storage access -- **๐Ÿ“ฑ Smart Context**: Captures environment details (User Agent, Viewport, Route Changes) for actionable bug reports. -- **๐Ÿง  Adaptive Severity**: Automatically escalates issue severity based on frequency (e.g., repeating errors become Critical). - -## ๐Ÿ› ๏ธ Usage - -1. **Install the Extension** (Developer Mode): - - Clone this repo - - Run `npm install` and `npm run build` - - Open `chrome://extensions` - - Enable "Developer mode" - - Click "Load unpacked" and select the `dist` folder - -2. **Start a Session**: - - Click the extension icon - - Hit **Start Session** - - Interact with your web application - - TestPilot records all hidden issues in the background - -3. **Analyze & Export**: - - Open the popup to see a categorized list of issues - - Use **Filters** to focus on Critical/High severity bugs - - Click **Export JSON** or **Export Markdown** to generate a bug report +**Intelligent Bug Detection & Telemetry Chrome Extension for Modern Web Apps** + +TestPilot is a production-grade Chrome extension that acts as a silent flight recorder for your web application. It captures bugs, network failures, security risks, and performance issues in real-time โ€” before your users report them. + +--- + +## โœจ Features + +### ๐Ÿ–ฅ๏ธ Console Monitoring +- Intercepts `console.error` and `console.warn` calls from deep inside the page (via a **Main World bridge script** that runs before any other JS) +- Captures the **caller file, line, and column** from the stack trace automatically +- Passes every console message through the **Security Scanner** before logging + +### ๐ŸŒ Network Intelligence +- Wraps both **`window.fetch`** and **`XMLHttpRequest`** to intercept all outgoing requests +- Captures **HTTP 4xx / 5xx** failures as `network_failure` issues +- Detects **Slow APIs** โ€” requests exceeding the configurable threshold (default: 1000ms) +- Detects **API Retry Storms** โ€” automatically escalates to `retry_storm` (Critical) when the same endpoint fails 3+ times within 5 seconds +- Records **request payload**, **response status**, and **response body** (up to 2000 chars) for every failing or slow request + +### โšก Runtime Crash Detection +- Listens for `window.onerror` to catch **unhandled JavaScript exceptions** with full file/line/column/stack metadata +- Listens for `window.onunhandledrejection` to catch **unhandled Promise rejections** + +### ๐Ÿ“ฆ Broken Resource Detection +- Listens on the capture phase for failed loads of ``, `