Skip to content

Admin Logging Tab

mdeguzis edited this page Jul 21, 2026 · 1 revision

Admin Logging Tab

Per-session in-memory ring buffer of every logFrontendEvent and ppTrack call, surfaced in the admin panel under Logging (/admin.html -> tab select -> Logging). Motivating pain point: debugging on mobile without remote devtools. Filed as #366.

Why this exists

Mobile debugging was a slow guessing loop. Symptoms (filter modal disappears, dropdown does nothing, checkbox stuck) came in via screenshots; nobody had visibility into what actually fired. console.log is invisible on a phone and Chrome remote devtools require a laptop plus USB plus a matching Chrome version.

This tab is the replacement. Every log call the frontend makes lands in a localStorage-backed ring, and the admin panel renders it as a filterable table. The bug that landed in c4c5e0301 (mobile filter modal premature-closes) was found this way in about ten minutes: enable Capture DEBUG, tap the X, look at the trace, see _handleOpenState decided isOpen=false firing 2ms before the X handler. Grep for who else strips .open. Done.

Contents

  • Where the code lives
  • How it captures
  • The tab UI
  • What every module in the codebase should push
  • Debugging workflow that actually works

Where the code lives

Piece Path
Ring buffer + subscribe API js/lib/log-buffer.js
Analytics hook (every ppTrack call pushes) js/lib/analytics.js
Topbar hook (logFrontendEvent mirrors to buffer) js/lib/topbar.js
Tab component js/admin/components/logging.js
Wiring into TAB_LOADERS js/admin/main.js
Permission gate js/admin/permissions.js (logging: ['view_analytics'])
Tab option + section admin.html
Load tag on every page <script type="module" src="js/lib/log-buffer.js?v=..."> in every HTML
Tests tests/logBuffer.test.js (16 cases, jsdom env)

How it captures

Ring size is 500 entries, oldest drops first. Backed by localStorage (not sessionStorage) so the admin tab in one browser tab sees logs from a game page in another tab -- same origin, same storage.

Capture level:

Level Default? Kept?
DEBUG no Dropped unless the user turns on "Capture DEBUG" in the tab (persisted to localStorage['pp:loglevel'] for the session) OR appends ?loglevel=debug to any URL.
INFO yes Always captured. Everything the filter change handlers, refreshReports, and unusual state transitions push.
WARN yes Always captured. Failed fetches, cache misses, retryable errors.
ERROR yes Always captured. Rate-limited via a 60s per-signature cooldown so a tight loop cannot flood the buffer.

Where the push happens:

  • logFrontendEvent(level, msg, ctx) in js/lib/topbar.js -- pushes to buffer AND fires the existing ppTrack('log', ...) -> site_events pipeline.
  • window.ppTrack(eventType, metadata) in js/lib/analytics.js -- pushes to buffer BEFORE the Supabase fetch so a slow network cannot mask visibility.
  • Any module can push directly: window.ppLogBuffer.pushLog('INFO', 'label', { source: 'my-module', ...ctx }).

Non-log ppTrack events (page_view, search_query, etc.) also land in the buffer, tagged with event_type in the ctx. That's how the tab's Module dropdown ends up populated with values like game-page:filter, page_view, search_query.

The tab UI

Toolbar:

  • Level filter -- All / DEBUG / INFO / WARN / ERROR.
  • Module filter -- All modules or a specific ctx.source / ctx.event_type / ctx.module value. Falls back to (untagged).
  • Search msg + ctx -- substring search across the message and stringified ctx.
  • Live tail -- default on, auto-scrolls to the bottom on every new entry.
  • Capture DEBUG -- flips capture level for the session (persists in localStorage).
  • Copy JSON -- copies the filtered set to the clipboard. Paste into an issue for reports.
  • Clear -- empties the ring for this session (confirmation prompt).

Table columns: Time, Level (color-coded), Module, Message + ctx (JSON pre-formatted below the message).

Meta line: <N> shown / <M> captured (session) — tells you at a glance how much your filter is narrowing the view.

What every module in the codebase should push

The rule of thumb: any code path that does a non-trivial lookup, fallback, async operation, OR user interaction should log enough context to diagnose failures without a debugger. ~/.claude/rules/general-rules.md covers this in more detail. In practice for this project:

  • Always tag the ctx with source: { source: 'module:name', ...ctx }. The Module dropdown pivots on this.
  • Log the outcome AND the branch that produced it (which API, which field name, which fallback fired).
  • On failure, include enough IDs to find the row in Supabase (appId, report id, session).
  • DEBUG for normal successes, INFO for lifecycle events (session start/stop, page view, filter interaction), WARN for unexpected states, ERROR for actual failures.
  • Never log inside tight loops -- one summary line per logical operation.

Debugging workflow that actually works

Mobile-side (any bug reported on a phone):

  1. Reproduce the symptom in the phone browser (any tab, no remote devtools needed).
  2. Open admin.html in a new tab in the same browser.
  3. Tab select -> Logging. Enable Capture DEBUG if the events are likely at DEBUG level.
  4. Filter Module = the surface you touched (game-page:filter, wireFilterPanelClose, etc.).
  5. Read the trace top-to-bottom. Note the ORDER of events -- inverted order often points at a race (a2 firing before a1 means something happened between them that you didn't expect).
  6. Copy JSON, paste into the GitHub issue as the bug report.

Desktop-side (a bug caught while browsing):

  • Same steps, minus the "in a new tab" part.

Related

  • Admin Deployments Tab -- companion for "is my push live yet"
  • ~/.claude/rules/general-rules.md -- the debug-logging rule that governs what every code path should push

Clone this wiki locally