Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
push:
branches: [main, ux-revamp]
branches: [trunk]
pull_request:
branches: [main]
branches: [trunk]

concurrency:
group: ci-${{ github.ref }}
Expand Down Expand Up @@ -73,6 +73,9 @@ jobs:
- name: Build extension
run: npm run build

- name: Check bundle size budgets
run: npm run check:bundle

- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ All notable changes to the APQ Debugger extension will be documented in this fil
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.0.0] - 2026-06-13

### Added
- Passive mode: resolve APQ hashes from the registry without contaminating live traffic
- GraphQL schema introspection and SDL viewer in a dedicated Schema tab
- Copy as cURL for captured requests
- Keyboard navigation and shortcuts across the DevTools panel
- Responsive layout for narrower DevTools dock sizes
- Bundle size budget check in CI (`npm run check:bundle`)

### Changed
- **Major UI redesign** — tabbed layout separating requests, schema, and settings
- Schema moved from inline controls to its own tab and viewer
- Optimized production bundle sizes for the service worker and DevTools panel

### Fixed
- Schema endpoint detection when multiple GraphQL endpoints are observed
- Prettier formatting violations that failed the CI lint job

## [2.0.0] - 2026-02-07

### Added
Expand Down Expand Up @@ -83,6 +102,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

| Version | Commit | Date |
|---------|--------|------|
| 3.0.0 | — | 2026-06-13 |
| 2.0.0 | — | 2026-02-07 |
| 1.1.0 | `5b035ba` | 2026-01-30 |
| 1.0.0 | `96f5ff3` | 2026-01-26 |
Expand Down
54 changes: 54 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project

Chrome MV3 DevTools extension that reveals the full GraphQL operations behind Apollo Automatic Persisted Query (APQ) hashes. It attaches the Chrome Debugger API to a tab, intercepts requests via `Fetch.requestPaused`, and replaces the APQ hash with a bogus one so the server responds `PersistedQueryNotFound`, forcing Apollo Client to retry with the full query text — which is then captured and shown in a DevTools panel.

## Commands

```bash
npm run build # Bundle & minify into extension_build/ (the only loadable folder)
npm run build:dev # Build with inline source maps, no minification
npm run watch # Rebuild on change (reload extension in chrome://extensions after)
npm test # Jest unit tests (coverage always collected)
npx jest tests/unit/hash-registry.test.js # Single test file
npx jest -t "should evict the oldest hash" # Single test by name
npm run test:e2e # Playwright e2e (launches real Chrome with the built extension)
npm run lint # ESLint
npm run format:check # Prettier check (see Windows note below)
```

Releases: `npm version patch|minor|major` (syncs manifest.json via `scripts/sync-version.js`), then `git push --follow-tags` — the release workflow builds, tests, and publishes the zip.

- The default branch is **`trunk`**, not `main`. CI runs on pushes/PRs to trunk only.
- `package.json` is the canonical version; `manifest.json` is synced from it (also patched at build time).
- **Windows note:** the repo checks out with CRLF (`core.autocrlf=true`), so `npm run format:check` fails locally on line endings alone. Use `npx prettier --check --end-of-line auto <globs>` to see real violations; commits are normalized to LF so CI is unaffected.

## Architecture

Two independently bundled contexts that communicate only via `chrome.runtime.sendMessage`:

- **Service worker** (`js/sw/`, entry `index.js` → built as `service-worker.min.js`): owns all Chrome Debugger interaction. `debugger-manager.js` handles attach/detach lifecycle, `interceptor.js` handles `Fetch.requestPaused` and hash contamination, `messaging.js` routes incoming messages, `chrome-api.js` wraps callback APIs in promises.
- **DevTools panel** (`js/ui/`, entry `index.js` → built as `devtools.min.js` loaded by `frontend/devtools.html`): one module per UI concern, all sharing the mutable `state` object in `js/ui/state.js`. The panel identifies its tab via `chrome.devtools.inspectedWindow.tabId` (reliable even when undocked).
- **`js/shared/`**: constants and logger used by both bundles. The GraphQL introspection query is a string literal here specifically so the `graphql` npm package stays out of the service worker bundle.

Key cross-cutting behaviors:

- **MV3 service worker restarts:** all state that must survive (attached tabs, URL patterns, hash registry, passive-mode flag) lives in `chrome.storage.local`; observed endpoints live in `chrome.storage.session` (right lifetime: survives SW restarts, cleared on browser exit). `isDebuggerActive()` checks memory → storage → live `chrome.debugger.getTargets()` and self-heals. `initRegistry()` and `restoreDebuggerState()` run on every SW start. Never keep must-survive state in module-level variables alone — a user-triggered message often wakes a *fresh* SW.
- **Hash registry / passive mode** (`js/sw/hash-registry.js`): active mode contaminates hashes and records hash→query mappings from the retries; passive mode resolves hashes from the registry without touching traffic. Capped at `HASH_REGISTRY_MAX_ENTRIES` (LRU eviction; insertion order is recency — re-registering deletes then re-sets the key).
- **Messaging convention:** SW→panel messages are broadcast with a `tabId` field and filtered on the receiving side; panel→SW messages are validated in `messaging.js` (`validateMessage`) before routing. Every `sendMessage` callback checks `chrome.runtime.lastError` because the panel is frequently not open.
- **Schema loading** (`js/sw/schema-loader.js`): runs in the SW (which has `<all_urls>` host permission for cross-origin fetch). Endpoints observed by the interceptor (`endpoint-tracker.js`) are known GraphQL endpoints, so they are introspected directly, replaying the request headers captured from live traffic (production gateways reject bare requests); only the common-path guesses on the page's origin are probed with a bare `{__typename}` request.
- **Build** (`build-enhanced.js`): esbuild bundles both entries as IIFEs; production drops `debugger` statements and treats `console.info`/`console.debug` as pure (stripped) — so use those for dev-only logging and `console.log`/`warn`/`error` for messages that should survive in release builds. The HTML/manifest are patched for the flat `extension_build/` layout; loading the repo root unpacked does not work.

## Tests

- Jest's default environment is **node**; DOM tests must start with a `/** @jest-environment jsdom */` docblock (see `tests/unit/devtools.test.js`, `tests/unit/curl-copy.test.js`).
- Chrome APIs are mocked manually as `global.chrome` in `tests/unit/setup.js` (not via jest-chrome) — extend that file when a test needs an API surface that isn't mocked yet.
- Files are transformed with esbuild (`jest-esbuild-transform.js`), so ES modules in `js/` import cleanly.
- Test DOM fixtures are built inline with `document.body.innerHTML` mirroring the relevant fragment of `frontend/devtools.html`.

## Conventions

From CONTRIBUTING.md, the ones that matter in practice: promise-wrap Chrome APIs via `chrome-api.js` rather than using raw callbacks; JSDoc on all exported functions; wrap `JSON.parse` in try/catch; handle `chrome.runtime.lastError` in every Chrome API callback; one responsibility per module.
38 changes: 30 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@
- **DevTools Integration**: Seamless integration with Chrome DevTools
- **APQ Fallback Visualization**: See both the hash-only and full-query phases of APQ requests
- **Request History**: Browse, filter, and inspect intercepted requests with syntax highlighting
- **Passive Mode**: Resolve APQ hashes from a persisted hash registry without modifying any traffic
- **Schema Explorer**: Auto-detect the GraphQL endpoint and browse its introspected schema
- **Copy as cURL**: Replay any captured request from Bash or PowerShell, including captured request headers

## Installation

1. Clone this repository
2. Open Chrome and go to `chrome://extensions/`
3. Enable "Developer mode"
4. Click "Load unpacked" and select the project folder (or `extension_build/` after building)
5. Open DevTools and look for the "APQ Debugger" panel
2. Run `npm install` and `npm run build` to produce the `extension_build/` folder
3. Open Chrome and go to `chrome://extensions/`
4. Enable "Developer mode"
5. Click "Load unpacked" and select the `extension_build/` folder
6. Open DevTools and look for the "APQ Debugger" panel

## Usage

Expand All @@ -41,25 +45,36 @@ apq-debugger/
│ ├── devtools.html # Main DevTools panel HTML
│ └── devtools.css # Panel styles (dark/light theme)
├── js/
│ ├── shared/ # Constants & logger shared by both bundles
│ ├── sw/ # Service worker modules
│ │ ├── index.js # Entry point – event wiring
│ │ ├── chrome-api.js # Promise wrappers for Chrome APIs
│ │ ├── debugger-manager.js # Attach/detach/restore lifecycle
│ │ ├── interceptor.js # Fetch.requestPaused handler
│ │ ├── messaging.js # onMessage router & action toggle
│ │ ├── storage.js # chrome.storage.local helpers
│ │ └── hash.js # SHA-256 digest utility
│ │ ├── hash.js # SHA-256 digest utility
│ │ ├── hash-registry.js # Persisted hash → query registry (passive mode)
│ │ ├── endpoint-tracker.js # GraphQL endpoints observed per tab
│ │ └── schema-loader.js # Endpoint auto-detection & introspection
│ ├── ui/ # DevTools panel modules
│ │ ├── index.js # Entry point – panel creation
│ │ ├── state.js # Shared reactive state
│ │ ├── dom-helpers.js # DOM query & escape utilities
│ │ ├── patterns.js # URL pattern CRUD & storage sync
│ │ ├── request-list.js # Request list rendering & filtering
│ │ ├── request-detail.js # Detail panel & copy-to-clipboard
│ │ ├── curl-copy.js # Copy-as-cURL generators & modal
│ │ ├── status.js # Status badge & banner management
│ │ └── debugger-controls.js # Start/stop button orchestration
│ ├── devtools.js # Legacy monolithic panel script
│ └── service-worker.js # Legacy monolithic service worker
│ │ ├── debugger-controls.js # Start/stop button orchestration
│ │ ├── registry-controls.js # Hash registry & passive mode controls
│ │ ├── schema-controls.js # Schema loading controls & progress
│ │ ├── schema-viewer.js # Introspected schema explorer
│ │ ├── settings.js # Panel settings
│ │ ├── toolbar.js # Toolbar actions
│ │ ├── keyboard.js # Keyboard navigation
│ │ ├── layout.js # Panel layout management
│ │ └── resize.js # Resizable panel dividers
├── scripts/
│ ├── sync-version.js # Sync package.json → manifest.json version
│ └── package-extension.js # Zip extension_build/ for Web Store upload
Expand Down Expand Up @@ -141,6 +156,13 @@ The GitHub Actions [release workflow](.github/workflows/release.yml) will automa
- **Message Passing**: Bidirectional communication between the panel and service worker via `chrome.runtime.sendMessage`
- **Persistent State**: URL patterns and attached-tab state are persisted in `chrome.storage.local` to survive service worker restarts

### Privacy Note

The **Copy as cURL** feature includes the captured request headers — which may contain
cookies and authorization tokens — in the generated command so the request can be
replayed as-is. This data never leaves your browser; it is only written to your
clipboard when you click **Copy**.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, architecture details, and contribution guidelines.
Expand Down
1 change: 1 addition & 0 deletions build-enhanced.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ async function bundleJS(entry, outfile) {
sourcemap: isDev ? 'inline' : false,
drop: isDev ? [] : ['debugger'],
pure: isDev ? [] : ['console.info', 'console.debug'],
legalComments: 'none',
logLevel: 'warning',
metafile: true,
});
Expand Down
Loading
Loading