Observability Logging
Updated: 2026-07-30
Barbelic keeps lightweight operational logs for slow-data and import diagnosis. Logs must explain where time was spent without exposing raw workout payloads, uploaded file contents, OAuth tokens, or provider raw payloads.
Correlation Fields
Use these fields consistently:
operationId/operation_id: one generated operation identifier.operationType: broad category such asrpcorwodup_import.operationName: concrete operation such asget_home_dashboardorwodup-start-import.durationMs/duration_ms: elapsed time in milliseconds.responseBytes: approximate JSON response size on the frontend.errorCode/error_code: stable error code when available.batchId/batch_id: Wodup import batch id.job_id: stats refresh job id.
Persistent Application Error Events
Authenticated browser, React render, returned server/RPC, network, and local draft persistence failures are sent through record_client_error_event(jsonb) into the private client_error_events table.
The application installs these capture boundaries:
- browser
errorandunhandledrejectionevents; - failed non-RPC
fetchresponses and network rejections (RPC failures are recorded once at the repository boundary — reads throughcallScreenRpc, writes throughcallMutationRpc, each with the RPC's real name); - React error boundaries;
- screen RPC and strict response-adapter failures;
- mutation RPC failures at
callMutationRpc(#664 Phase 2) and draft persistence failures; - every user-facing error message via the
reportedUserErrorgate (#664 Phase 1): copy is returned only after the unreported error is captured, withPUBLIC_ERROR_MESSAGEScodes excluded as expected user errors, and pre-RPC client invariant violations classified asclient_contract_errorviaclientContractError.
A report-once marker (owner-scoped, including the cause chain of wrapped errors) guarantees that overlapping boundaries store one event per error object.
The browser keeps at most 50 unsent events in sessionStorage["barbelic-pending-error-events-v1"] and retries after an authenticated session or restored connectivity is available, with bounded exponential backoff after transient failures. Pending entries are partitioned by authenticated user; an account switch cannot submit the previous account's entry under the new auth.uid(). Invalid or permanently rejected client envelopes are discarded from transport state so they cannot block later diagnostics. Normal duplicate events are suppressed for 30 seconds and browser ingestion is capped at 20 events per minute. The database independently caps authenticated ingestion at 60 events per minute and 1,000 per 24 hours.
The server binds every row to auth.uid() and verifies the transport's expected_user_id. Normal users cannot directly read, insert, update, or delete the table; service-role diagnostics can read it. Anonymous ingestion is intentionally disabled. Replaying the same client event ID with different content is rejected using a server-computed payload hash.
Free-form exception messages and URL/resource paths are never part of the server ingest contract. Stored rows contain only bounded classifications, error codes, contract field paths, operation keys, numeric status/timing data, and asset-basename/line/column stack frames. Tokens, authorization data, emails, titles, file names, query strings, request/response bodies, workout payloads, provider identifiers, and raw user-agent data are excluded.
User-facing error surfaces render only application-owned copy. Raw provider, RPC, response-contract, and import error strings stay out of normal and admin workflow screens; operational details are inspected in private server logs.
No TTL, purge RPC, cron job, or automatic deletion policy is installed during development. Stored rows remain until an explicit manual retention decision is made. The bounded browser pending queue is transport state, not the server log.
Frontend Debug Rings
Screen RPC calls write both compatibility RPC logs and the general operation log.
The in-memory rings are updated synchronously. sessionStorage is hydrated once per ring and whole-ring persistence is coalesced onto an idle callback (or a timer fallback), so RPC completion does not repeatedly parse and stringify both history buffers before the screen can render.
Storage:
globalThis.__liftGuildRpcDebugsessionStorage["barbelic-rpc-debug-log"]globalThis.__liftGuildOperationDebugsessionStorage["barbelic-operation-debug-log"]
Console output is opt-in:
?debug-rpc=1or?lg-rpc-debug=1?debug-ops=1or?lg-operation-debug=1localStorage["barbelic-rpc-debug"] = "1"localStorage["barbelic-operation-debug"] = "1"
Required screen RPC fields:
event:screen_rpc:started,screen_rpc:completed, orscreen_rpc:failedoperationIdoperationType: "rpc"operationNamerpcNamedurationMsresponseBytesparamKeyserrorCodeand safeerrorobject on failure
Wodup import frontend events:
wodup_import_upload:started|completed|failedwodup_import_start:started|completed|failedwodup_import_poll:completed|failed
These events may include batchId, file name, file size, import status, and counts. They must not include raw JSONL text or raw provider payloads.
Completed-workout stats dispatch events:
stats_refresh_dispatch:startedstats_refresh_dispatch:coalescedstats_refresh_dispatch:completedstats_refresh_dispatch:failed
These include the source operation and session id, but never the workout payload. coalesced means another same-user write joined the active bounded drain instead of starting a parallel processor. A failed dispatch is logged as a warning because the canonical write has already committed and the database cron job remains the recovery path.
Edge Function Logs
Vercel authentication handlers emit structured [barbelic server error] entries for configuration, provider exchange, and unexpected request failures. Those entries contain only an event key, timestamp, error name/code, and safe numeric metadata; arbitrary exception messages, provider response text, and credentials are excluded. Their HTTP/native error responses use only application-owned copy, so provider diagnostics do not become error pages.
wodup-start-import, wodup-process-import-jobs, and stats-process-refresh-jobs write structured server logs with:
operation_idbatch_iduser_idstatusduration_mson terminal eventserror_codeon failures- stage counts such as
session_count,session_exercise_count, andset_count
The frontend sends operation_id to the Edge Function in the request body and x-lift-guild-operation-id header, so a failed start call can be matched between browser logs and Supabase function logs.
Wodup import server events:
wodup_import:receivedwodup_import:batch_loadedwodup_import:queuedwodup_import:worker_dispatchedwodup_import:worker_dispatch_failedwodup_import:worker_receivedwodup_import:claimedwodup_import:normalizedwodup_import:stagedwodup_import:importingwodup_import:canonical_completedwodup_import:completedwodup_import:worker_completedwodup_import:worker_job_failedwodup_import:failed
The start request should usually end around wodup_import:queued and wodup_import:worker_dispatched. Longer normalization and canonical import work runs in wodup-process-import-jobs while the UI polls wodup_import_batches.
Stats refresh worker events:
stats_refresh:worker_receivedstats_refresh:worker_completedstats_refresh:worker_failed
DB Stats Job Logs
user_exercise_stats_refresh_jobs remains the write-side lifecycle table. For operational reads, use:
select *
from public.user_exercise_stats_refresh_job_observability
order by updated_at desc
limit 50;The view exposes:
job_iduser_idevent_typesource_refstatusoperation_idduration_mserror_codeerror_messageprocessing_started_atfinished_at
The view is not granted to normal authenticated app users. It is intended for service/admin diagnostics.