Skip to content

feat(trace): :rf.trace/trigger-handler — error events carry triggering-handler source-coord (rf2-3nn8)#408

Merged
mike-thompson-day8 merged 1 commit into
mainfrom
rf2-3nn8-trigger-handler-coord
May 12, 2026
Merged

feat(trace): :rf.trace/trigger-handler — error events carry triggering-handler source-coord (rf2-3nn8)#408
mike-thompson-day8 merged 1 commit into
mainfrom
rf2-3nn8-trigger-handler-coord

Conversation

@mike-thompson-day8
Copy link
Copy Markdown
Contributor

Summary

Extend re-frame2 error traces to carry the source-coord of the handler whose execution produced the error. Tools (10x, pair, IDE jump-to-source) consume the slot to render click-to-jump links straight from any :rf.error/* event to the offending handler's source. The registrar already stamps :ns / :file / :line / :column at registration time; this threads that data through to the emitted error event.

The four locks

Q Lock
Q1 Naming :rf.trace/trigger-handler (nested), :kind + :id alongside the coord. Flattening to :rf.handler/source-coord would conflate "what handler" with "where in source".
Q2 Coverage Optional field. Present when a handler is in scope at emit time (event / sub / fx / cofx / view); absent for outermost-dispatch errors with no handler resolved.
Q3 Site Registration-site coord. Call-site coord (the specific (inject-cofx :foo) line inside the handler body) is a separate follow-up.
Q4 Elision Not separately elided. The trace surface as a whole remains dev-only via interop/debug-enabled?; when an error event is emitted, the field rides along.

Mechanism

Single trace/*current-trigger-handler* dynamic Var that each runtime boundary binds around the user code it runs. emit-error! reads the Var and hoists its value to the top-level :rf.trace/trigger-handler slot when bound. The trigger-handler-from-meta helper picks :ns / :file / :line / :column off the registrar slot's meta map and re-nests them under :source-coord; returns nil when no coord was stamped (programmatic registration) so the slot is omitted rather than populated with placeholder data.

Runtime boundaries that bind

  • router.cljcprocess-event* around the interceptor chain + post-chain phases (db commit, flows, fx walk).
  • fx.cljchandle-one-fx around the user-fx handler call.
  • cofx.cljcinject-cofx around the cofx fn body (both arities).
  • subs.cljcvalidate-and-trace around the sub body-fn invocation.
  • views.cljsreg-view*'s wrapper around the render-fn call.

Interceptor errors flow up to the router's :rf.error/handler-exception emit site, which fires inside the event-runtime binding — so the enclosing handler's coord is what surfaces (interceptors aren't reg-stamped, per the bead's locked design). The no-such-cofx / no-such-fx paths fire while the enclosing event-handler binding is still active, so they carry the event's coord.

Spec changes

  • Spec-Schemas.md §:rf/error-event — optional :rf.trace/trigger-handler added to the canonical Malli shape with the :kind enum, :id keyword, and :source-coord sub-map.
  • 009-Instrumentation.md §The error event shape — per-context coverage table (present vs absent), registration-site (not call-site) coord choice, "no-poison-data on programmatic registration" rule, production-elision wording.
  • docs/guide/14-errors.md — user-facing error-event example updated with the new slot and tool-consumption note.

Files touched

Spec: spec/Spec-Schemas.md, spec/009-Instrumentation.md, docs/guide/14-errors.md

Runtime impl: implementation/core/src/re_frame/trace.cljc (new dynamic Var + helper + emit-error! hoist), router.cljc, fx.cljc, cofx.cljc, subs.cljc, views.cljs

Tests: implementation/core/test/re_frame/trigger_handler_coord_test.clj — 9 deftests / 46 assertions covering the four locks (top-level placement; per-kind coord propagation: event handler-exception, fx handler-exception, sub-exception, no-such-cofx, no-such-fx; negative no-such-handler case; programmatic-registration negative case; field-by-field parity with the registrar's source-coord).

Test plan

  • clojure -M:test green across core (235 tests), schemas (49), machines (114), routing (21), flows (26), ssr (31), epoch (59), http (39)
  • npm run test:cljs green (571 tests)
  • npm run test:browser green (559 tests)
  • npm run test:elision green — the trace surface still DCEs cleanly in :advanced + goog.DEBUG=false
  • npm run test:examples green

Discipline

  • JVM interop preserved — source-coord under JVM continues to use *file* / (meta &form) at the macro path; the per-boundary binding sits on top and works identically on JVM and CLJS.
  • Single-import contract preserved — no new namespace required to read :rf.trace/trigger-handler off an error event; it's a plain map key.
  • No new registries / dispatch types / effect substrates / component substrates. This bead enriches existing trace events; no new primitive.
  • Additive change to the schema — existing error-event consumers that don't read the field are unaffected.

…g-handler source-coord

Extend re-frame2 error traces to carry the source-coord of the handler
whose execution produced the error, so tools (10x, pair, IDE
jump-to-source) can render click-to-jump links from any :rf.error/*
event straight to the offending handler. The registrar already stamps
:ns / :file / :line / :column at registration time; this threads that
data onto the emitted error event.

Locks per the design walkthrough:
- Q1 Naming: :rf.trace/trigger-handler (nested), :kind+:id alongside
  the coord — flattening to :rf.handler/source-coord would conflate
  "what handler" with "where in source".
- Q2 Coverage: optional field. Present when a handler is in scope at
  emit time (event / sub / fx / cofx / view); absent for outermost-
  dispatch errors with no handler resolved (e.g. :rf.error/no-such-
  handler, :rf.error/drain-depth-exceeded after rollback).
- Q3 Site: registration-site coord. Call-site coord (the specific
  (inject-cofx :foo) line inside the handler body) is a separate
  follow-up.
- Q4 Elision: not separately elided. The trace surface as a whole
  remains dev-only via interop/debug-enabled?; when an error event is
  emitted, the trigger-handler field rides along. Apps that keep the
  trace surface in production get the trigger-handler coord with every
  error event.

Mechanism: a single trace/*current-trigger-handler* dynamic var that
each runtime boundary binds around the user code it runs. emit-error!
reads the var and hoists its value to the top-level :rf.trace/trigger-
handler slot when bound. The trigger-handler-from-meta helper picks
:ns / :file / :line / :column off the registrar slot's meta map and
re-nests them under :source-coord; returns nil when no coord was
stamped (programmatic registration) so the slot is omitted rather
than populated with placeholder data.

Boundaries that bind:
- router.cljc — process-event* / process-event! around the interceptor
  chain + post-chain phases (db commit, flows, fx walk).
- fx.cljc — handle-one-fx around the user-fx handler call.
- cofx.cljc — inject-cofx around the cofx fn body (both arities).
- subs.cljc — validate-and-trace around the sub body-fn invocation.
- views.cljs — reg-view*'s wrapper around the render-fn call.

Interceptor errors flow up to the router's :rf.error/handler-exception
emit site, which fires inside the event-runtime binding — so the
enclosing handler's coord is what surfaces (interceptors aren't reg-
stamped). The no-such-cofx / no-such-fx paths fire while the enclosing
event-handler binding is still active, so they carry the event's coord.

Spec changes:
- Spec-Schemas.md §:rf/error-event — adds the optional :rf.trace/
  trigger-handler field to the canonical error-event Malli shape;
  enumerates the :kind enum, the :id keyword, and the :source-coord
  sub-map.
- 009-Instrumentation.md §The error event shape — adds a per-context
  coverage table (when the slot is present vs absent), pins the
  registration-site (not call-site) coord choice, pins the
  "no-poison-data on programmatic registration" rule, and clarifies
  the production-elision wording.
- docs/guide/14-errors.md — updates the user-facing error-event
  example with the new slot and explains the tool-consumption story.

Tests:
- implementation/core/test/re_frame/trigger_handler_coord_test.clj —
  9 tests / 46 assertions covering the four locks. Per error category
  emitted while a handler is in scope (event handler-exception, fx
  handler-exception, sub-exception, no-such-cofx with enclosing event,
  no-such-fx with enclosing event), asserts the trigger-handler shape
  matches the registrar's source-coord field-by-field. Per the
  no-handler-in-scope path, asserts :rf.trace/trigger-handler is
  absent. Plus the top-level-placement test (not under :tags) and the
  programmatic-registration negative path.

Verified:
- clojure -M:test green across core (235 tests), schemas (49),
  machines (114), routing (21), flows (26), ssr (31), epoch (59),
  http (39).
- npm run test:cljs green (571 tests).
- npm run test:browser green (559 tests).
- npm run test:elision green (the trace surface still DCEs cleanly in
  :advanced + goog.DEBUG=false).
- npm run test:examples green.

JVM interop preserved — source-coord under JVM continues to use
*file* / (meta &form) at the macro path; the per-boundary binding sits
on top and works identically on both runtimes. Single-import contract
preserved — no new namespace required to read the slot off an error
event. No new registries / dispatch types / effect substrates /
component substrates.
@mike-thompson-day8 mike-thompson-day8 merged commit 1c3727d into main May 12, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant