Skip to content

ENG-9602: feat: add frontend inspector for mapping DOM nodes to Python source#6411

Open
FarhanAliRaza wants to merge 10 commits into
reflex-dev:mainfrom
FarhanAliRaza:inspector
Open

ENG-9602: feat: add frontend inspector for mapping DOM nodes to Python source#6411
FarhanAliRaza wants to merge 10 commits into
reflex-dev:mainfrom
FarhanAliRaza:inspector

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Apr 28, 2026

Copy link
Copy Markdown
Contributor
  • Add frontend_inspector, frontend_inspector_shortcut, and frontend_inspector_editor config options; "dev" mode fails the prod build (validated at Config init and re-checked at compile time).
  • New reflex_base.inspector package: state flag, capture registry that records source location per component, and an integration module owning compile-time wiring.
  • Component.create stamps emitted tags with a data-rx-source id when capture is enabled (Fragments skipped).
  • Vite config and package.json now react to inspector state and are re-rendered each compile; launch-editor pulled in as a dev dep on demand.
  • Extract framework-frame walking from console.py into shared reflex_base.utils.frames, reused by the inspector.
  • Exclude _inspector_id from generated pyi stubs.
  • Docs: new advanced_onboarding/frontend_inspector.md page, sidebar entry, and pointer from the configuration guide.
  • Tests for config validation, runtime gating, capture lifecycle, frame walking, browser contract, and the component data-attribute.

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

New Feature Submission:

  • Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Alt-click actually opens the file in the editor.

image

- Add `frontend_inspector`, `frontend_inspector_shortcut`, and
  `frontend_inspector_editor` config options; "dev" mode fails the prod
  build (validated at Config init and re-checked at compile time).
- New `reflex_base.inspector` package: state flag, capture registry that
  records source location per component, and an integration module
  owning compile-time wiring.
- `Component.create` stamps emitted tags with a `data-rx-source` id when
  capture is enabled (Fragments skipped).
- Vite config and package.json now react to inspector state and are
  re-rendered each compile; `launch-editor` pulled in as a dev dep on
  demand.
- Extract framework-frame walking from `console.py` into shared
  `reflex_base.utils.frames`, reused by the inspector.
- Exclude `_inspector_id` from generated pyi stubs.
- Docs: new `advanced_onboarding/frontend_inspector.md` page, sidebar
  entry, and pointer from the configuration guide.
- Tests for config validation, runtime gating, capture lifecycle, frame
  walking, browser contract, and the component data-attribute.
…n alt

- Build inspector script src and runtime URLs through prepend_frontend_path
  so the script and its assets load when the app is hosted under a base
  path (e.g. docs at /docs/). Inspector JS now reads the URLs from the
  window config instead of hardcoding /__reflex/...
- Editor middleware matches /__open-in-editor as a substring so it still
  fires when the request comes in prefixed.
- Dedupe capture entries by (file, line, column, component); identical
  call sites now share an id, cutting the docs source-map from ~200MB to
  ~600KB.
- Require alt at click time and add a 350ms post-focus guard so
  re-focusing the browser window doesn't hijack a click into 'open in
  editor'; hide the overlay on blur to avoid stale snap on return.
- Update the inspector docs to reflect alt+click.
@FarhanAliRaza FarhanAliRaza requested review from a team and Alek99 as code owners April 28, 2026 20:03
@codspeed-hq

codspeed-hq Bot commented Apr 28, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing FarhanAliRaza:inspector (77626c4) with main (b288bdc)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a dev-only frontend inspector that maps rendered DOM nodes back to their Python Component.create call sites. It is activated via rx.plugins.FrontendInspectorPlugin() in rxconfig.py, automatically disabled in production, and comprises a frame-walking capture registry, a deterministic source-map writer, a Vite plugin for editor middleware, and a browser overlay script.

  • New reflex_base.inspector package: state (global on/off toggle), capture (frame-walking registry keyed by stable hashes), and shortcut (config parser). Component.create stamps each component with a data-rx id at page-evaluation time; the plugin writes the full id→location map to public/__reflex/source-map.json at compile time.
  • inspector.js overlay: hover highlighting and configurable modifier-key-click-to-open-in-editor, with session-persistent toggle and fresh source-map re-fetch on each re-activation.
  • frames.py extracts framework-root discovery from console.py into a shared utility, reused by both the inspector and the deprecation-warning frame walker.

Confidence Score: 5/5

Safe to merge; the inspector is gated entirely behind a dev-mode check and is a no-op in production builds.

The feature is well-isolated: the plugin is inactive in production, the compiler change is minimal, and the implementation is backed by comprehensive tests covering capture, state, shortcut, browser contract, and Playwright integration. The noted issues are theoretical edge cases with no impact on current usage.

capture.py (empty-tuple sentinel) and frontend_inspector.py (silent Vite anchor miss) deserve attention before future refactoring of those modules.

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/plugins/frontend_inspector.py Core plugin wiring: Vite config injection, static asset emission, head-script injection, and launch-editor dev dep. Silent failure mode when Vite config anchors are absent is the main fragility.
packages/reflex-base/src/reflex_base/inspector/capture.py Frame-walking capture registry with deterministic hash IDs. _ensure_framework_roots uses falsy-empty-tuple guard that would busy-rediscover roots if discover_framework_roots ever returns ().
packages/reflex-base/src/reflex_base/inspector/state.py Global enabled flag with lazy initialisation and correct env-var lifecycle. Worker-process fallback via source-map file detection is well-designed.
packages/reflex-base/src/reflex_base/assets/inspector/inspector.js Browser overlay and keyboard shortcut toggle. Previous thread issues (hardcoded altKey, stale source map) are both addressed.
packages/reflex-base/src/reflex_base/components/component.py Adds _inspector_id field and stamps it in create; _get_tag_props guards against Fragment and uses setdefault so user-set data-rx is not overridden.
packages/reflex-base/src/reflex_base/utils/frames.py New shared stack-walking utilities extracted from console.py. Clean design with a cached predicate and distribution-aware root discovery.
packages/reflex-base/src/reflex_base/assets/inspector/dev_server_middleware.js Vite dev-server middleware forwarding /__open-in-editor to launch-editor. file param without path validation is intentional for a dev-only tool.
reflex/compiler/compiler.py Adds start_compile plugin lifecycle hook call before the main compilation progress loop. Minimal, correct change.
packages/reflex-base/src/reflex_base/utils/console.py Refactors _get_first_non_framework_frame to use the new shared frames utilities; behaviour is preserved.

Reviews (2): Last reviewed commit: "fix underscore links" | Re-trigger Greptile

Comment thread packages/reflex-base/src/reflex_base/assets/inspector/inspector.js Outdated
Comment thread packages/reflex-base/src/reflex_base/assets/inspector/inspector.js Outdated
…prod

frontend_inspector="dev" is already inert under REFLEX_ENV_MODE=prod
(is_active gates every emission site), so the validate() raise just
broke shared rxconfigs that get reused across run modes — including
the docs CI which runs reflex run --env prod against a config that
keeps the inspector on for local dev.
Cache the source map per modifier-press/click so edits are picked up without reloading the page, and gate clicks on the configured shortcut (alt/ctrl/meta/shift) instead of hard-coding altKey.
Comment on lines +263 to +268
frontend_inspector: FrontendInspectorMode = "off"

frontend_inspector_shortcut: str = "alt+x"

frontend_inspector_editor: str = ""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would rather this be implemented as a plugin, and the config options belong in that plugin, similar to sitemap

@masenf masenf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree with full implementing this as a plugin.

not sure if it's possible, but the first thing i wished this had was the ability to see the stack of where the component was instantiated. maybe i want to jump to the component instantiation? or maybe i want to jump into the page function or sidebar function that called the helper.

lot of potential here though; hopefully we can integrate this into claude code's preview browser to automatically select context from UI 🤤

Move every dev-only inspector concern (head scripts, vite plugin file + vite.config.js splice, launch-editor dev dep, browser asset copy, source map emit, runtime state toggle) into FrontendInspectorPlugin. Add a single new Plugin.start_compile(app) hook because pre_compile fires after page evaluation — too late to reset capture or extend app.head_components before compile_document_root reads them.

Drops the frontend_inspector / *_shortcut / *_editor BaseConfig fields, the inspector= arg on vite_config_template, the now-unused inspector/integration.py, inspector/emit.py, compiler/inspector_plugins.py, and the dead refresh_framework_roots / discover_framework_roots_fast pair.
@FarhanAliRaza

FarhanAliRaza commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

we currently send this data for each component and dedupe it.

    "565": {
        "file": "/home/farhan/code/reflex/docs/app/reflex_docs/docgen_pipeline.py",
        "line": 625,
        "column": 1,
        "component": "AccordionContent"
    },

We can store whatever data it is our will, but if we do alot of work it will slow down compilation and increase size of the source map that we send to the frontend. Now for the docs app we send about 500+ kb. That is for development so size is, I think, not an issue.

@FarhanAliRaza FarhanAliRaza requested review from adhami3310 and masenf May 1, 2026 18:05
Comment thread packages/reflex-base/src/reflex_base/plugins/base.py Outdated
Align the plugin start_compile hook with the other lifecycle hooks by
threading the app through a typed context kwargs dict instead of a
positional argument.
@masenf masenf changed the title feat: add frontend inspector for mapping DOM nodes to Python source ENG-9602: feat: add frontend inspector for mapping DOM nodes to Python source May 26, 2026
@FarhanAliRaza

Copy link
Copy Markdown
Contributor Author

Need to test it with memoized components.

FarhanAliRaza and others added 2 commits May 27, 2026 01:21
Dynamic components compile in a one-shot subprocess and re-render in a
freshly spawned Granian worker, so the worker never sees the parent's
in-memory registry or env writes. Hash (file, line, column, component)
to a stable id and use source-map.json on disk as the enable signal,
so data-rx values emitted at request time match the ids the compile
pass wrote.
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.

3 participants