ENG-9166 feat(otel): browser tracing plugin — traceparent per event, web vitals, render timing (3/3)
Part of #6227 (ENG-9166). Stack: 1/3 → 2/3 → 3/3 (this). Base is farhan/eng-9166-reflex-otel-3; the diff is the two commits on top of 2/3.
What
reflex_otel.OtelPlugin (add to rx.Config(plugins=[...])) compiles a small OpenTelemetry web bundle into the frontend:
- every event sent to the backend gets a
PRODUCERspan and a W3Ctraceparent, so one user interaction is one trace: browser → backendSERVERevent → chainedINTERNALevents; - web vitals (
web_vital.LCP/CLS/INP/FCP/TTFB) as spans with value/rating attributes; socket.connect/socket.disconnectspans (unintentional disconnects marked ERROR);- opt-in
render_timing=True:react.renderspan per React commit via a root<Profiler>; the plugin aliasesreact-dom/client → react-dom/profilinginvite.config.jsbecause<Profiler>is a no-op in production builds otherwise. Off by default (span volume). Verified in a--env prodbuild.
Wiring: get_frontend_dependencies (pinned @opentelemetry/*, web-vitals), get_static_assets (utils/otel.js), update_env_json (OTEL entry: endpoint, service name, headers, flags), add_modify_task on entry.client.js (import + root wrap). Endpoint defaults follow OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_ENDPOINT → /v1/traces; must allow CORS. headers are compiled into the public bundle (documented).
Framework seam: state.js calls window.__reflex_otel?.onEventSend/onSocketConnect/onSocketDisconnect — undefined unless the plugin is active.
Docs: new docs/api-reference/observability.md (backend + frontend setup, hot-reload-safe guard, metrics table). Log correlation is out of this stack (follow-up: #6903).
Known limitation: the browser has no per-event completion signal, so the browser event span has no duration; it exists to carry the trace context.
Tests
tests/units/reflex_otel/test_plugin.py (env/endpoint resolution, asset, env.json, entry patch idempotency + warning, profiling alias patch).