<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[TabQA]]></title><description><![CDATA[TabQA]]></description><link>https://tabqa.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6aa10524dd111ac41fc3f3a3/68ee9648-ed28-4076-a155-9c3695ea8716.png</url><title>TabQA</title><link>https://tabqa.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 15 Sep 2026 23:20:34 GMT</lastBuildDate><atom:link href="https://tabqa.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to Test Android Devices and Intercept WebView Traffic Directly in Chrome (Without Local ADB)]]></title><description><![CDATA[If you have ever tested mobile apps on Android, you are likely familiar with the friction of the traditional toolchain:

You plug in the phone and launch desktop mirroring tools like Scrcpy or Vysor;
]]></description><link>https://tabqa.hashnode.dev/how-to-test-android-devices-and-intercept-webview-traffic-directly-in-chrome-without-local-adb</link><guid isPermaLink="true">https://tabqa.hashnode.dev/how-to-test-android-devices-and-intercept-webview-traffic-directly-in-chrome-without-local-adb</guid><category><![CDATA[chrome extension]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Android]]></category><dc:creator><![CDATA[Lijiawei]]></dc:creator><pubDate>Wed, 09 Sep 2026 07:27:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aa10524dd111ac41fc3f3a3/d4e179c4-18cf-4144-8c45-08493d0799d5.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you have ever tested mobile apps on Android, you are likely familiar with the friction of the traditional toolchain:</p>
<ol>
<li>You plug in the phone and launch desktop mirroring tools like Scrcpy or Vysor;</li>
<li>You open a terminal to stream <code>adb logcat</code> and manually grep for errors;</li>
<li>If you need to inspect HTTPS traffic in an embedded WebView or hybrid app, you configure Charles or Fiddler, setup Wi-Fi proxies, and struggle with Android 7+ (API 24+) rejecting user-installed CA root certificates;</li>
<li>When an intermittent crash or ANR occurs, you scramble to take screenshots, clip screen recordings, align timestamps with logs, and assemble everything into Jira or Notion.</li>
</ol>
<p>A recurring question arose in our engineering team:<br /><strong>Can we consolidate device mirroring, log filtering, network traffic interception, and bug evidence collection natively inside the browser—without requiring local ADB binaries, drivers, or desktop proxy software?</strong></p>
<p>This article explores the technical implementation of such a workspace, leveraging the <strong>WebUSB API</strong> for hardware communication and the <strong>Chrome DevTools Protocol (CDP)</strong> for certificate-free network interception, based on our open-source project <strong><a href="https://github.com/openutx/TabQA">TabQA</a></strong>.</p>
<hr />
<h2>1. Direct Hardware Communication: Implementing ADB over WebUSB</h2>
<p>Traditional Android debugging relies on an ADB server process listening on host port <code>5037</code>. In multi-developer environments or non-technical QA machines, this often results in SDK configuration hurdles or <code>adb server version doesn't match this client</code> conflicts.</p>
<p>Modern Chromium browsers provide the <strong>WebUSB API</strong>, enabling secure user-authorized contexts to communicate directly with physical USB bulk endpoints.</p>
<h3>1.1 Device Filtering and Interface Claiming</h3>
<p>According to the Android Open Source Project (AOSP) specification, the ADB interface defines a fixed descriptor:</p>
<ul>
<li><code>bInterfaceClass</code>: <code>0xff</code> (Vendor Specific)</li>
<li><code>bInterfaceSubClass</code>: <code>0x42</code> (ADB)</li>
<li><code>bInterfaceProtocol</code>: <code>0x01</code></li>
</ul>
<p>In the browser, we request the device via permission prompts:</p>
<pre><code class="language-javascript">// Filter and request Android devices matching ADB interface descriptors
const device = await navigator.usb.requestDevice({
  filters: [{
    classCode: 0xff,
    subclassCode: 0x42,
    protocolCode: 0x01
  }]
});

await device.open();
await device.selectConfiguration(1);

// Locate and claim the ADB interface
const adbInterface = device.configuration.interfaces.find(iface =&gt;
  iface.alternates.some(alt =&gt; alt.interfaceClass === 0xff &amp;&amp; alt.interfaceSubclass === 0x42)
);
await device.claimInterface(adbInterface.interfaceNumber);
</code></pre>
<h3>1.2 Framing ADB Packets &amp; In-Memory RSA Authentication</h3>
<p>Communication requires implementing the 24-byte binary ADB header directly in JavaScript:</p>
<pre><code class="language-javascript">// ADB message header format: command, arg0, arg1, data_length, data_checksum, magic
function createAdbPacket(command, arg0, arg1, payload = new Uint8Array(0)) {
  const header = new ArrayBuffer(24);
  const view = new DataView(header);
  
  view.setUint32(0, command, true);              // A_CNXN, A_OPEN, A_WRTE, etc.
  view.setUint32(4, arg0, true);
  view.setUint32(8, arg1, true);
  view.setUint32(12, payload.byteLength, true);  // Payload size
  view.setUint32(16, calculateChecksum(payload), true);
  view.setUint32(20, command ^ 0xffffffff, true);// Magic verification
  
  return concatBuffers(header, payload.buffer);
}
</code></pre>
<p>When connecting to an unauthorized device, Android replies with an <code>A_AUTH</code> challenge. The browser extension generates a 2048-bit RSA key pair in-memory (or loads a stored key from <code>chrome.storage.local</code>), encodes the public key in Android's expected token format, and sends it to the device. Once the user taps <strong>"Allow USB Debugging"</strong> on their screen, the transport channel is established.</p>
<p>No system <code>adb</code> process is invoked, eliminating port contention entirely.</p>
<hr />
<h2>2. Inspecting WebView Traffic Without CA Certificates (CDP)</h2>
<p>Inspecting HTTPS network requests in mobile WebViews or hybrid apps is notoriously painful due to SSL Pinning and Android 7+ Network Security Config restrictions.</p>
<h3>2.1 Forwarding DevTools Sockets</h3>
<p>Android Chrome and debug-enabled WebViews (<code>WebView.setWebContentsDebuggingEnabled(true)</code>) expose an abstract Unix domain socket on the device (e.g., <code>@webview_devtools_remote_&lt;pid&gt;</code>).</p>
<p>By issuing an ADB port forward command across our WebUSB connection, the browser connects to the target WebView via a standard WebSocket.</p>
<h3>2.2 Direct CDP Network Inspection</h3>
<p>Once connected, we send Chrome DevTools Protocol commands directly to the engine:</p>
<pre><code class="language-javascript">// Enable network event streaming from the WebView
function startNetworkCapture(ws) {
  ws.send(JSON.stringify({
    id: 1,
    method: 'Network.enable',
    params: {
      maxPostDataSize: 65536 // Capture request bodies up to 64KB
    }
  }));
}

// Receive parsed network events
ws.onmessage = (event) =&gt; {
  const data = JSON.parse(event.data);
  
  if (data.method === 'Network.responseReceived') {
    const { requestId, response } = data.params;
    console.log(`[Status ${response.status}] ${response.url}`);
    
    // Fetch response body on demand
    ws.send(JSON.stringify({
      id: generateUniqueId(),
      method: 'Network.getResponseBody',
      params: { requestId }
    }));
  }
};
</code></pre>
<p>Because telemetry is extracted directly from the Chromium rendering process:</p>
<ul>
<li><strong>No CA root certificates are required</strong> on the Android device;</li>
<li><strong>No Wi-Fi proxy or DNS tampering</strong> is needed;</li>
<li>Requests, response headers, payloads, and timings are extracted cleanly, ready for export as cURL or sanitized HAR.</li>
</ul>
<hr />
<h2>3. Rolling Ring Buffers for Video and Log Synchronization</h2>
<p>Capturing intermittent bugs requires capturing evidence <em>before</em> you realize a crash happened. Storing hours of continuous high-definition recording and full device logcat quickly exhausts memory.</p>
<p>We implemented an in-memory <strong>Rolling Ring Buffer</strong>:</p>
<pre><code class="language-javascript">class RollingRingBuffer {
  constructor(capacity = 180) { // Holds 180 seconds of 1-second chunks
    this.buffer = new Array(capacity);
    this.capacity = capacity;
    this.head = 0;
    this.size = 0;
  }

  push(slice) {
    this.buffer[this.head] = slice;
    this.head = (this.head + 1) % this.capacity;
    if (this.size &lt; this.capacity) this.size++;
  }

  dump() {
    const result = [];
    let start = this.size &lt; this.capacity ? 0 : this.head;
    for (let i = 0; i &lt; this.size; i++) {
      result.push(this.buffer[(start + i) % this.capacity]);
    }
    return result;
  }
}
</code></pre>
<ul>
<li><strong>Video Stream</strong>: Compressed via WebCodecs or MediaRecorder into 1-second GOPs and pushed to the buffer;</li>
<li><strong>Logcat Stream</strong>: Read asynchronously over the ADB channel, with regex filters isolating the foreground package and flagging <code>FATAL EXCEPTION</code> or <code>ANR</code> signatures;</li>
<li><strong>On-Demand Dump</strong>: When a bug occurs, clicking "Finish" dumps the synchronized video clip, structured Markdown summary, screenshots, and filtered logs into an all-in-one developer-ready package.</li>
</ul>
<hr />
<h2>4. Architectural Comparison: Desktop vs. Browser</h2>
<table>
<thead>
<tr>
<th>Architectural Dimension</th>
<th>Traditional Desktop Tools (e.g., Scrcpy + Charles)</th>
<th>Browser Native Workspace (TabQA)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Communication Layer</strong></td>
<td>Host <code>adb</code> binary (<code>localhost:5037</code>)</td>
<td>Native WebUSB API (USB Bulk Transfer)</td>
</tr>
<tr>
<td><strong>Setup &amp; Dependencies</strong></td>
<td>Requires platform tools, drivers, proxies</td>
<td><strong>Zero host dependencies</strong> (Chrome 118+)</td>
</tr>
<tr>
<td><strong>Frame Rates</strong></td>
<td>High (60–120fps, ideal for gaming/long video)</td>
<td>Stable (Focus on UI testing and interaction)</td>
</tr>
<tr>
<td><strong>Traffic Inspection</strong></td>
<td>MITM Proxy (Requires installed CA Root Cert)</td>
<td><strong>Built-in CDP</strong> (Zero certificates required)</td>
</tr>
<tr>
<td><strong>Crash Extraction</strong></td>
<td>Manual terminal grep / full system dump</td>
<td><strong>Target app log filtering</strong> with ANR/Crash alerts</td>
</tr>
<tr>
<td><strong>Bug Delivery</strong></td>
<td>Dispersed files across folders</td>
<td><strong>Integrated Markdown report + assets for Jira/Notion</strong></td>
</tr>
<tr>
<td><strong>Data Privacy</strong></td>
<td>Local host</td>
<td><strong>Local-first</strong> (Data never leaves browser sandbox)</td>
</tr>
</tbody></table>
<hr />
<h2>5. Conclusion &amp; Open Source Project</h2>
<p>By combining WebUSB, WebCodecs, and Chrome DevTools Protocol, it is now entirely feasible to build a unified mobile testing workspace directly inside browser side panels.</p>
<p>We packaged this architecture into an open-source tool called <strong>TabQA</strong>:</p>
<ul>
<li><strong>GitHub Repository</strong>: <a href="https://github.com/openutx/TabQA">openutx/TabQA</a></li>
<li><strong>Official Documentation</strong>: <a href="https://tabqa.openutx.cn/en/">tabqa.openutx.cn/en/</a></li>
<li><strong>Chrome Web Store</strong>: <a href="https://chromewebstore.google.com/detail/tabqa/ddbodfcbakkoakaonpodnpgbkmmgpedp">Install TabQA for free</a></li>
</ul>
<p>We welcome feedback, issues, and contributions from testing and mobile development teams!</p>
]]></content:encoded></item><item><title><![CDATA[Tackling Intermittent Android Bugs: Rolling Video Buffers and Automated Logcat Alignment in the Browser]]></title><description><![CDATA[Tackling Intermittent Android Bugs: Rolling Video Buffers and Automated Logcat Alignment in the Browser
Every mobile QA engineer and Android developer has experienced this nightmare scenario:
You are ]]></description><link>https://tabqa.hashnode.dev/tackling-intermittent-android-bugs-rolling-video-buffers-and-automated-logcat-alignment-in-the-browser</link><guid isPermaLink="true">https://tabqa.hashnode.dev/tackling-intermittent-android-bugs-rolling-video-buffers-and-automated-logcat-alignment-in-the-browser</guid><category><![CDATA[Android]]></category><category><![CDATA[chrome extension]]></category><category><![CDATA[Testing]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Lijiawei]]></dc:creator><pubDate>Wed, 09 Sep 2026 07:13:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aa10524dd111ac41fc3f3a3/46693b70-3ee8-4729-8752-74a7ef634648.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Tackling Intermittent Android Bugs: Rolling Video Buffers and Automated Logcat Alignment in the Browser</h1>
<p>Every mobile QA engineer and Android developer has experienced this nightmare scenario:</p>
<p>You are testing a complex user flow—perhaps a multi-step checkout or a flaky gesture interaction. Suddenly, the app freezes with an <strong>Application Not Responding (ANR)</strong> dialog, or abruptly crashes back to the home screen.</p>
<p>You freeze.<br /><em>Did I have screen recording turned on?</em> <strong>No.</strong><br /><em>Was a terminal running</em> <code>adb logcat</code> <em>in the background?</em> <strong>No.</strong><br /><em>Can you reproduce it on the next attempt?</em> <strong>Of course not.</strong></p>
<p>Even if you keep a desktop screen recorder running all day, you are left with a 45-minute, 2GB video file and a 200,000-line logcat dump. Handing those massive, disconnected files to an engineer is a guaranteed recipe for frustration.</p>
<p>In this article, we'll examine the engineering principles behind solving intermittent mobile bugs: <strong>implementing an in-browser rolling ring buffer that continuously captures screen video and streams logcat, automatically isolating the crash context the moment it occurs.</strong></p>
<hr />
<h2>1. The Core Architecture: Dual-Stream Asynchronous Buffering</h2>
<p>To capture unexpected bugs without blowing up browser memory, we need two decoupled, continuous streams running in a sliding window (e.g., the most recent 120–180 seconds):</p>
<pre><code class="language-plaintext">                     +---------------------------------------+
                     |         Live Android Device           |
                     +-------------------+-------------------+
                                         |
                       [ WebUSB Transport Pipeline ]
                                         |
                 +-----------------------+-----------------------+
                 |                                               |
                 v                                               v
       [ Video Frame Stream ]                         [ Raw Logcat Stream ]
                 |                                               |
                 v                                               v
      +--------------------+                         +--------------------+
      | 1-Second GOP Chunk |                         | Regex Filter Engine|
      |   (WebCodecs API)  |                         | (Package &amp; Crash)  |
      +----------+---------+                         +----------+---------+
                 |                                               |
                 v                                               v
      +--------------------+                         +--------------------+
      | Rolling RingBuffer |                         | Chrono Log Buffer  |
      | (Sliding 180s Max) |                         | (Keyed by WallTime)|
      +----------+---------+                         +----------+---------+
                 |                                               |
                 +-----------------------+-----------------------+
                                         |
                                         v  [ Trigger: Crash Detected or Manual Stop ]
                             +-----------------------+
                             | Sliced MP4 + Log Slice|
                             |   Timestamp Aligned   |
                             +-----------------------+
</code></pre>
<hr />
<h2>2. Implementing the High-Performance Rolling Ring Buffer</h2>
<p>A circular ring buffer allows continuous pushing of streaming chunks with $O(1)$ amortized memory allocation, automatically overwriting expired slices.</p>
<p>Here is the TypeScript/JavaScript implementation used for managing video and log chunks:</p>
<pre><code class="language-typescript">export interface TimeStampedChunk {
  timestamp: number; // Monotonic performance.now()
  data: Uint8Array | Blob | string;
}

export class SlidingRingBuffer&lt;T extends TimeStampedChunk&gt; {
  private buffer: (T | null)[];
  private capacity: number;
  private head: number = 0;
  private currentSize: number = 0;

  constructor(maxItems: number) {
    this.capacity = maxItems;
    this.buffer = new Array(maxItems).fill(null);
  }

  public push(item: T): void {
    this.buffer[this.head] = item;
    this.head = (this.head + 1) % this.capacity;
    if (this.currentSize &lt; this.capacity) {
      this.currentSize++;
    }
  }

  // Returns all items ordered chronologically from oldest to newest
  public dump(): T[] {
    const result: T[] = [];
    const startIndex = this.currentSize &lt; this.capacity ? 0 : this.head;
    
    for (let i = 0; i &lt; this.currentSize; i++) {
      const idx = (startIndex + i) % this.capacity;
      const item = this.buffer[idx];
      if (item !== null) {
        result.push(item);
      }
    }
    return result;
  }

  // Slices only the last N seconds prior to the trigger event
  public dumpRecent(durationMs: number): T[] {
    const all = this.dump();
    if (all.length === 0) return [];
    
    const latestTime = all[all.length - 1].timestamp;
    const cutoffTime = latestTime - durationMs;
    return all.filter(item =&gt; item.timestamp &gt;= cutoffTime);
  }
}
</code></pre>
<hr />
<h2>3. Real-Time Logcat Filtering &amp; Anomaly Detection</h2>
<p>A continuous <code>logcat</code> stream outputs thousands of lines per second across the entire operating system. To make it actionable:</p>
<ol>
<li><p><strong>Target Package Isolation</strong>: We resolve the foreground PID and filter specifically for the application under test.</p>
</li>
<li><p><strong>Signature Matching</strong>: We run regex state machines listening for <code>FATAL EXCEPTION</code>, <code>ANR in &lt;package&gt;</code>, and <code>AndroidRuntime: E</code> patterns.</p>
</li>
</ol>
<pre><code class="language-javascript">// Stream parser and crash detector
const CRASH_SIGNATURES = [
  /FATAL EXCEPTION:\s*(.*)/i,
  /AndroidRuntime:\s*Process:\s*([a-zA-Z0-9._]+),\s*PID:\s*(\d+)/i,
  /ActivityManager:\s*ANR in\s*([a-zA-Z0-9._]+)/i
];

function processLogLine(rawLine, targetPackage, logBuffer, onCrashDetected) {
  const monotonicTime = performance.now();
  
  // Format: MM-DD HH:MM:SS.mmm PID TID Level Tag: Message
  const isRelevant = rawLine.includes(targetPackage) || 
                     CRASH_SIGNATURES.some(sig =&gt; sig.test(rawLine));

  if (!isRelevant) return;

  // Store structured log entry with synchronized monotonic timestamp
  logBuffer.push({
    timestamp: monotonicTime,
    data: rawLine
  });

  // Check for critical anomalies
  for (const regex of CRASH_SIGNATURES) {
    if (regex.test(rawLine)) {
      onCrashDetected({
        signature: rawLine,
        detectedAt: monotonicTime
      });
      break;
    }
  }
}
</code></pre>
<hr />
<h2>4. Aligning Video Frame Presentation with Log Timestamps</h2>
<p>The most difficult challenge in mobile debugging is correlating <em>what the user saw</em> with <em>what the operating system threw</em>.</p>
<ul>
<li><p><strong>The Problem</strong>: Android's <code>logcat</code> timestamps reflect the device's internal Real-Time Clock (RTC), which may have clock drift relative to the host computer recording the video.</p>
</li>
<li><p><strong>The Solution (Monotonic Anchor Sync)</strong>:</p>
<ol>
<li><p>When initiating the WebUSB session, the host queries the device uptime via <code>SystemClock.elapsedRealtime()</code>;</p>
</li>
<li><p>Simultaneously, the browser records <code>performance.now()</code>;</p>
</li>
<li><p>Every incoming H.264 video keyframe and every parsed log line is indexed against this common monotonic host timeline.</p>
</li>
</ol>
</li>
</ul>
<p>When a crash occurs, the exported package contains:</p>
<ul>
<li><p>A trimmed MP4 video focusing specifically on the 30–60 seconds leading up to the issue;</p>
</li>
<li><p>A parsed Markdown file matching visual timestamps (<code>00:23s - Screen tap</code>) directly with corresponding error logs (<code>00:23.412 - NullPointerException at MainActivity.java:84</code>).</p>
</li>
</ul>
<hr />
<h2>5. Architectural Comparison: Full Dump vs. Sliced Capture</h2>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Traditional Full Device Dump</th>
<th>Sliding Buffer Capture (TabQA Model)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>File Footprint</strong></td>
<td>500MB – 2GB (Full MP4 + Raw Logcat)</td>
<td><strong>5MB – 25MB</strong> (Targeted Clip + Sliced Context)</td>
</tr>
<tr>
<td><strong>Developer Triaging Time</strong></td>
<td>15–30 minutes (Manual scrubbing &amp; grepping)</td>
<td><strong>&lt; 2 minutes</strong> (Instant stack trace + visual repro)</td>
</tr>
<tr>
<td><strong>Crash Discovery</strong></td>
<td>Lost if not proactively recording</td>
<td><strong>Retrospective capture</strong> ("Rewind" anytime)</td>
</tr>
<tr>
<td><strong>Storage Impact</strong></td>
<td>Rapidly fills disk with stale runs</td>
<td><strong>Zero persistent bloat</strong> (Held in volatile RAM)</td>
</tr>
<tr>
<td><strong>Host Toolchain Required</strong></td>
<td>Desktop Screen Recorder + ADB Terminal</td>
<td><strong>100% Browser Side Panel</strong> (No local install)</td>
</tr>
</tbody></table>
<hr />
<h2>6. Open-Source Implementation</h2>
<p>This sliding window architecture, combining WebUSB device streaming and retrospective evidence capture, is fully implemented in the open-source browser extension <a href="https://github.com/openutx/TabQA"><strong>TabQA</strong></a>.</p>
<ul>
<li><p><strong>GitHub Repository</strong>: <a href="https://github.com/openutx/TabQA">https://github.com/openutx/TabQA</a></p>
</li>
<li><p><strong>Detailed Workflow Guide</strong>: <a href="https://tabqa.openutx.cn/en/guides/record-android-screen-and-logcat">https://tabqa.openutx.cn/en/guides/record-android-screen-and-logcat</a></p>
</li>
<li><p><strong>Install Free on Chrome</strong>: <a href="https://chromewebstore.google.com/detail/tabqa/ddbodfcbakkoakaonpodnpgbkmmgpedp">Chrome Web Store Link</a></p>
</li>
</ul>
<p>If your team struggles with capturing intermittent mobile crashes or aligning test evidence with bug trackers like Jira or Notion, feel free to give this browser-native approach a try!</p>
]]></content:encoded></item></channel></rss>