Skip to content

Blog

How to Debug React and Next.js Apps with AI Using Gasoline MCP

React and Next.js applications have a unique set of debugging challenges — hydration mismatches, stale closures, useEffect dependency bugs, SSR/client divergence, and API route failures. Your AI coding assistant can fix all of these faster if it can actually see your browser.

Here’s how Gasoline MCP gives your AI the runtime context it needs to debug React and Next.js apps effectively.

What Makes React/Next.js Debugging Different

Section titled “What Makes React/Next.js Debugging Different”

React errors are notoriously unhelpful:

Uncaught Error: Minified React error #418

Even in development mode, React errors like “Cannot update a component while rendering a different component” don’t tell you which component or what triggered the update. And Next.js adds its own layer of complexity:

  • Hydration mismatches — server HTML differs from client render
  • SSR errors — server-side code fails but the page looks fine on the client
  • API route failures/api/* routes return 500s that the client silently swallows
  • Middleware issues — redirects and rewrites that happen before the page loads
  • Client/server boundary confusion"use client" and "use server" scope mistakes

Your AI assistant can read your source code, but without browser data it can’t see what’s actually happening at runtime.

observe({what: "errors"})

Your AI sees every console error with the full message, stack trace, and source file location. For minified builds, Gasoline resolves source maps — so even in production, the AI sees the original component name and line number.

Most React bugs involve data:

observe({what: "error_bundles"})

Error bundles return each error with its correlated context — the network requests that happened around the same time, the user actions that preceded it, and relevant console logs. One call gives the AI the complete picture:

  • The error: TypeError: Cannot read properties of undefined (reading 'map')
  • The API call: GET /api/products → 200, but the response body was { products: null } instead of { products: [] }
  • The user action: Clicked “Load More” button

The AI immediately knows: the API returned null where the component expected an array.

For race conditions and ordering issues:

observe({what: "timeline"})

The timeline shows actions, network requests, and errors in chronological order. This reveals:

  • Components that fetch data before mounting
  • Effects that fire in unexpected order
  • Network requests that resolve after the component unmounts

Symptom: “Text content does not match server-rendered HTML” or “Hydration failed because the initial UI does not match.”

observe({what: "errors"})

The AI sees the hydration warning with the mismatched content. Common causes:

  • Using Date.now() or Math.random() during render (different on server vs client)
  • Checking window or localStorage during initial render
  • Conditional rendering based on typeof window !== 'undefined'

The AI can find the component, identify the non-deterministic code, and move it into a useEffect or behind a suppressHydrationWarning.

Symptom: A feature silently fails. No error in the UI, but the data is wrong.

observe({what: "network_bodies", url: "/api"})

The AI sees every API route call with the full request and response body. A 500 response from /api/checkout with {"error": "STRIPE_KEY is undefined"} tells the AI exactly what’s wrong — an environment variable isn’t set.

Symptom: The component re-renders endlessly, or an effect doesn’t fire when it should.

observe({what: "network_waterfall", url: "/api"})

If an effect with a missing dependency is refetching on every render, the waterfall shows dozens of identical API calls in rapid succession. The AI sees the pattern and checks the effect’s dependency array.

Symptom: “Can’t perform a React state update on an unmounted component.”

observe({what: "timeline", include: ["actions", "errors", "network"]})

The timeline shows: user navigates away → API call from the previous page resolves → state update on the now-unmounted component. The AI adds cleanup logic to the effect.

Symptom: Page transitions feel sluggish.

observe({what: "vitals"})
observe({what: "performance"})

The AI checks INP (responsiveness) and long tasks. If client-side navigation triggers heavy re-renders, the performance snapshot shows the blocking time. The AI can suggest React.memo, useMemo, code splitting, or moving work to a Web Worker.

Server components run on the server and stream HTML to the client. Errors in server components don’t always appear in the browser console.

observe({what: "network_bodies", url: "/"})

The response body for a Next.js page includes the serialized server component tree. If a server component throws, the error boundary HTML is visible in the response.

Next.js middleware runs before the page loads. If a redirect or rewrite misbehaves:

observe({what: "network_waterfall"})

The waterfall shows every request including redirects (301, 307, 308). The AI can see if middleware is redirecting to the wrong URL or creating redirect loops.

Next.js <Image> component can cause CLS if dimensions aren’t right:

observe({what: "vitals"}) // Check CLS
configure({action: "query_dom", selector: "img"}) // Check image dimensions

After adding a new dependency:

observe({what: "network_waterfall"})
observe({what: "performance"})

The network summary shows total JavaScript transfer size. If it jumped from 300KB to 800KB, the waterfall identifies which new bundles appeared.

You: “The product page is broken — it shows a blank screen after I click ‘Add to Cart’.”

The AI:

  1. Calls observe({what: "error_bundles"}) — sees a TypeError: Cannot read properties of undefined (reading 'quantity') correlated with POST /api/cart → 201 that returned {item: {id: 5}} (no quantity field)

  2. Reads the cart component — finds cartItem.quantity.toString() without null checking

  3. Checks the API route — finds the response omits quantity for new items (it defaults to 1 on the backend but isn’t serialized)

  4. Fixes both: adds quantity to the API response and adds a fallback in the component

  5. Calls interact({action: "refresh"}) then observe({what: "errors"}) — confirms zero errors

Total time: 3 minutes. No manual DevTools inspection. No reproducing the bug by clicking through the UI.

Use error_bundles as your first call. It returns errors with their network and action context in one shot — faster than calling errors, then network_bodies, then actions separately.

Check the waterfall after deploys. New React bundles, changed chunk names, and different loading order are all visible in the network waterfall. The AI spots unexpected changes immediately.

Profile page transitions. Use interact({action: "navigate", url: "/products"}) to trigger a client-side navigation. The perf_diff shows the performance impact of that navigation including any heavy re-renders.

For SSR issues, check response bodies. The HTML response for a Next.js page contains the server-rendered markup. If something is wrong on the server side, it’s visible in the network body before hydration even starts.

How to Debug WebSocket Connections in 2026

WebSocket debugging in Chrome DevTools is painful. You get a flat list of frames, no filtering, no search, no way to correlate messages with application state, and if you close the tab, everything is gone.

For real-time applications — chat, live dashboards, collaborative editors, trading platforms — you need better tools. Here’s the modern approach using AI-assisted debugging.

The Problem with DevTools WebSocket Debugging

Section titled “The Problem with DevTools WebSocket Debugging”

Open Chrome DevTools, go to the Network tab, filter by WS, click on your connection, and look at the Messages tab. That’s the entire experience. Here’s what’s missing:

No filtering by message type. If your WebSocket sends 10 message types (chat, typing indicators, presence updates, notifications), you can’t filter to just one. You scroll through hundreds of messages hunting for the one you need.

No directional filtering. You can’t show only incoming or only outgoing messages without reading every row.

No correlation. When a WebSocket message causes an error, there’s no link between the Network tab and the Console tab. You’re manually matching timestamps.

No persistence. Navigate away or refresh, and the WebSocket data is gone. You can’t compare messages across page loads.

No AI access. Even if you find the problematic message, you can’t easily get it to your AI assistant. You’re back to copy-pasting.

With Gasoline MCP, your AI can observe WebSocket traffic directly, filter it, correlate it with errors, and diagnose issues without you touching DevTools.

observe({what: "websocket_status"})

The AI immediately knows:

  • How many WebSocket connections are open
  • Their URLs and states (connecting, open, closed, error)
  • Message rates per connection
  • Total messages sent and received
  • Inferred message schemas (if JSON)
observe({what: "websocket_events", direction: "incoming", last_n: 20})

The AI sees the actual message payloads, filtered to just what’s relevant. No scrolling through thousands of frames.

observe({what: "timeline", include: ["websocket", "errors"]})

The timeline shows WebSocket events and console errors chronologically. The AI sees: “The user_presence message arrived at 14:23:05.123, and a TypeError occurred at 14:23:05.125 — the presence handler is crashing.”

Your real-time dashboard stopped updating. No error in the console. The data just went stale.

You: “The dashboard stopped getting live updates.”

The AI calls observe({what: "websocket_status"}) and sees:

Connection ws-1: wss://api.example.com/live
State: closed
Close code: 1006 (abnormal closure)
Messages received: 3,847
Last message: 2 minutes ago

Close code 1006 means the connection dropped without a proper close handshake — likely a network interruption or server crash. The AI checks:

observe({what: "websocket_events", connection_id: "ws-1", last_n: 5})

The last messages were normal data frames, then nothing. No close frame from the server. The AI looks at the client-side reconnection logic and finds it has a bug — it tries to reconnect but uses the wrong URL after a server failover.

After a backend deploy, the chat stops working. Messages send but nothing appears.

The AI calls observe({what: "websocket_events", direction: "outgoing", last_n: 5}):

{"type": "message", "payload": {"text": "hello", "room": "general"}}

Then observe({what: "websocket_events", direction: "incoming", last_n: 5}):

{"type": "error", "code": "INVALID_PAYLOAD", "message": "missing field: channel"}

The backend renamed room to channel but the frontend still sends room. The AI finds the mismatch, updates the frontend, and the chat works again.

The page slows down when connected to the WebSocket. CPU usage spikes.

observe({what: "websocket_status"})
Connection ws-2: wss://api.example.com/stream
State: open
Incoming rate: 340 msg/sec
Total messages: 48,291

340 messages per second is flooding the client. The AI checks:

observe({what: "vitals"})

INP is 890ms — the main thread is completely blocked processing messages. The AI looks at the message handler, finds it’s updating React state on every message (triggering a re-render 340 times per second), and refactors it to batch updates with requestAnimationFrame or useDeferredValue.

WebSocket connections fail immediately after a deploy.

observe({what: "websocket_events", last_n: 10})

Shows open followed immediately by close with code 1008 (policy violation). The AI checks the server’s WebSocket authentication — the new deploy requires a different auth token format, but the client is sending the old format.

The most powerful pattern: combining WebSocket data with error tracking.

observe({what: "error_bundles"})

Error bundles include WebSocket events in the correlation window. When a WebSocket message triggers a JavaScript error, the AI sees both together:

  • Error: TypeError: Cannot read properties of undefined (reading 'user')
  • Correlated WebSocket message: {"type": "presence_update", "data": null} (arrived 50ms before the error)
  • User action: None (this was server-pushed)

The AI knows the server sent a presence_update with null data, and the handler doesn’t check for null. One fix: add a null guard in the handler. Better fix: also fix the server so it doesn’t send null presence data.

Real-time features are everywhere in 2026:

  • AI chat interfaces with streaming responses
  • Collaborative editing (Notion, Figma, Google Docs style)
  • Live dashboards and monitoring
  • Multiplayer applications
  • Real-time notifications

These applications live and die by their WebSocket connections. A dropped connection means lost messages. A format change means silent failures. A flooding server means frozen UIs.

DevTools hasn’t evolved to match. The WebSocket debugging experience in Chrome is fundamentally the same as it was in 2018. Meanwhile, applications have moved from “we have one WebSocket for notifications” to “we have five WebSocket connections handling different data streams.”

AI-assisted debugging — where the AI can filter, correlate, and diagnose WebSocket issues programmatically — is the first real advancement in WebSocket debugging in years.

  1. Install Gasoline (Quick Start)
  2. Open your real-time application
  3. Ask your AI: “Show me all active WebSocket connections and their status.”

Your AI calls observe({what: "websocket_status"}) and you’re debugging WebSockets without opening DevTools.

How to Fix Slow Web Vitals with AI Using Gasoline MCP

Your Core Web Vitals are red. LCP is 4.2 seconds. CLS is 0.35. Google Search Console is sending angry emails. Lighthouse gives you a list of suggestions, but they’re generic — “reduce unused JavaScript” doesn’t tell you which JavaScript or why it’s slow.

Here’s how to use Gasoline MCP to give your AI assistant real-time performance data, so it can identify exactly what’s wrong and fix it.

The Problem with Traditional Performance Tools

Section titled “The Problem with Traditional Performance Tools”

Lighthouse runs a synthetic test on a throttled connection. It’s useful for benchmarking but disconnects from your actual development experience:

  • It’s a snapshot, not real-time — you fix something, re-run Lighthouse, wait 30 seconds, check the score, repeat
  • Suggestions are generic — “eliminate render-blocking resources” doesn’t tell you which stylesheet is the problem
  • No before/after — you can’t easily compare metrics across changes
  • No correlation — it doesn’t connect slow performance to specific code changes or network requests

Gasoline solves all four problems.

observe({what: "vitals"})

Your AI gets the real numbers immediately:

MetricValueRating
FCP2.1sneeds_improvement
LCP4.2spoor
CLS0.35poor
INP280msneeds_improvement

No waiting for Lighthouse. No throttled simulation. These are the real metrics from your real browser on your real page.

observe({what: "performance"})

This returns everything — not just vitals, but the full diagnostic picture:

Navigation timing: TTFB, DomContentLoaded, Load event — shows where time is spent during page load.

Network summary by type: How many scripts, stylesheets, images, and fonts loaded. Total transfer size and decoded size per category. Your AI can immediately see “you’re loading 2.1MB of JavaScript across 47 files.”

Slowest requests: The top resources by duration. If a single API call takes 3 seconds, it shows up here.

Long tasks: JavaScript execution that blocks the main thread for more than 50ms. The count, total blocking time, and longest task. If INP is bad, this is where you find out why.

LCP measures when the main content becomes visible. Common causes of slow LCP:

High TTFB: If time_to_first_byte is over 800ms, the server is the bottleneck. The AI checks your server code, database queries, or caching configuration.

Render-blocking resources: The network waterfall shows which scripts and stylesheets load before content paints:

observe({what: "network_waterfall"})

The AI looks for CSS and JavaScript files with early start_time and long duration. These are the render-blocking resources. The fix: defer non-critical scripts, inline critical CSS, use media attributes on non-essential stylesheets.

Large hero images: If the LCP element is an image, the performance snapshot shows its transfer size. A 2MB uncompressed PNG as the hero image? The AI suggests WebP, proper sizing, and fetchpriority="high".

Late-loading content: If FCP is fast but LCP is slow, the main content loads late — maybe behind an API call or a client-side render. The timeline shows the gap:

observe({what: "timeline", include: ["network"]})

CLS measures visual stability. Things that cause layout shifts:

Images without dimensions: An <img> without width and height causes the browser to reflow when the image loads. The AI can audit your images:

configure({action: "query_dom", selector: "img"})

Dynamic content insertion: Ads, banners, or lazy-loaded content that pushes existing content down. The timeline shows when shifts happen relative to network requests.

Font loading: Web fonts that cause text to resize. The AI checks for font-display: swap or font-display: optional in your CSS.

CSS without containment: The AI can check if your dynamic containers use contain: layout or explicit dimensions.

INP measures the worst-case responsiveness to user input. If INP is high, the main thread is busy when the user interacts.

Long tasks are the smoking gun: The performance snapshot shows total blocking time and the longest task. If you have 800ms of blocking time from 12 long tasks, the AI knows exactly what to target.

Heavy event handlers: The AI can read your click and input handlers to find expensive operations (DOM manipulation, synchronous computation, large state updates) that should be deferred or moved to a Web Worker.

Third-party scripts: The network waterfall shows which third-party scripts are loading and how long their execution takes:

observe({what: "third_party_audit"})

A third-party analytics script running 200ms of JavaScript on every page load directly impacts INP.

This is where Gasoline shines. After the AI makes a change:

interact({action: "refresh"})

Gasoline automatically captures before and after performance snapshots and computes a diff. The result includes:

  • Per-metric comparison: LCP went from 4200ms to 2800ms (-33%, improved, rating: needs_improvement)
  • Resource changes: “Removed analytics-v2.js (180KB), resized bundle.js from 450KB to 320KB”
  • Verdict: “improved” — more metrics got better than worse

The AI says: “LCP improved from 4.2s to 2.8s after removing the synchronous analytics script. CLS dropped from 0.35 to 0.08 after adding image dimensions. INP is still 250ms — let me look at the long tasks.”

No re-running Lighthouse. No waiting. Instant feedback.

If INP is the remaining problem, profile the actual interactions:

interact({action: "click", selector: "text=Load More", analyze: true})

The analyze: true parameter captures before/after performance around that specific click. The AI sees exactly how much main-thread time that button click consumes.

When you’re done optimizing:

generate({format: "pr_summary"})

This produces a before/after performance summary suitable for your pull request description — showing stakeholders exactly what improved and by how much.

Here’s a real workflow condensed:

Initial vitals: LCP 5.1s, CLS 0.42, INP 380ms

AI diagnosis:

  1. Network waterfall shows 3.2MB of JavaScript across 62 requests
  2. TTFB is 1.8s — slow API call blocks server-side rendering
  3. Five images without width/height attributes cause CLS
  4. Long tasks total 1.2s of blocking time — mostly from a charting library initializing synchronously

AI fixes:

  1. Adds loading="lazy" to below-fold charts, defers non-critical scripts → JS drops to 1.4MB initial
  2. Adds Redis caching to the slow API endpoint → TTFB drops to 200ms
  3. Adds explicit dimensions to all images → CLS drops to 0.02
  4. Wraps chart initialization in requestIdleCallback → blocking time drops to 180ms

Final vitals: LCP 1.9s (good), CLS 0.02 (good), INP 150ms (good)

Total time: One conversation, about 20 minutes. Each fix was verified immediately with perf_diff.

LighthouseGasoline
Speed30s synthetic run per checkReal-time, instant
ComparisonManual before/afterAutomatic perf_diff
DiagnosisGeneric suggestionsYour actual bottlenecks
Fix cycleRun → fix → re-run → checkFix → refresh → see diff
ContextScore and suggestionsFull waterfall, timeline, long tasks
IntegrationSeparate toolSame terminal as your AI assistant

Lighthouse tells you your LCP is 4.2 seconds and suggests “reduce unused JavaScript.” Gasoline tells your AI that analytics-v2.js (180KB) loads synchronously in the head, blocks FCP by 800ms, and can be deferred without breaking anything.

Set budgets in .gasoline.json to catch regressions automatically:

{
"budgets": {
"default": {
"lcp_ms": 2500,
"cls": 0.1,
"inp_ms": 200,
"total_transfer_kb": 500
},
"routes": {
"/": { "lcp_ms": 2000 },
"/dashboard": { "lcp_ms": 3000, "total_transfer_kb": 800 }
}
}
}

When any metric exceeds its budget, the AI gets an alert. Regressions are caught during development, not after deploy.

  1. Install Gasoline and connect your AI tool (Quick Start)
  2. Navigate to your slowest page
  3. Ask: “What are the Web Vitals for this page, and what’s causing the worst ones?”

Your AI sees the numbers, identifies the bottlenecks, and starts fixing. Real metrics, real fixes, real-time feedback.

One Tool Replaces Four: How Gasoline MCP Eliminates Loom, DevTools, Selenium, and Playwright

Most development teams juggle at least four tools to ship a feature: Loom for demos and bug reports, Chrome DevTools for debugging, Selenium or Playwright for automated testing, and some combination of all three for QA. Each tool has its own setup, its own learning curve, and its own context switch.

Gasoline MCP replaces all four with a single Chrome extension and one MCP server. And the result isn’t just fewer tools — it’s dramatically faster cycle times.

Loom — “Let Me Show You What’s Happening”

Section titled “Loom — “Let Me Show You What’s Happening””

Product managers record Loom videos to demo features. Developers record Loom videos to show bugs. QA records Loom videos to document test failures. Everyone records Loom videos because the alternative — writing a detailed description with screenshots — takes even longer.

The problem: Loom videos are static. They can’t be replayed against a new build. They can’t be edited when the flow changes. They can’t be version-controlled. And they require $12.50/user/month.

Chrome DevTools — “Let Me Check the Console”

Section titled “Chrome DevTools — “Let Me Check the Console””

Every debugging session starts with opening DevTools, switching between Console, Network, and Elements tabs, copying error messages, and pasting them somewhere the AI or another developer can see them.

The problem: DevTools is manual and disconnected. The AI can’t see what’s in DevTools. You’re the human bridge between the browser and your tools.

Selenium / WebDriver — “Let Me Automate This”

Section titled “Selenium / WebDriver — “Let Me Automate This””

Automated browser testing requires WebDriver binaries, a programming language (Java, Python, JavaScript), and coded selectors that break whenever the UI changes.

The problem: High setup cost, high maintenance cost, requires developer skills. Product managers and QA without coding experience can’t use it.

Playwright — “Let Me Write a Proper Test”

Section titled “Playwright — “Let Me Write a Proper Test””

Modern browser automation that’s better than Selenium but still requires JavaScript/TypeScript, an npm project, and coded selectors.

The problem: Same fundamental issue — you need code to create tests. And when tests break (they always break), you need code to fix them.

Instead of recording a video:

"Navigate to the dashboard. Add a subtitle: 'Welcome to the Q1 report.'
Click the revenue tab. Subtitle: 'Revenue is up 23% quarter over quarter.'
Click the export button. Subtitle: 'One click to export to PDF.'"

The AI navigates the application while displaying narration text at the bottom of the viewport — like closed captions. Action toasts show what’s happening (“Click: Revenue Tab”). The audience watches a live, narrated walkthrough.

Why it’s better than Loom:

  • Replayable — run the same script against tomorrow’s build
  • Editable — change one line of text, not re-record a whole video
  • Adaptive — semantic selectors survive UI redesigns
  • Versionable — store scripts in your repo, diff them in PRs
  • Free — no per-seat subscription

Instead of opening DevTools and copy-pasting:

"What browser errors do you see?"

The AI calls observe({what: "errors"}) and sees every console error with full stack traces. Then observe({what: "network_bodies", url: "/api"}) for the API response body. Then observe({what: "websocket_status"}) for WebSocket connection state. Then observe({what: "vitals"}) for performance metrics.

Why it’s better than DevTools:

  • The AI sees it directly — no human copy-paste bridge
  • Everything in one place — errors, network, WebSocket, performance, accessibility, security
  • Correlatederror_bundles returns the error with its network context and user actions
  • Persistent — data doesn’t vanish on page refresh
  • Actionable — the AI diagnoses and fixes, not just observes

Selenium → interact() + Natural Language

Section titled “Selenium → interact() + Natural Language”

Instead of writing Java with WebDriver:

"Go to the registration page. Fill in 'Jane Doe' as the name,
'jane@example.com' as the email, and 'secure123' as the password.
Click Register. Verify you see the welcome message."

The AI navigates, types, clicks, and verifies — using semantic selectors (label=Name, text=Register) that survive UI changes.

Why it’s better than Selenium:

  • No code — describe the test in English
  • No setup — no WebDriver, no JDK, no project scaffolding
  • Resilient — semantic selectors adapt to redesigns
  • Anyone can use it — PMs, QA, designers, not just developers

Playwright → generate(format: “test”)

Section titled “Playwright → generate(format: “test”)”

After running a natural language test, lock it in for CI:

generate({format: "test", test_name: "registration-flow",
assert_network: true, assert_no_errors: true})

Gasoline generates a complete Playwright test from the session — real selectors, network assertions, error checking. The AI explored in English; Gasoline exports for CI/CD.

Why it’s better than writing Playwright by hand:

  • Faster — describe the flow, don’t code it
  • Accurate — generated from real browser behavior, not guessed
  • Maintainable — when the test breaks, re-run in English and regenerate

The Compound Effect: Radical Cycle Time Reduction

Section titled “The Compound Effect: Radical Cycle Time Reduction”

Replacing four tools isn’t just about having fewer subscriptions. It’s about what happens when demo, debug, test, and automate are the same workflow.

  1. PM records a Loom showing the desired feature (10 minutes)
  2. Developer watches the Loom, opens DevTools, starts building (context switch)
  3. Developer debugs in DevTools, copies errors, pastes to AI, gets suggestions (context switch)
  4. Developer writes Playwright tests for the feature (30-60 minutes)
  5. QA records a Loom of a bug they found (10 minutes)
  6. Developer watches the Loom, reproduces, opens DevTools again (context switch)
  7. Developer fixes and re-runs tests (context switch)
  8. PM records another Loom for the stakeholder demo (10 minutes)

Four tools. Six context switches. Half the time spent on ceremony instead of building.

  1. PM describes the feature to the AI: “The user should be able to export the report as PDF”
  2. AI builds the feature, debugging in real time — it sees errors as they happen, fixes them, verifies with observe({what: "errors"}), checks performance with observe({what: "vitals"})
  3. AI generates a test: generate({format: "test", test_name: "pdf-export"})
  4. AI runs the demo with subtitles for the stakeholder
  5. If QA finds a bug, the AI already has the error context — observe({what: "error_bundles"}) — and fixes it in the same session
  6. AI regenerates the test if the fix changed the flow

One tool. Zero context switches. The cycle from “PM describes feature” to “tested, demo-ready feature” happens in a single conversation.

Activity4-Tool CycleGasoline Cycle
Feature demo (PM)10 min Loom recording0 — AI demos with subtitles
Debugging20 min (DevTools + copy-paste)2 min (AI observes directly)
Test creation30-60 min (Playwright)2 min (generate from session)
Bug report10 min Loom + reproduce1 min (AI already has context)
Bug fix verification5 min (re-run tests)30 sec (refresh + observe)
Stakeholder demo10 min (new Loom)1 min (replay demo script)
Total85-115 min~7 min

That’s not an incremental improvement. It’s an order of magnitude.

Product velocity isn’t about how fast you type. It’s about how fast you can go from “idea” to “shipped and verified.” Every context switch adds latency. Every tool boundary adds friction. Every manual step adds error.

When demo, debug, test, and automate collapse into a single AI conversation:

  • Feedback loops tighten — the AI sees the result of every change in real time
  • Iteration cost drops — trying a different approach is a sentence, not a sprint
  • Quality increases — tests are generated from real behavior, not written from memory
  • Everyone participates — PMs can demo, test, and file bugs without developer involvement

This is what AI-native development looks like. Not “AI helps you write code faster” — but “AI collapses the entire build-debug-test-demo cycle into minutes.”

The one remaining advantage Loom has over Gasoline is shareability — you can send a Loom link to anyone with a browser. Gasoline’s demo scripts require the AI to replay them.

The fix: tab recording. Chrome’s tabCapture API can record the active tab as video while the AI runs a demo script. Subtitles and action toasts are already rendered in the page, so they’d be captured automatically. The output: a narrated demo video, generated from a replayable script, with burned-in captions. No Loom subscription. No manual recording. No re-takes.

That feature is on the roadmap. When it ships, the Loom replacement is complete.

You don’t need four tools. You need one browser extension, one MCP server, and an AI that can see your browser.

Loom → Gasoline subtitles + demo scripts (+ tab recording, coming soon) Chrome DevTools → Gasoline observe() Selenium → Gasoline interact() + natural language Playwright → Gasoline generate(format: “test”)

One install. Zero subscriptions. Faster than all four combined.

Get started →

Subresource Integrity (SRI) Explained: Protect Your Site from CDN Compromise

Every <script src="https://cdn.example.com/library.js"> on your page is a trust decision. You’re trusting that the CDN will always serve the exact file you expect. If the CDN is compromised, hacked, or serves a corrupted file, your users execute the attacker’s code.

Subresource Integrity (SRI) eliminates this risk. Here’s how it works and how to implement it.

In 2018, a cryptocurrency mining script was injected into the British Airways website through a compromised third-party script. In 2019, Magecart attacks hit thousands of e-commerce sites through CDN compromises. In 2021, the ua-parser-js npm package was hijacked to serve malware.

The attack pattern is always the same:

  1. Attacker compromises a CDN, package registry, or hosting provider
  2. The script content changes (malware added, data exfiltration code injected)
  3. Every website loading that script from that CDN now serves the attacker’s code
  4. Users’ data is stolen, credentials harvested, or cryptocurrency mined

SRI prevents step 3. Even if the CDN is compromised, the browser refuses to execute the modified script.

SRI adds a cryptographic hash to your <script> and <link> tags:

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"
integrity="sha384-OYp56H6p7T3JKjPdRfL7gAHEdJ7yrfCCe3Ew5EHouvE7qdLCHs6PGoGMOQ/pA6j"
crossorigin="anonymous"></script>

When the browser downloads the file, it computes the SHA-384 hash of the content and compares it to the integrity attribute. If they don’t match — because the file was modified, corrupted, or replaced — the browser refuses to execute it.

Format: algorithm-base64hash

integrity="sha384-OYp56H6p7T3JKjPdRfL7gAHEdJ7yrfCCe3Ew5EHouvE7qdLCHs6PGoGMOQ/pA6j"

Supported algorithms:

  • SHA-256sha256-...
  • SHA-384sha384-... (recommended — good balance of security and performance)
  • SHA-512sha512-...

You can include multiple hashes for fallback:

integrity="sha384-abc... sha256-xyz..."

The browser accepts the resource if any hash matches.

SRI requires CORS. Add crossorigin="anonymous" to cross-origin scripts:

<script src="https://cdn.example.com/lib.js"
integrity="sha384-..."
crossorigin="anonymous"></script>

Without crossorigin, the browser can’t compute the hash for cross-origin resources and SRI silently fails.

ThreatSRI Protection
CDN compromise (attacker modifies hosted files)Blocks modified scripts
Man-in-the-middle attacks (on HTTP resources)Blocks tampered resources
CDN serving wrong versionBlocks unexpected content
Package registry hijacking (modified npm package)Blocks modified bundles
DNS hijacking (CDN domain points to attacker)Blocks attacker’s response
ThreatWhy SRI Doesn’t Help
First-party script compromiseSRI is for third-party resources
XSS via inline scriptsUse CSP for inline script protection
Version pinningSRI verifies content, not version semantics
AvailabilityIf the CDN is down, SRI can’t make it work
Terminal window
# Generate a hash for a local file
cat library.js | openssl dgst -sha384 -binary | openssl base64 -A
# Generate a hash for a remote file
curl -s https://cdn.example.com/library.js | openssl dgst -sha384 -binary | openssl base64 -A

Gasoline observes which third-party scripts and stylesheets your page loads and generates SRI hashes automatically:

generate({format: "sri"})

Output per resource:

  • URL — the resource location
  • Hashsha384-... in browser-standard format
  • Ready-to-use HTML tag<script> or <link> with integrity and crossorigin attributes
  • File size — for reference
  • Already protected — flags resources that already have SRI

Filter to specific resource types or origins:

generate({format: "sri", resource_types: ["script"]})
generate({format: "sri", origins: ["https://cdn.jsdelivr.net"]})

The output is copy-paste ready:

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"
integrity="sha384-OYp56H6p7T3JKjPdRfL7gAHEdJ7yrfCCe3Ew5EHouvE7qdLCHs6PGoGMOQ/pA6j"
crossorigin="anonymous"></script>

Replace your existing tags with integrity-protected versions:

<!-- Before -->
<script src="https://cdn.example.com/chart.js"></script>
<!-- After -->
<script src="https://cdn.example.com/chart.js"
integrity="sha384-abc123..."
crossorigin="anonymous"></script>

Use the webpack-subresource-integrity plugin:

const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');
module.exports = {
output: { crossOriginLoading: 'anonymous' },
plugins: [new SubresourceIntegrityPlugin()]
};

Vite doesn’t generate SRI natively. Use a plugin or add integrity attributes to your HTML template for third-party CDN scripts.

For scripts loaded via <Script> component, add the integrity prop:

<Script
src="https://cdn.example.com/analytics.js"
integrity="sha384-abc123..."
crossOrigin="anonymous"
/>

If a CDN updates the file content (even whitespace changes), the hash won’t match and the browser blocks the script.

Fix: Pin to specific versions (lodash@4.17.21 not lodash@latest) and update hashes when you upgrade versions.

Some CDNs serve different content based on the User-Agent header (e.g., minified vs unminified). This means the hash differs across browsers.

Fix: Use CDNs that serve consistent content regardless of User-Agent. Gasoline warns you when it detects Vary: User-Agent on a resource.

If your framework dynamically creates <script> elements, you’ll need to add integrity attributes programmatically:

const script = document.createElement('script');
script.src = 'https://cdn.example.com/lib.js';
script.integrity = 'sha384-abc123...';
script.crossOrigin = 'anonymous';
document.head.appendChild(script);

Service workers can modify responses, which breaks SRI. If you’re using a service worker that caches CDN resources, ensure it passes through the original response without modification.

SRI and CSP work together:

  • CSP restricts which origins can load resources
  • SRI verifies the content of resources from allowed origins

CSP alone: an attacker compromises an allowed CDN → your CSP still allows the compromised script.

SRI alone: an attacker injects a <script> from a new origin → SRI doesn’t help because the new tag doesn’t have an integrity attribute.

Both together: CSP blocks scripts from unauthorized origins, and SRI blocks modified scripts from authorized origins. The attacker needs to compromise your specific CDN and produce content that matches the hash — which is cryptographically impossible.

  1. Browse your app with Gasoline connected
  2. Generate SRI hashes: generate({format: "sri"})
  3. Add integrity attributes to your third-party script and link tags
  4. Test — verify all resources load correctly
  5. Regenerate when you update third-party library versions

Yes, if you load scripts or stylesheets from third-party CDNs. The implementation cost is minimal (add two attributes to each tag) and the protection against supply chain attacks is significant.

Skip it for first-party resources served from your own domain. SRI adds value when you don’t control the server — if you control both the page and the resource server, CSP provides sufficient protection.

The combination of SRI for third-party resources and CSP for all resources gives you defense in depth against the most common web supply chain attacks.