Skip to content
File

Blob: docs/client-sentry-runtime-config-plan.md

Markdown392 lines

Client-side Sentry Runtime Config Plan

Summary

This plan covers client-side Sentry only. Worker-side Sentry is out of scope; Cloudflare Workers Observability remains the backend observability path.

The chosen approach is runtime HTMLRewriter injection that exposes public client config through the SPA shell:

  • turnstile_site_key
  • sentry_dsn

This keeps Worker Env as the source of truth for public runtime config, avoids spreading build-time public env vars across local machines and CI, and keeps client bootstrap on a single runtime path.


Goals

  • Add client-side Sentry without adding Worker-side Sentry.
  • Keep reportClientError as the single vendor-neutral reporting entry point.
  • Make Turnstile and Sentry read from the same public runtime config source.
  • Avoid requiring VITE_SENTRY_DSN or VITE_TURNSTILE_SITE_KEY in every build environment.
  • Avoid unnecessary platform complexity unless the benefit clearly justifies it.

Current Constraints

  • The SPA shell is served from static assets.
  • In wrangler.jsonc, run_worker_first now covers all document routes except hashed assets under /assets/*.
  • The Worker now has an ASSETS binding and document navigations pass through Worker code first.
  • TURNSTILE_SITE_KEY already lives in Worker Env.
  • reportClientError already exists as the intended vendor-neutral capture surface.
  • React 19 is in use, so root-level error hooks are available in createRoot(...).

Options Explored

1. Build-time import.meta.env

Shape

  • VITE_TURNSTILE_SITE_KEY
  • VITE_SENTRY_DSN

Advantages

  • Smallest implementation.
  • Best fit for Sentry's recommended "initialize before the rest of the app" browser setup.
  • No extra runtime request.
  • No SPA shell or routing changes.

Drawbacks

  • Public config lives in a second channel outside Worker Env.
  • Values are frozen into the built client bundle and require rebuilds to change.
  • Public config has to exist anywhere production-like assets are built.
  • This repo already has the Turnstile public key in Worker Env, so build-time injection would introduce config duplication immediately.

Decision

Rejected. This is the smallest code diff, but it spreads public config into build infrastructure and moves bland away from Worker Env as the configuration source of truth.


2. Runtime HTMLRewriter injection

Shape

  • Add an ASSETS binding.
  • Change SPA document routing to Worker-first.
  • Fetch /index.html from env.ASSETS.
  • Inject window.__BLAND_PUBLIC_CONFIG__ into the shell with HTMLRewriter.

Advantages

  • Worker Env remains the single source of truth.
  • No extra client fetch.
  • Public config exists before app boot, which is the cleanest runtime fit for browser Sentry initialization.
  • Turnstile and Sentry can share the same bootstrap object.
  • Removes the startup/failure-recovery hazard of making auth and reporting depend on a separate config request.

Drawbacks

  • Requires changing bland from asset-first document serving to Worker-first document serving.
  • Requires routing logic in src/worker/index.ts for shell requests and static file pass-through.
  • Delivers client config through SPA shell transformation logic.
  • Cloudflare documents this as a valid pattern, but it changes how every HTML navigation is served and should therefore remain tightly scoped.

Decision

Chosen. The extra routing work is justified because it removes a hard bootstrap dependency while still keeping public config in Worker Env.


3. Runtime /api/v1/config endpoint

Shape

  • Add unauthenticated GET /api/v1/config.
  • Return public config from Worker Env.
  • Client fetches config at startup and auth pages reuse the same loader.

Advantages

  • Keeps Worker Env as the source of truth.
  • No build-time VITE_SENTRY_DSN requirement.
  • Matches the existing bland API architecture.
  • Easy to test with focused worker and client unit tests.

Drawbacks

  • Adds one startup config request.
  • Browser Sentry is not initialized at the exact first line of client code.
  • Login and invite pages must wait for config before mounting Turnstile.
  • Makes bootstrap and failure recovery depend on a second request path even though the data is already available synchronously in Worker Env.

Decision

Rejected. This path worked mechanically, but the extra request was the wrong tradeoff once it became part of startup-critical bootstrap and auth recovery behavior.


4. Hybrid HTMLRewriter + /api/v1/config

Shape

  • Inject config into the shell when available.
  • Fall back to /api/v1/config if bootstrap data is missing.

Decision

Rejected as unnecessary complexity for v1. If the Worker bootstrap is missing, bland should treat that as a shell/bootstrap failure for that page load and should not carry a second config transport path.


Chosen Design

Public API

Inject:

<script>
  window.__BLAND_PUBLIC_CONFIG__ = {
    turnstile_site_key: "1x00000000000000000000AA",
    sentry_dsn: null,
  };
</script>

Notes:

  • turnstile_site_key is required.
  • sentry_dsn is nullable so local and test environments can omit it.
  • The bootstrap data is public and unauthenticated.
  • The injected JSON should be escaped for inline-script safety.
  • Shell caching is acceptable because these public config values change rarely; this design does not require overriding cache headers to no-store.

Implementation Plan

1. Shared contract

Add a shared bootstrap schema in src/shared/types.ts:

  • PublicClientConfig
    • turnstile_site_key: string
    • sentry_dsn: string | null

This keeps the public config shape typed on both the client and worker sides.


2. Worker shell bootstrap

Implement public-config bootstrap in the Worker shell path.

Behavior:

  • Add assets.binding = "ASSETS" in wrangler.jsonc.
  • Change assets.run_worker_first to ["/*", "!/assets/*"].
  • Keep not_found_handling = "single-page-application".
  • In src/worker/index.ts:
    • keep /parties/* on Partyserver / DocSync routing
    • keep /api/* and /uploads/* on the existing Hono app path
    • pass direct non-HTML asset requests like /favicon.svg through env.ASSETS.fetch(request)
    • fetch the document request through env.ASSETS for document GET requests
    • inject window.__BLAND_PUBLIC_CONFIG__ into <head> with HTMLRewriter

Environment changes:

  • Add optional SENTRY_DSN to .dev.vars.example
  • Regenerate worker-configuration.d.ts

This keeps public runtime config fully inside Worker Env while making it available before the client app boots.


3. Client config loader

Implement the client config module in src/client/lib/client-config.ts as a synchronous bootstrap reader.

Responsibilities:

  • Read window.__BLAND_PUBLIC_CONFIG__
  • Parse it once with the shared schema
  • Cache the parsed result or parse error for the lifetime of the page
  • Expose:
    • getClientConfigSnapshot(): PublicClientConfig | null
    • getClientConfigErrorSnapshot(): Error | null

This module becomes the shared path for:

  • main.tsx bootstrapping
  • Turnstile auth pages
  • future client-side public config consumers

4. reportClientError and Sentry bootstrap

Expand src/client/lib/report-client-error.ts into the browser reporting control point.

Behavior:

  • Always log to console.error for local debugging.
  • Normalize unknown into a consistent error object.
  • Keep the existing source + context call contract so product code stays vendor-neutral.
  • Queue captured events until the lazy @sentry/react import finishes.
  • If sentry_dsn is present:
    • lazily import @sentry/react
    • initialize Sentry once
    • flush queued events through Sentry
  • If sentry_dsn is absent, or bootstrap config is missing/invalid:
    • remain console-only
    • clear any queued events because there is no Sentry destination for this page load

Filtering:

  • Add a helper that declines expected API errors such as:
    • validation_error
    • unauthorized
    • forbidden
    • not_found
    • turnstile_failed
  • Report only high-signal handled failures:
    • render crashes
    • unhandled promise rejections
    • bootstrap config failures
    • Turnstile script/render failures
    • swallowed background failures that currently disappear silently
    • unexpected 5xx/transport failures in selected UI flows

This keeps the integration Sentry-specific internally while preserving a vendor-neutral surface in app code.


5. Startup sequence

Update src/client/main.tsx.

Boot order:

  1. Register early global listeners:
    • window.addEventListener("error", ...)
    • window.addEventListener("unhandledrejection", ...)
  2. Read window.__BLAND_PUBLIC_CONFIG__ synchronously through client-config.ts.
  3. Prime the reporter immediately from that bootstrap config.
  4. Start the existing session refresh bootstrap.
  5. Render the app after the current auth bootstrap completes.

React 19 root error hooks:

  • Use createRoot(..., { onUncaughtError, onCaughtError, onRecoverableError })
  • Each hook calls reportClientError(...)
  • Do not wire createRoot(...) directly to Sentry-specific helpers; keep it going through the local abstraction
  • Treat the root hooks as the reporting path for React render errors. Existing local ErrorBoundary components remain responsible for scoped fallback UI, not duplicate reporting.

This preserves the current startup model while still capturing early client failures through the local queue.


6. Turnstile integration

Replace the current import.meta.env-backed constant in src/client/lib/constants.ts.

Changes:

UI behavior:

  • If bootstrap config is present, auth pages mount Turnstile immediately.
  • If bootstrap config is missing or invalid, show a clear inline error and do not attempt to mount Turnstile.
  • The auth UI should offer reload and should not background-retry config.
  • TurnstileWidget should call reportClientError(...) on:
    • script load failure
    • missing window.turnstile after script load
    • widget render failure

This aligns Turnstile with the same public config path as Sentry.


7. Client caller sites to update

Keep reporting focused on high-signal failures rather than every user-facing error toast.

Update these sites:

Do not add reportClientError(...) to normal expected form/API failures in:

  • login-page.tsx
  • invite-page.tsx
  • workspace-settings.tsx
  • profile-settings.tsx
  • share-dialog.tsx

Those flows already surface user-actionable errors and would mostly create noise.


8. Tests and verification

Worker tests

Add focused worker tests covering:

  • shell bootstrap injection into /index.html
  • safe escaping of injected JSON
  • routing splits for:
    • /parties/*
    • /api/*
    • /uploads/*
    • direct asset paths
    • document GET / HEAD requests

Client tests

Add focused tests for:

  • synchronous bootstrap parsing and memoization
  • missing / invalid bootstrap handling
  • reportClientError queueing and flush behavior during Sentry lazy-load
  • filtering of expected vs high-signal errors

Verification commands

  • npm run typecheck
  • npm run test
  • npm run build

Assumptions

  • Worker-side Sentry remains out of scope.
  • SENTRY_DSN is optional in local/test environments.
  • reportClientError remains the only app-level error reporting API.
  • Worker-first shell delivery is acceptable here because it removes a startup-critical config fetch and does not change hashed asset delivery.
  • Missing earliest possible pre-module capture is still an acceptable tradeoff; the important change is eliminating the separate runtime config request from bootstrap.

Final Decision Rationale

HTMLRewriter-based shell bootstrap is the best balance of correctness, operational simplicity, and platform safety for bland.

It keeps public runtime config in Worker Env and avoids duplicating it in build-time VITE_* variables.

It gives the browser the public config it needs before the client app starts, without adding a second runtime config transport.

It does add routing logic to the Worker shell path, but that complexity stays localized and keeps bootstrap behavior explicit and deterministic.