Architecture Overview
Inspector Lab is a browser extension DevTools inspector designed for web debugging across desktop and tablet browsers. This page describes the system architecture, key components, and how they interact.
System Architecture
Key Components
Background Service Worker (background.ts)
The background script runs as the extension's central hub. Its message listener is registered at module scope—before any other initialization—to ensure it remains responsive even if later APIs fail to initialize.
Core responsibilities:
- Message routing: Handles all
chrome.runtime.onMessagecalls from the inspector UI and content scripts, dispatching to handlers for PING, EVALUATE, TRACK_INSPECTOR, FETCH_SOURCE, GET_COOKIES, SET_COOKIE, DELETE_COOKIE, CLEAR_SITE_COOKIES, GET_NETWORK_DETAILS, INTERCEPT_CONSOLE, and INTERCEPT_NETWORK operations - Prehook registration: Registers console and network prehooks via
chrome.scripting.registerContentScriptsatdocument_startin the MAIN world for each origin that has granted per-site host permission; prehooks capture console and network events before any page script runs - Network interception: Listens to
chrome.webRequestevents (onSendHeaders, onCompleted, onErrorOccurred) to maintain a headers-only log of HTTP requests for inspected tabs (capped at 500 entries per tab; oldest entries are dropped when exceeded) - Cookie operations: Bridges access to
chrome.cookiesAPI for read, write, and delete operations; validates cookie identity and handles permission checks - Session tracking: Uses
chrome.storage.sessionwith in-memoryMapfallback to track which tabs have the inspector open (keyed by tab ID) and which panel is active per origin; in-memory fallback is essential on browsers like Orion for iOS that lack or partially implement the session storage API - Source fetching: Retrieves page sources (HTML, CSS, JavaScript) with a 60 KB per-file limit, matching the Sources panel's display cap; capped previews are shown at 2 KB
- Cookie retrieval at scale: Fetches all cookies via
chrome.cookies.getAll()when the user grants the optional all-hosts permission, or returns an empty set otherwise
The message listener is registered at the very first line to ensure it stays responsive even if later initialization fails. This prevents silent failures where panels go completely mute if APIs like chrome.storage.session or chrome.webRequest are unavailable. Each per-message handler has its own try-catch, so individual operations can fail gracefully without silencing the listener.
Storage and origin-based keys:
openTab:{tabId}→ origin of the tab with an open inspectorpanel:{origin}→ name of the active panel tab for that origin- Prehook registration is keyed by
inspector-lab-prehook:{origin}and is checked before re-registering to avoid duplicate content scripts
Diagnostic logging:
The background worker installs a global error handler via installGlobalDiagnostics() so uncaught errors are logged to persistent diagnostics, readable from the popup—the only console available on iPad.
Inspector Entry Point (inspector-entry.tsx)
The main React application mounted into a shadow-DOM host on every inspected page:
- UI frame management: Maintains the inspector window as floating or docked (bottom, left, right) with resize and drag support
- Panel coordination: Houses a six-panel tab interface (Elements, Console, Sources, Network, Cookies, Storage)
- State synchronization: Bridges between the panel UI and the page/background via the messaging protocol
- Device support: Handles both desktop (floating/docked windows) and tablet (sheet-like UI) layouts
- Theme management: Applies theming from settings and syncs color-scheme preferences
- Reload persistence: Re-injects itself after page reloads when the inspector was open (not closed)
Page Bridge
A lightweight transport for code that needs to run in the page's MAIN world when the background cannot be reached:
- Evaluation fallback: Runs JavaScript expressions in MAIN context via injected scripts when
chrome.scripting.executeScriptis unavailable (e.g., under strict Content Security Policy) - Capture hooks: Connects console and network interceptors to the page's event system as a fallback to prehook registration
Message Flow
Typical Request–Response
- Panel action (user clicks "Evaluate" in Console)
- Panel sends runtime message via
sendRuntimeMessage()to background - Background receives via
chrome.runtime.onMessage - Background processes (e.g., calls
chrome.cookies.getAll()) - Background sends response back to panel
- Panel updates UI with the result
Console & Network Capture
Early capture requires code injected before page scripts run:
- On inspector open, background calls
ensurePrehookRegistered()to register console and network prehooks viachrome.scripting.registerContentScriptswithrunAt: "document_start"andworld: "MAIN"(registration is skipped if the origin lacks host permission) - Prehooks install hooks into
window.__inspectorLabConsoleHookandwindow.__inspectorLabNetworkHookto capture events - Hooks buffer events until the inspector sends INTERCEPT_CONSOLE_MESSAGE or INTERCEPT_NETWORK_MESSAGE with a unique event name
- Background flushes buffered events into the page by dispatching custom events and maintains the event channel open
- Page code dispatches events on document, caught by a content-script listener that forwards them back to the panel
If prehooks cannot be registered (no per-site host permission granted), console and network capture starts at the next page reload instead — an acceptable degradation since users must opt in to full functionality. Diagnostic logging records when permissions are unavailable.
Storage & Persistence
Session Storage
Used to track which tabs have the inspector open and remember the active panel per origin:
- Key:
openTab:{tabId}→ value: origin - Key:
panel:{origin}→ value: active tab name - Implementation: Native
chrome.storage.sessionwith in-memoryMapfallback for browsers that don't support it (e.g., Orion on iOS); the in-memory mirror is hydrated on service-worker start by reading all keys and re-hydrated on each message, so reload persistence survives service-worker restarts as long as the tab remains open
Network Log
A per-tab Map in the background tracks HTTP requests captured by chrome.webRequest listeners:
- Size limit: 500 entries per tab (oldest dropped when exceeded)
- Data: Capped to headers only (status, URL, method, request/response headers, timing); full request bodies are lazily fetched via FETCH_SOURCE_MESSAGE only when the user inspects a row
- Lifetime: Lost on service-worker restart; panel treats this the same as pre-inspector history
Settings
User preferences (color scheme, custom theme, panel layout) are stored in chrome.storage.local and watched for changes so the UI stays in sync across tabs.
Error Handling & Fallbacks
The architecture gracefully degrades when APIs are unavailable:
| Failure | Fallback |
|---|---|
chrome.storage.session missing or failing | In-memory Map for session tracking; reload persistence works for the lifetime of the tab |
chrome.webRequest unsupported | Network panel falls back to fetch/XHR hooks from page (via prehooks if registered, or page bridge if CSP-blocked) |
chrome.scripting.executeScript blocked | Page bridge runs evaluation via injected script in the page's MAIN world |
chrome.permissions.contains unavailable | Prehook registration is skipped; console/network capture starts at next reload |
| Background unreachable | Cookie operations use document.cookie (read-only); evaluation falls back to page bridge |
Each degradation is logged diagnostically and, where appropriate, surfaced in the UI so users understand why functionality is limited.
Cross-World Communication
Inspector Lab operates across three JavaScript worlds:
| World | Role | Capabilities |
|---|---|---|
| Isolated (Content Script) | Inspector UI host | Access to chrome.* APIs; cannot touch page globals |
| MAIN (Page Context) | Hooks & evaluation | Full page access; no extension APIs |
| Service Worker | Background relay | All extension APIs; long-lived relay |
Communication between worlds uses:
chrome.runtime.sendMessage(Isolated ↔ Service Worker)- Custom events (Isolated ↔ MAIN via document.dispatchEvent)
- Injected scripts (bootstrap page bridge from Isolated into MAIN)
Key Design Decisions
Why register the message listener at module scope?
Two critical APIs—chrome.storage.session and chrome.webRequest—are absent or partially implemented on some browsers (Orion for iOS). If either throws at module scope before the message listener is registered, every panel goes completely silent because there is no listener to answer. By registering the listener first, any initialization failure becomes a per-message error that the handler can catch and respond to, rather than a fatal invisible failure.
Why prehooks?
Console and network events fire before any inspector UI can attach. Prehooks installed at document_start capture them early and buffer until the inspector connects, ensuring no events are lost. Without prehooks, events that fire in the first few milliseconds are missed, leaving gaps in the console and network log.
Why in-memory session storage?
Browsers like Orion on iOS expose chrome.storage.session incompletely or not at all. An in-memory mirror lets the inspector work everywhere with a narrower (session-only) contract rather than failing silently. The mirror is hydrated on service-worker start and kept in sync by message handlers, so reload persistence survives as long as the tab is open.
Why per-tab network logging in the background?
The background's chrome.webRequest listeners run synchronously and cover all request types (images, stylesheets, fonts) that never pass through fetch/XHR. An in-memory log is fast and sufficient for debugging; full request bodies are lazily fetched only when the user inspects a row, keeping memory use low. The log is capped at 500 entries per tab to prevent unbounded growth.
Why document-scoped themes?
Inspector UI is isolated in shadow DOM but needs to respond to system dark mode and user preference changes. Theme settings are watched and re-applied on every change so the UI stays in sync even if the user toggles their system settings mid-session.
Why origin-based prehook registration?
Host permissions and prehook state are per-origin. Registering prehooks only for origins that have granted permission ensures the inspector respects user intent; deferred capture on reload (when no permission is granted) is an acceptable degradation because users must opt in to full functionality via the popup's permission request.
See also:
- Message Protocol — Full specification of all runtime messages
- Page Bridge & Injection — How code runs in the page context
- Theming & Styling — Theme resolution and design tokens