Skip to content

Debug Toolbar

The debug toolbar is a self-contained visual overlay injected into the HTML response when debug mode is active. It provides real-time cache diagnostics, per-section purge controls, and a live webhook event feed — all without external dependencies.

For usage instructions, see the Debug Toolbar guide.

┌──────────────────────────────────────────────────────┐
│ Browser (debug mode active) │
│ │
│ ┌─────────┐ hover ┌────────────┐ │
│ │ Pill │ ──────> │ Action Bar │ │
│ └─────────┘ └──────┬─────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ ┌─────┴────┐ ┌─────┴────┐ ┌─────┴──────┐ │
│ │ Sections │ │ Events │ │ Purge │ │
│ │ Panel │ │ Panel │ │ Actions │ │
│ └──────────┘ └─────┬────┘ └─────┬──────┘ │
│ │ │ │
│ poll 5s POST /api/ │
│ │ cache/purge │
└────────────────────────────┼──────────────┼─────────┘
│ │
┌────────────────────────────┼──────────────┼─────────┐
│ Dispatch Worker │ │ │
│ │ auth + forward │
│ │ │ │
└────────────────────────────┼──────────────┼─────────┘
│ │
┌────────────────────────────┼──────────────┼─────────┐
│ Theme Worker │ │ │
│ │ │ │
│ ┌──────────────┐ ┌─────┴────┐ ┌─────┴──────┐ │
│ │ Page Renderer │ │ Event │ │ Section │ │
│ │ (collects │ │ Ring │ │ Cache │ │
│ │ debug data) │ │ Buffer │ │ Purge │ │
│ └──────────────┘ └──────────┘ └────────────┘ │
└─────────────────────────────────────────────────────┘

The debug toolbar is injected during HTML response construction in the theme worker. The flow is:

  1. Render: page-renderer.ts renders each section, collecting SectionDebugInfo when debugMode is true
  2. Assemble data: worker.ts builds a DebugBarData object with sections, deploy version, cache status, and purge token
  3. Build HTML: debug-bar.ts assembles <style> + <div id="lk-debug"> + <script> from the data
  4. Inject: The HTML block is inserted before </body> in the response stream

All CSS is scoped under #lk-debug with CSS custom properties. All JS runs as an IIFE that receives the serialized data object. Zero external dependencies.

interface SectionDebugInfo {
id: string; // unique section ID
type: string; // section type (e.g. "hero", "header")
cached: boolean; // whether served from section cache
renderMs: number; // time in executeComponent() (0 for cache hits)
sizeBytes: number; // HTML output byte length
cacheKey?: string; // exact Cache API URL for purge targeting
dynamic?: boolean; // true if section accesses session data
sessionDeps?: string[]; // which session deps (cart, customer, etc.)
}

The cacheKey is the full URL used by the Cache API (e.g. https://section-cache/fmdf/slideshow/2b18d6bd). This is what gets passed to cache.delete() during purge.

Dynamic sections are identified by getSectionMeta() which reads the section registry. These sections access session-scoped data (cart, customer, checkout) and are rendered fresh on every request — they have no cache key.

The pill also shows the page-level CDN cache status from the dispatch worker’s X-Cache header:

ValueMeaning
HITPage served from dispatch CDN cache (zero Worker invocations)
MISSPage rendered by theme worker, now cached at CDN edge
bypassDebug mode skips CDN cache to prevent debug HTML from being cached

Three purge actions are available, each with a different scope:

Click section purge button
→ Client JS: POST /api/cache/purge { sectionKeys: ["https://section-cache/..."] }
→ Dispatch Worker: auth check → forward to theme worker via service binding
→ Theme Worker: cache.delete(new Request(key)) on Cache API
→ Theme Worker: record PurgeEvent in ring buffer
→ Theme Worker: return { sectionsPurged: 1 }
→ Dispatch Worker: merge response → return { purged: 0, sectionsPurged: 1, themeForwarded: true }
→ Client JS: toast "Purged 1 cached item(s)"

On next page load, the purged section renders fresh (cache miss).

Click "Page" → "Confirm?"
→ Client JS: POST /api/cache/purge {
paths: ["/current-page"],
sectionKeys: [all section cache keys on page]
}
→ Dispatch Worker: cache.delete on CDN page entry + forward to theme worker
→ Theme Worker: cache.delete on each section key
→ Response merges both counts

Same as page purge but sends paths: ["/"] to purge the homepage CDN entry. Section keys from the current page are still sent — this does not purge every section across the entire site (that would require a full cache clear via the CF API).

The purge token flows through two workers:

  1. Theme worker bakes CACHE_PURGE_TOKEN into the debug bar HTML as purgeToken
  2. Client JS sends Authorization: Bearer <purgeToken> with purge requests
  3. Dispatch worker validates the token using timing-safe comparison against its own CACHE_PURGE_TOKEN binding
  4. Theme worker receives the forwarded request (already authed by dispatch)

Both workers must have the same CACHE_PURGE_TOKEN value. The deploy script reads it from process.env.CACHE_PURGE_TOKEN and bakes it as a secret_text binding. Set the env var before deploying:

Terminal window
CACHE_PURGE_TOKEN="your-token" pnpm deploy:theme <slug> --promote

Using wrangler secret put after deploy will be overwritten by subsequent deploys — always pass via env var.

The events panel shows a live feed of cache purge events via a polling endpoint.

purge-events.ts maintains an in-memory ring buffer (max 100 events) per Worker isolate. Each purge action (section, page, webhook) records a PurgeEvent:

interface PurgeEvent {
id: string; // UUID
type: string; // "path" | "key" | "d1" | "full"
paths: string[]; // URLs/keys that were purged
kvEvicted: number; // KV keys evicted
pathsPurged: number; // cache entries deleted
timestamp: number; // Unix ms
durationMs: number; // purge duration
}

The endpoint GET /__dev/purge-events?since=<ts> returns events newer than the given timestamp, gated by the debug cookie.

The debug bar JS polls every 5 seconds with the last-seen timestamp as cursor:

GET /__dev/purge-events?since=1709341200000
→ [{ id, type, paths, timestamp, ... }, ...]

New events are prepended to the panel. If the panel is closed, a toast notification appears and the Events button badge increments.

The ring buffer is in-memory — if a webhook purge hits a different CF isolate than the one serving your debug bar poll, those events won’t appear. This is expected and acceptable for dev/debug. Cross-isolate visibility via KV with short TTL is a potential future upgrade.

Sections that access session-scoped data (cart, customer, checkout) are marked as dynamic in the section registry via getSectionMeta(). These sections:

  • Are always rendered fresh (never cached in the section cache)
  • Show a yellow DYNAMIC badge in the sections panel
  • Have no cache key (purge button shows “No cache key” toast)
  • Are excluded from the cached/total ratio in the pill

The sessionDeps field lists which session dependencies triggered the dynamic classification (e.g. ["cart"], ["customer", "checkout"]).

FileRole
cf-client-site/src/lib/debug-bar.tsAssembles CSS + DOM + JS into injectable HTML
cf-client-site/src/lib/debug-bar-css.tsAll styles scoped under #lk-debug
cf-client-site/src/lib/debug-bar-js.tsClient-side IIFE — pill, panels, events, highlighting
cf-client-site/src/lib/purge-events.tsIn-memory ring buffer for purge events
cf-client-site/src/render/page-renderer.tsCollects SectionDebugInfo per section during render
cf-client-site/src/worker.tsInjects debug bar + serves /__dev/purge-events endpoint
cf-client-dispatch/src/worker.tsAuth + forward purge requests to theme worker