<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Sola Samuel</title>
    <description>The latest articles on Forem by Sola Samuel (@solasamuel).</description>
    <link>https://forem.com/solasamuel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3900889%2F6b432340-af56-4306-a697-0fbac6e43e60.png</url>
      <title>Forem: Sola Samuel</title>
      <link>https://forem.com/solasamuel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/solasamuel"/>
    <language>en</language>
    <item>
      <title>I built a Chrome extension that explains your console errors in plain English</title>
      <dc:creator>Sola Samuel</dc:creator>
      <pubDate>Sun, 10 May 2026 14:13:05 +0000</pubDate>
      <link>https://forem.com/solasamuel/i-built-a-chrome-extension-that-explains-your-console-errors-in-plain-english-o33</link>
      <guid>https://forem.com/solasamuel/i-built-a-chrome-extension-that-explains-your-console-errors-in-plain-english-o33</guid>
      <description>&lt;p&gt;Here is an error message I got last week:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning: Each child in a list should have a unique "key" prop.
Check the render method of `ProductList`.
    at li
    at ProductList (webpack-internal:///./src/components/ProductList.jsx:34:5)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I know what this error means. I've seen it a hundred times. I fixed it in thirty seconds.&lt;/p&gt;

&lt;p&gt;Here is a different error I got the same day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'map')
    at processApiResponse (api.js:847:23)
    at async fetchProducts (api.js:112:5)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did not know what that one meant. Not immediately. The function at line 847 was calling &lt;code&gt;.map()&lt;/code&gt; on something, and that something was &lt;code&gt;undefined&lt;/code&gt; when it shouldn't have been. But why? Was the API returning a different shape? A race condition? A missing null check upstream?&lt;/p&gt;

&lt;p&gt;I spent twenty minutes on that error. I Googled it, read three Stack Overflow answers that all said "just add &lt;code&gt;?.&lt;/code&gt; before the map" — technically correct but doesn't explain why the value is undefined or whether the real fix is somewhere else.&lt;/p&gt;

&lt;p&gt;I've been a developer long enough to know what &lt;code&gt;undefined&lt;/code&gt; means. The problem was the twenty minutes.&lt;/p&gt;

&lt;p&gt;So I built DebugBuddy.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;DebugBuddy is a Chrome extension that captures JavaScript errors from any tab and explains them with the Claude API. Click the extension icon, and the active tab's errors show up in a popup. Click any error, and you get a structured explanation: a summary, the likely cause, a suggested fix, and relevant docs links.&lt;/p&gt;

&lt;p&gt;That's the whole product. It is deliberately not a DevTools panel, a content script, or a paste-in-anything tool. The version that ships does one thing — capture errors from the active tab via the Chrome DevTools Protocol and explain them — and tries to do that part well.&lt;/p&gt;

&lt;p&gt;The interesting work was not the UI. The interesting work was getting the capture pipeline to behave under a few constraints that are not obvious until you hit them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why CDP, not content scripts
&lt;/h2&gt;

&lt;p&gt;The first design question was: how do you actually capture &lt;code&gt;console.error&lt;/code&gt; and uncaught exceptions from an extension?&lt;/p&gt;

&lt;p&gt;The two viable approaches are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content script + &lt;code&gt;document_start&lt;/code&gt; injection.&lt;/strong&gt; Inject a script into the page context that overrides &lt;code&gt;window.console.error&lt;/code&gt; before any page code runs. Bridge messages back to the extension via &lt;code&gt;window.postMessage&lt;/code&gt;. This is the classic approach and it works, but it has problems: the page can untrap your override, strict CSP can block the injected script, and you're racing against the page's own setup during hydration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chrome DevTools Protocol via &lt;code&gt;chrome.debugger&lt;/code&gt;.&lt;/strong&gt; Attach to the tab as a debugger client and subscribe to &lt;code&gt;Runtime.exceptionThrown&lt;/code&gt; and &lt;code&gt;Log.entryAdded&lt;/code&gt;. This is what DevTools itself uses. It can't be untrapped by page code, isn't affected by CSP, and gives you structured data — a real &lt;code&gt;stackTrace.callFrames&lt;/code&gt; array, not a message string you have to re-parse.&lt;/p&gt;

&lt;p&gt;I went with CDP. The cost is the yellow "DebugBuddy started debugging this browser" banner Chrome shows whenever a debugger is attached, which is annoying but unavoidable. The benefit is that capture is bulletproof in a way that injection just isn't.&lt;/p&gt;

&lt;p&gt;The capture loop is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/background/index.ts&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;debugger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;captured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Runtime.exceptionThrown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;captured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseExceptionThrown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Log.entryAdded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;captured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseLogEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;addError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;updateBadge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ERROR_CAPTURED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;captured&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it for capture. The rest of the work is making sure the debugger session stays attached to the right tabs, doesn't fight with anyone else, and survives Chrome's MV3 lifecycle.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three constraints that took longer than the feature
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The MV3 service worker dies. The CDP session doesn't.
&lt;/h3&gt;

&lt;p&gt;In Manifest V3, the background script is a service worker that gets killed after about thirty seconds of inactivity. When it spins back up, every in-memory variable is gone. That includes the &lt;code&gt;Set&amp;lt;number&amp;gt;&lt;/code&gt; I was using to track which tabs the extension had attached to.&lt;/p&gt;

&lt;p&gt;The wrinkle: Chrome can keep the underlying CDP session alive across a service worker restart. So you wake up, your in-memory &lt;code&gt;attachedTabs&lt;/code&gt; says "I'm not attached to anything," and you call &lt;code&gt;chrome.debugger.attach()&lt;/code&gt; — which throws, because Chrome thinks you're already attached.&lt;/p&gt;

&lt;p&gt;The fix uses two sources of truth and treats &lt;code&gt;Runtime.enable&lt;/code&gt; as a probe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/background/debugger.ts&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isReallyAttached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;debugger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTargets&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;tabId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attached&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tryEnableDomains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;debugger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tabId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Runtime.enable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;debugger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tabId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Log.enable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getTargets()&lt;/code&gt; answers "is anything attached to this tab?" but not "am I the one attached?" The CDP routing rules give you that for free: &lt;code&gt;sendCommand&lt;/code&gt; only succeeds for the client that owns the session. If &lt;code&gt;Runtime.enable&lt;/code&gt; succeeds, we're the owner. If it throws, someone else is — or the session genuinely doesn't exist.&lt;/p&gt;

&lt;p&gt;The full attach logic uses both: if our in-memory set says we're attached, try to re-enable domains; if &lt;code&gt;getTargets()&lt;/code&gt; shows something attached but we don't own it, try one more time in case Chrome held our session across the restart; otherwise fall through to a fresh &lt;code&gt;attach()&lt;/code&gt; call.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. There's no API for "is another extension attached?"
&lt;/h3&gt;

&lt;p&gt;Only one debugger client can own a tab's CDP session at a time. If the user has DevTools open, or another debugger extension running, your &lt;code&gt;attach()&lt;/code&gt; call fails with a generic "Another debugger is already attached" error.&lt;/p&gt;

&lt;p&gt;There is no API that tells you &lt;em&gt;who&lt;/em&gt; is attached. &lt;code&gt;chrome.debugger.getTargets()&lt;/code&gt; returns an &lt;code&gt;attached: boolean&lt;/code&gt; and an &lt;code&gt;extensionId&lt;/code&gt; field, but &lt;code&gt;extensionId&lt;/code&gt; is empty when native DevTools holds the session, and you can't programmatically distinguish "another extension" from "the user pressed F12."&lt;/p&gt;

&lt;p&gt;The workaround is the probe pattern from constraint #1. Try to &lt;code&gt;sendCommand&lt;/code&gt;. If it works, we already own the session and the user just opened a fresh popup. If it fails, surface a clean message — "Another debugger is already attached to this tab" — instead of letting the raw exception propagate to the UI.&lt;/p&gt;

&lt;p&gt;This came from a real bug. I had a session where DevTools was open in another window, capture silently stopped working, and the only signal was a stack trace in the service worker logs that no user would ever see.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Restricted URLs and the auto-attach race
&lt;/h3&gt;

&lt;p&gt;I wanted attaching to be invisible — when you switch tabs, the extension should attach to the new tab automatically so errors are captured from the moment you arrive. Two listeners cover that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onActivated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tabId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isRestrictedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;attachDebugger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onUpdated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;changeInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;changeInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isRestrictedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;attachDebugger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "obvious" version of this breaks immediately, because &lt;code&gt;chrome.debugger.attach()&lt;/code&gt; throws on a long list of internal URLs: &lt;code&gt;chrome://&lt;/code&gt;, &lt;code&gt;chrome-extension://&lt;/code&gt;, &lt;code&gt;devtools://&lt;/code&gt;, &lt;code&gt;about:&lt;/code&gt;, &lt;code&gt;view-source:&lt;/code&gt;, and a few others. You can't attach to the new-tab page. You can't attach to the extension's own options page. You can't attach to DevTools itself.&lt;/p&gt;

&lt;p&gt;The fix is a protocol allowlist checked before every attach attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RESTRICTED_PROTOCOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chrome://&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chrome-extension://&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edge://&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;about:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;view-source:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;devtools://&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chrome-search://&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chrome-untrusted://&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isRestrictedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;RESTRICTED_PROTOCOLS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second half of the fix is in the message handler. When the popup opens on a restricted page and asks the background to attach, returning an error is the wrong UX — there's nothing the user can do about it, and they don't care. So restricted pages return &lt;code&gt;success: true, restricted: true&lt;/code&gt;. The popup uses the &lt;code&gt;restricted&lt;/code&gt; flag to render a "this page can't be debugged" hint instead of an error.&lt;/p&gt;




&lt;h2&gt;
  
  
  The smaller details
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Deduplication is by message hash.&lt;/strong&gt; Storage groups errors using a djb2 hash of the message string. If the same error fires twice, the existing entry's &lt;code&gt;count&lt;/code&gt; is incremented and the timestamp updated, instead of pushing a new row. A 50-error-per-tab rolling buffer keeps storage bounded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is global per tab, not windowed. A render loop firing the same error 50 times per second collapses to a single row with &lt;code&gt;count: 50&lt;/code&gt;, which is the behavior I want — render loops should be visible as a high count, not a wall of identical rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explanations are cached for an hour.&lt;/strong&gt; The cache is keyed by error hash, sits in the service worker's memory, and has a 1-hour TTL. Re-clicking the same error returns the cached explanation instantly without re-billing the API. The cache is wiped on service worker restart, which is fine — the worst case is one extra API call after thirty seconds of idle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model is Claude Haiku 4.5.&lt;/strong&gt; Error explanations are short (a few hundred tokens at most), there's no benefit to a larger model, and Haiku is fast enough that the explanation feels instant when it isn't cached. The system prompt asks for a structured JSON response with &lt;code&gt;summary&lt;/code&gt;, &lt;code&gt;likelyCause&lt;/code&gt;, &lt;code&gt;suggestedFix&lt;/code&gt;, and &lt;code&gt;relevantLinks&lt;/code&gt; — which the popup parses into four sections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's BYOK.&lt;/strong&gt; The user's Claude API key is stored in &lt;code&gt;chrome.storage.local&lt;/code&gt; and sent directly to &lt;code&gt;api.anthropic.com&lt;/code&gt; from the service worker, with the &lt;code&gt;anthropic-dangerous-direct-browser-access&lt;/code&gt; header set. Nothing routes through any server I run. The options page validates the key by sending a tiny &lt;code&gt;ping&lt;/code&gt; message before saving it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd build differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The yellow banner is a tax on every user.&lt;/strong&gt; Chrome shows "DebugBuddy started debugging this browser" whenever the debugger is attached, and there's no way to suppress it. A future version might detach during long quiet periods and re-attach lazily, but the heuristics are messy — you don't want to miss the first error after a quiet stretch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-tab CDP sessions don't survive navigation cleanly.&lt;/strong&gt; A full-page navigation can trigger &lt;code&gt;onDetach&lt;/code&gt;, requiring a re-attach. The current code handles this via &lt;code&gt;tabs.onUpdated&lt;/code&gt; with &lt;code&gt;status === "loading"&lt;/code&gt;, but it's racy — if the new page throws an error in the first 50ms before re-attach completes, that error is lost. A more robust approach would use &lt;code&gt;webNavigation.onBeforeNavigate&lt;/code&gt; to pre-arm the attach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's no DevTools panel.&lt;/strong&gt; A dedicated panel would let users see explanations alongside the Console tab without opening the popup, and it would dodge the yellow-banner problem because DevTools is already attached. It's the most-requested feature and it's on the roadmap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Chrome Web Store:&lt;/strong&gt; [&lt;a href="https://chromewebstore.google.com/detail/debugbuddy/cccnbldheilinhcljcmililgalpialig?hl=en-GB&amp;amp;utm_source=ext_sidebar" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/debugbuddy/cccnbldheilinhcljcmililgalpialig?hl=en-GB&amp;amp;utm_source=ext_sidebar&lt;/a&gt;]&lt;br&gt;
&lt;strong&gt;GitHub (MIT):&lt;/strong&gt; [&lt;a href="https://github.com/solasamuel/debugbuddy" rel="noopener noreferrer"&gt;https://github.com/solasamuel/debugbuddy&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;You'll need a Claude API key — paste it into the options page once. New Anthropic accounts get a small starting credit; after that, expect a fraction of a cent per explanation at current Haiku 4.5 pricing.&lt;/p&gt;

&lt;p&gt;The most interesting file in the repo is &lt;code&gt;src/background/debugger.ts&lt;/code&gt; — the attach/probe logic for constraints #1 and #2 is all there in about 100 lines.&lt;/p&gt;




&lt;p&gt;The error that started this took me twenty minutes. With DebugBuddy, the same error would take thirty seconds. The twenty minutes wasn't because I'm a bad developer — it was because the tooling made me do unnecessary work.&lt;/p&gt;

&lt;p&gt;I'm curious what errors you hit most often. Drop them in the comments — I want to make sure the explanations are good for the errors that matter, not just the ones I happen to encounter in my own work.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a browser extension that reviews your PRs before your teammates do</title>
      <dc:creator>Sola Samuel</dc:creator>
      <pubDate>Mon, 27 Apr 2026 17:59:05 +0000</pubDate>
      <link>https://forem.com/solasamuel/i-built-a-browser-extension-that-reviews-your-prs-before-your-teammates-do-1odk</link>
      <guid>https://forem.com/solasamuel/i-built-a-browser-extension-that-reviews-your-prs-before-your-teammates-do-1odk</guid>
      <description>&lt;p&gt;There's a specific kind of dread that comes with opening a large pull request you've been assigned to review.&lt;/p&gt;

&lt;p&gt;You click the link. The diff loads. You scroll. Three hundred changed files. No description. The PR title says "fix stuff." The author is already in another country on a flight.&lt;/p&gt;

&lt;p&gt;You've got no idea where to start.&lt;/p&gt;

&lt;p&gt;I've been on both sides of this — the reviewer who has to piece together intent from raw diffs, and the author who forgets to explain anything because it all seems obvious from inside the work. After one too many PRs where I spent more time understanding the change than actually reviewing it, I built something to fix the cold-start problem.&lt;/p&gt;

&lt;p&gt;It's called &lt;strong&gt;GitBrief&lt;/strong&gt;. It injects an AI-powered sidebar into every GitHub PR page that tells you what the PR does before you read a single line of diff.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with code review as it exists
&lt;/h2&gt;

&lt;p&gt;Code review has a fundamental asymmetry. The author has been inside the problem for hours or days. They know which files matter, which approach they tried first and abandoned, which edge cases they had to handle. The reviewer has none of that. They open the diff cold and have to reverse-engineer the intent from the changes.&lt;/p&gt;

&lt;p&gt;This creates three failure modes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Superficial reviews.&lt;/strong&gt; Reviewers skim, approve, and move on because the overhead of actually understanding the PR is too high.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long review delays.&lt;/strong&gt; PRs sit unreviewed for days because the cognitive cost of starting is high enough that other tasks win the prioritisation battle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missed issues.&lt;/strong&gt; A reviewer who hasn't understood what the PR is trying to do can't evaluate whether it's doing it correctly.&lt;/p&gt;

&lt;p&gt;None of this is anyone's fault. It's structural. The tooling doesn't help reviewers orient quickly.&lt;/p&gt;




&lt;h2&gt;
  
  
  What GitBrief does
&lt;/h2&gt;

&lt;p&gt;When you open a GitHub PR, GitBrief injects a collapsible sidebar that gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PR Summary&lt;/strong&gt; — 3-5 sentences on what the PR does and why, streamed progressively as soon as the page loads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk Assessment&lt;/strong&gt; — flags missing tests, sensitive file changes (auth, .env, payment files), large diff warnings, and anything else worth flagging before you dive in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suggested Review Comments&lt;/strong&gt; — 3-5 specific comments mapped to actual files, not generic advice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complexity Score&lt;/strong&gt; — a 1-10 rating with a short rationale based on lines changed, files touched, number of reviewers, and branch age&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explain This File&lt;/strong&gt; — click any file in the diff view to get a plain-English breakdown of what changed in it specifically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sidebar streams progressively. The summary appears first, then the risk flags, then the suggestions — so you're reading useful context within a few seconds of opening the page, not waiting 15 seconds for a spinner to resolve.&lt;/p&gt;

&lt;p&gt;It works on Chrome and Firefox from a single TypeScript codebase.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building it: the technical problems worth talking about
&lt;/h2&gt;

&lt;p&gt;Most of the extension logic is straightforward. The interesting parts are three specific challenges that look easy until they aren't.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. GitHub doesn't reload pages between PRs
&lt;/h3&gt;

&lt;p&gt;GitHub uses Turbolinks-style navigation. When you click from one PR to another, the page doesn't do a full reload — it swaps the content via JavaScript. This means a naive &lt;code&gt;document.ready&lt;/code&gt; event listener fires exactly once and then never again.&lt;/p&gt;

&lt;p&gt;The extension needs to detect every PR page navigation, not just the first one, and re-inject the sidebar each time. It also needs to remove the previous sidebar before injecting a new one, or you end up with duplicates.&lt;/p&gt;

&lt;p&gt;The correct approach is a &lt;code&gt;MutationObserver&lt;/code&gt; on &lt;code&gt;document.body&lt;/code&gt; watching for &lt;code&gt;childList&lt;/code&gt; changes, combined with intercepting the browser history API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalPushState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pushState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pushState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;originalPushState&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;handleNavigation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;popstate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleNavigation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MutationObserver&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isPRPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;sidebarExists&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;injectSidebar&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;childList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subtree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;isPRPage()&lt;/code&gt; check uses URL pattern matching (&lt;code&gt;/^\/[^/]+\/[^/]+\/pull\/\d+/&lt;/code&gt;). The &lt;code&gt;sidebarExists()&lt;/code&gt; check prevents duplicate injection when the observer fires multiple times during a single navigation.&lt;/p&gt;

&lt;p&gt;I spent longer than I'd like to admit getting this right. The MutationObserver fires a lot — GitHub makes dozens of DOM changes during navigation — so the callbacks need to be cheap and idempotent.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Large diffs will eat your API budget
&lt;/h3&gt;

&lt;p&gt;GitHub PRs can have diffs with 50,000+ lines across hundreds of files. Sending everything to Claude is both expensive and slow.&lt;/p&gt;

&lt;p&gt;The chunking strategy has three layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File prioritisation.&lt;/strong&gt; Not all files are equally important to review. The extension sorts files before sending them to Claude:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auth files, env files, payment-related files → highest priority&lt;/li&gt;
&lt;li&gt;Core business logic → high priority
&lt;/li&gt;
&lt;li&gt;Tests → low priority (important, but a missing test shows up in the risk assessment)&lt;/li&gt;
&lt;li&gt;Config files, package.json → lowest priority&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Truncation.&lt;/strong&gt; Any file over 400 lines is truncated to its first 200 lines and last 200 lines, with a &lt;code&gt;[N lines truncated]&lt;/code&gt; marker in the middle. For most files, the beginning and end contain the information you need — the class/function signatures, the main logic, and the return values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two-pass approach for very large PRs.&lt;/strong&gt; For PRs that are still too large after truncation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send the file list only (names, sizes, types) and ask Claude which files are most worth looking at&lt;/li&gt;
&lt;li&gt;Send only those files in full&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This keeps costs predictable and response times under 10 seconds for almost any PR.&lt;/p&gt;

&lt;p&gt;Results are cached in &lt;code&gt;chrome.storage.session&lt;/code&gt; keyed by &lt;code&gt;{owner}/{repo}/{pr}/{commit_sha}&lt;/code&gt;, so navigating back to a PR you've already analysed is instant.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Streaming from a background service worker to a content script
&lt;/h3&gt;

&lt;p&gt;The Claude API supports Server-Sent Events for streaming responses, which is how the sidebar updates progressively. But there's an architectural wrinkle: in a browser extension, the Claude API call happens in the background service worker (which has network access), and the sidebar UI lives in the content script (which doesn't). You need to relay the stream between them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chrome.runtime.sendMessage&lt;/code&gt; is one-shot — you send a message, you get a response, connection closes. That doesn't work for a stream.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chrome.runtime.connect&lt;/code&gt; creates a long-lived port that stays open, which is what you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In the content script — opens a port to the background worker&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;analysis-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chunk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;appendToSidebar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;analyse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;extractedDiff&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In the background service worker — relays SSE chunks to the port&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onConnect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;analyse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chunk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sidebar renders summary, risk, and suggestions as separate sections, so each one can appear as soon as Claude has finished generating it — the user is reading the summary while the risk assessment is still being generated.&lt;/p&gt;




&lt;h2&gt;
  
  
  The thing about building for GitHub specifically
&lt;/h2&gt;

&lt;p&gt;One thing I didn't anticipate: GitHub's UI is very... opinionated about external injections. The extension has to be careful not to interfere with existing event handlers, not to pollute the global namespace, and not to add layout that breaks GitHub's responsive behaviour.&lt;/p&gt;

&lt;p&gt;The sidebar is absolutely positioned relative to the viewport, not the PR content area, so it doesn't affect the diff layout. It's also collapse-by-default on mobile-width viewports so it doesn't eat the screen.&lt;/p&gt;

&lt;p&gt;There's also the question of keeping up with GitHub's DOM changes. They ship UI updates regularly and don't publish a changelog for their HTML structure. The extension uses structural selectors and data attributes rather than class names (which GitHub obfuscates) wherever possible, but you still need to test after major GitHub UI updates.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The v1 feature set covers the core use case. The v2 roadmap I'm thinking about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review checklist&lt;/strong&gt; — per-repo customisable checklist injected into the sidebar. "Did you update the docs?", "Is there a migration script?", "Have you tested on staging?" — the things teams always forget to check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale PR detector&lt;/strong&gt; — badge the PR list view with age warnings for PRs that have been open longer than a team-defined threshold. PRs that sit too long tend to get merged with less scrutiny, not more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team mode&lt;/strong&gt; — shared review templates and history across an org, so teams can build up a library of common review patterns.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Chrome Web Store:&lt;/strong&gt; [&lt;a href="https://chromewebstore.google.com/detail/ikifklfoigcncmigejeamnlopbhlbefh?utm_source=item-share-cb" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/ikifklfoigcncmigejeamnlopbhlbefh?utm_source=item-share-cb&lt;/a&gt;]&lt;br&gt;&lt;br&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; [&lt;a href="https://github.com/solasamuel/gitbrief" rel="noopener noreferrer"&gt;https://github.com/solasamuel/gitbrief&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;You need a Claude API key — enter it once in the settings popup and it persists across devices. The free tier handles most PRs without any cost.&lt;/p&gt;




&lt;p&gt;The code review cold-start problem is real and fixable. I've been using GitBrief on my own workflow for a few weeks now and it's changed how I approach large PRs — I read the sidebar first, then go to the files I actually need to look at, rather than scrolling the entire diff trying to build a mental model.&lt;/p&gt;

&lt;p&gt;If you build something on top of it or find a bug, open an issue. And if you have opinions about what v2 should prioritise, I'd genuinely like to know — drop them in the comments.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
