Skip to content

feat(react-devtools): inspect React components and profile renders through the Metro inspector - #3613

Open
thymikee wants to merge 15 commits into
mainfrom
feat/react-devtools-profiler
Open

thymikee wants to merge 15 commits into
mainfrom
feat/react-devtools-profiler

Conversation

@thymikee

@thymikee thymikee commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

agent-device react-devtools is now built-in React Native component inspection and render profiling. Every command attaches to the app's JavaScript runtime through its Metro dev server (CDP Runtime.evaluate) and drives the React DevTools backend that development builds already ship. The app, Metro config and Babel config need no changes, and there is no helper to start or wait for.

Components

$ agent-device react-devtools find ContactRow --exact --limit 1
60 mounted components match "ContactRow":
  @c1213 ContactRow [memo] key=.$0 in ContactList < MemoDefeatedScenario
  +59 more (--limit 60)
$ agent-device react-devtools inspect PerfLabScreen
@c1169 PerfLabScreen key=memo-defeated
rendered by: PerfLabRoute(./perf-lab.tsx) @c1168 < WrappedScreenComponent @c1167 < Route(perf-lab) @c1158 < SceneView @c1148 < …
props:
  initialScenario: "memo-defeated"
hooks:
  State: "memo-defeated"
  State: 0
  State: "idle"
$ agent-device react-devtools errors
1 mounted component logged errors or warnings while rendering:
  @c14 ContextNavigator: 2 warnings
    warning: `expo-router/unstable-native-tabs` is deprecated. Use `expo-router/native-tabs` instead.
  • tree [@ref|Name] [--depth] [--limit] prints the mounted tree with @c refs. Wrapper chains such as providers collapse onto one line, and runs of three or more same-shape siblings (list rows) into one counted line, so the 1,510-component Expo Router test app fits in about 25 lines.
  • find <Name> [--exact] lists matches with the components that rendered them.
  • inspect <@ref|Name> prints owners, props, state, hooks and logged problems. A name that several components share fails closed and lists their refs.
  • errors lists components that logged console errors or warnings while rendering, with the messages.

The tree comes from the backend's operations stream (names, keys, child order, error and warning counts). Props, state and hooks come from its inspectElement. When another DevTools frontend owns that stream, the tree commands fail with tree-unavailable instead of returning a partial tree.

Render profiling

$ agent-device react-devtools profile start
$ agent-device react-devtools profile stop
React profile 4.7s: 5 commits, 2091ms rendering, 5 over 16ms (worst #1 438ms)
1. memo-defeated ContactRow: memo bypassed on 300/300 updates; new highlight (object) 300x, onSelect (function) 300x from ContactList -> wrap those props in useCallback/useMemo in ContactList
More: profile report [Component] | profile commit [N]
  • stop prints ranked findings and saves the raw recording.
  • report and commit re-analyze the saved recording, so no view loses detail another trimmed.
  • report with no name lists components by self time with why they rendered; report <Component> shows each variant (Row and memo Row stay separate) with its findings.
  • commit with no index lists commits in order (the slowest ones when capped); commit <N> lists every component that rendered in it.

Findings carry the component kind, so a finding about memo Row never explains a slow commit spent in plain Row. A renderer whose profiling data cannot be read fails stop with collect-failed, or adds a note when other renderers were read.

Each subcommand rejects options it does not read. If one app is connected from several devices, every command fails closed until --metro-device names one.

Removed: the npm exec agent-react-devtools passthrough, its 8097 remote tunnel and port reverse, and the injected-dispatch seam only that port reverse used.

Validation

At 4178ccd7b1, check:affected --run (4,589 tests), check:production-exports and check:di-seams pass.

Setup What ran Result
Android test app, RN 0.88-rc (Expo Router) tree, find, inspect by ref and name, errors, ambiguous names, inspect during a profile, all profile views pass
iOS simulator, bare RN 0.83.1 every inspection command, plus all 7 Perf lab scenarios through profile start/stop/report/commit pass, each scenario's expected finding
iOS simulator, bare RN 0.83.1 with react-devtools-core 8.0.0 swapped in (no RN release ships DevTools 7+ yet; 0.88 and the 0.89 nightly pin ^6.1.5) tree, find, inspect, errors, profile with mounts while recording pass, with operations naming. The previous build failed tree here; 6.1.5 rechecked after the change
Android, bare RN 0.76.9 tree, inspect (hooks), errors, profile pass; errors notes that the list can be incomplete
Android, bare RN 0.74.7 tree, find, inspect, errors, profile (memo-defeated) pass; same errors note
agent-device proxy, client with its own state dir profile start/stop, drill-downs pass; the saved profile stays on the client
Remote bridge: agent and Metro in a Linux container (colima), emulator and daemon on the macOS host, app traffic through a local companion bridge connect --remote-config, open, inspection and profiling from the container pass, through the session's Metro binding

Found and fixed while verifying:

  • Debugger URL behind a tunnel. A tunnel shows Metro its own loopback address, which Metro then puts in webSocketDebuggerUrl. The CDP socket now opens on the server that listed the target.
  • Metro choice on a bridge. Inspection and profiling use explicit flags, then the session's Metro binding, then localhost:8081. They never use a device-facing URL (metroPublicBaseUrl or the bridge).
  • errors on older backends. React DevTools 5 (RN 0.74 and 0.76) does not attribute every console call to a component, so errors now says its list can be incomplete instead of claiming nothing was logged.

By design, not changed: Metro accepts the debugger socket only from its own machine (local socket and loopback Origin). The companion bridge opens upstream WebSockets without an Origin, so Metro refuses debugger sockets sent through the bridge. That keeps the bridge from exposing JavaScript execution in the app, so the companion stays as it is. The removed 8097 tunnel did something else: it carried the app's DevTools backend to a frontend on the host.

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-3613/

Built to branch gh-pages at 2026-10-15 18:06 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.24 MB 5.28 MB +48.5 kB
Package (unpacked) 5.24 MB 5.28 MB +48.5 kB
Package (download) 1.57 MB 1.59 MB +16.2 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.7 ms 26.6 ms -0.1 ms
CLI --help 77.6 ms 75.6 ms -2.0 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 25 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread website/docs/docs/debugging-profiling.md Outdated
Comment thread website/docs/docs/debugging-profiling.md Outdated
Comment thread src/cli/react-profiler/react-profile-command.test.ts Outdated
Comment thread src/cli/react-profiler/react-profile-analysis.ts Outdated
Comment thread src/cli/react-profiler/react-profiler-runtime.fixtures.ts Outdated
Comment thread src/cli/react-profiler/react-profile-format.ts Outdated
Comment thread src/cli/react-profiler/react-profile-command.ts Outdated
Comment thread src/cli/react-profiler/react-profile-command.ts Outdated
Comment thread src/cli/react-profiler/react-profile-command.ts Outdated
Comment thread src/cli/react-profiler/react-profile-command.ts Outdated
@thymikee

Copy link
Copy Markdown
Member Author

At f87e64a, the Metro-inspector transport and injected runtime look like real new capability, but two problems need fixing before merge.

selectReactNativeTarget ignores the session's device (https://github.com/callstack/agent-device/blob/f87e64a/src/cli/react-profiler/metro-inspector.ts#L124). When one Metro serves the same app on two devices, it picks the last one listed (.at(-1)), and react-profile-command.ts never passes a device to narrow it. So --session qa1 react-devtools profile start can record on qa2's runtime, and qa1's stop can save another device's profile under react-profiles/qa1.json. Nothing errors, so the findings get attributed to the wrong session. The rule: start, status and stop must each resolve to exactly one runtime on the session's device, and ambiguity across devices must fail closed the way ambiguity across apps already does at line 115. Please enforce it in selectReactNativeTarget, the one selector every subcommand uses, by throwing INVALID_ARGS when candidates span more than one device and nothing narrows them. A test with two devices for the same app that expects INVALID_ARGS would cover it.

Help and docs say "a development build on React Native 0.74 or newer" for every platform (https://github.com/callstack/agent-device/blob/f87e64a/src/commands/schema/cli-help.ts#L308), but the PR body says iOS was not verified. The remote-bridge branch at react-profile-command.ts:149 is new code that was never run, and help does not say so, unlike debugging-profiling.md. Please either show an iOS simulator run on the Perf lab through the CLI argv (react-devtools profile start, run the burst, profile stop printing the scenario's expected first finding, then profile report <Component> from the saved profile), or narrow help and docs to Android. For the remote bridge, either show a run with metroPublicBaseUrl where /json/list is fetched from the public URL and stop succeeds, or add the same "not verified" note to help.

Is about 1,970 production lines (3,536 gross) the right shape for one PR, given the 700 net-production and 1,000-line gross budgets in docs/agents/pull-requests.md? The transport and injected runtime (about 600 lines) are the new capability and nothing else owns them, so I would keep those. Could the findings engine instead live upstream, with a Metro/CDP transport mode added to agent-react-devtools, which already owns slow, rerenders, report, timeline, export and diff? If it must live here, could this be stacked: transport, runtime and start/stop with the stop summary first, drill-down formatters and subcommands second, so each slice's exports have consumers? For the upstream route, an ADR or issue should first decide where render-profile analysis belongs. For the stack, only a re-base onto feat/test-app-perf-lab is needed.

Not blocking, and you can take or leave these: the stateDir ?? process.cwd() fallback at react-devtools.ts:202 would write react-profiles/ into the cwd, the as unknown as string[] cast at metro-inspector.ts:158, and stop swallowing describeProps errors at react-profile-command.ts:310.

The Lint & Format failure is the PR's own. The di-seams gate fails on fetchImpl?: typeof fetch at react-profile-command.ts:65, which this diff adds, and the connect and write options are the same kind of test-only seam. Please remove those seams from ReactProfileCommandOptions rather than marking them approved. The Smoke Tests failure is the iOS "Preflight iOS runner" job with daemon_startup_failed. The diff touches no daemon, startup or iOS runner code, so it is likely unrelated. I did not run a device, so iOS behavior, /json/list ordering across devices and the remote-bridge Origin handling are unchecked. No conflicts were reported.

Before merge, the lint failure and the two findings above need fixing.

Of the open inline threads, the P2 threads on #3613 (comment) (context renders), #3613 (comment) (inferred effect chain), #3613 (comment) (start-reset test), #3613 (comment) (duplicate instance counts), #3613 (comment) (emoji fixture length), #3613 (comment) (help "effect sets state"), #3613 (comment) (help "unrelated context"), #3613 (comment) (unused disconnect handle), #3613 (comment) (lastId only), #3613 (comment) (prop candidate cap), #3613 (comment) (timing-only effect claim), #3613 (comment) (top mounts only) and #3613 (comment) (stale react-native topic) still stand. Also, 10 lower-priority P3 threads still apply. No thread is outdated, so none need resolving.

@thymikee

Copy link
Copy Markdown
Member Author

At f25394b07e (this PR) and b11b5ade89 (#3618):

Device ambiguity: selectReactNativeTarget, which every subcommand uses, now fails closed with INVALID_ARGS when candidates span more than one device. The hint lists the device names, and --metro-device <name> narrows to one. Two tests cover it, one in the selector and one through the command.

iOS: shown through the CLI on a bare RN 0.83 iOS simulator. Each scenario ran as profile start, the burst, wait, then profile stop, and each printed its expected first finding (7/7). After that, profile report ProductTile from the saved profile printed both of its findings and its costliest commits. Help and docs now say "verified on Android emulators and iOS simulators".

Remote bridge: not run. Help now says remote bridge sessions are not verified yet, matching the docs.

Lint: removed the fetchImpl, connect and write seams from ReactProfileCommandOptions and the fetch parameter from listMetroInspectorTargets. Tests stub fetch, spy on InspectorSession.connect and capture stdout instead. check:di-seams passes.

Shape: restacked as you suggested. This PR keeps the transport, runtime, analysis and start/stop/status with the stop summary. #3618 adds the drill-down formatters and subcommands (377 lines), and every layer's exports have consumers (check:production-exports passes on both). I kept the analysis here rather than upstream: the summary is the product surface, and it depends on agent-device's own Metro/session resolution. agent-react-devtools' slow/rerenders/report work over its @c tree from its own daemon, which this path doesn't run.

Non-blocking items:

  • A missing stateDir now falls back to the daemon state dir, not the cwd.
  • The WebSocket cast became one named constructor type with a comment saying why.
  • A describeProps failure now shows as a Note: line in the summary instead of being swallowed.

All 25 inline threads are answered and resolved.

Re-validated on the final top-of-stack build: Android RN 0.88-rc, 7/7.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 16 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/cli/react-profiler/metro-inspector.ts Outdated
Comment thread src/cli/react-devtools/react-profile-command.test.ts
Comment thread website/docs/docs/commands.md Outdated
Comment thread src/cli/commands/react-devtools.ts Outdated
Comment thread src/cli/react-profiler/react-profile-analysis.ts Outdated
@thymikee

Copy link
Copy Markdown
Member Author

I reviewed f25394b again. The code looks good, and the seams that failed Lint & Format (di-seams) at f87e64a are gone from ReactProfileCommandOptions. Most earlier wording, test and flag-parsing items are now fixed. 17 of 20 checks are still queued or running, and none has failed. The PR touches only the React profiler, its command, help, docs and skills, so the daemon and Android-helper files in the delta come from the base move and cannot explain a failure here. No conflicts. I did not run a device. The iOS and Android runs you reported are not verified by me, and no run with two devices on one Metro was reported, so --metro-device is covered only by unit tests over a synthetic /json/list.

Not blocking: when one app runs on several devices, every session sharing that Metro gets INVALID_ARGS until --metro-device is passed (https://github.com/callstack/agent-device/blob/f25394b/src/cli/react-profiler/metro-inspector.ts#L137), so you could default the filter when the session's device name equals exactly one candidate's deviceName and keep failing closed otherwise. You can take or leave this.

Several inline threads from cubic-dev-ai still stand. #3613 (comment) (duplicate-name hint) is the one to fix first: when two devices share a deviceName, the INVALID_ARGS hint must list a value that --metro-device matches uniquely (deviceKey or logicalDeviceId whenever names collide). Please add a test with two same-named devices that passes that value. Next come #3613 (comment) (weak selector test), #3613 (comment) ("over budget" wording), #3613 (comment) (state dir env) and #3613 (comment) (mount cost double-count). The other threads are fixed at this head, or moved to #3618 or judged harmless, and you can resolve them: r4272372132, r4272372164, r4272372176, r4272372179, r4272372188, r4272372199, r4272372206, r4272372231, r4272372240, r4272372246, r4272372259, r4272372269, r4272372288, r4272372296, r4272372312, r4272372331, r4272372386 and r4272372402 are fixed. r4272372222 is deliberate, because a replacement runtime reuses the attached backend. r4272372279, r4272372349, r4272372357, r4272372365 and r4272372393 left this PR for #3618. r4272372326 is harmless, because the docs line is general enough that omitting a zero count does not mislead.

Before merge, fix the duplicate-name hint above, then wait for CI to finish, including check:di-seams and check:production-exports.

Base automatically changed from feat/test-app-perf-lab to main October 14, 2026 17:01
@thymikee
thymikee force-pushed the feat/react-devtools-profiler branch 4 times, most recently from b7eb909 to f1f454b Compare October 14, 2026 17:18
@thymikee

Copy link
Copy Markdown
Member Author

At 7c60d2db8f, rebased onto main after #3612 merged:

  • Duplicate-name hint (r4273400840): the hint now lists each device's name when it is unique and its logical device id when names collide. --metro-device matches either value. A test covers two simulators with the same name and selects one by its key.
  • r4273400870, r4273400899, r4273400936: fixed (connect-URL assertion, "commits over the 16 ms frame budget" wording, per-group mount cost).
  • r4273400917: no longer applies, because f1f454b removed the stateDir fallback.
  • Defaulting to the session's device: not taken. An agent-device device name is often different from the deviceName that Metro reports (for example, a simulator UDID or an AVD name versus the model name), so a match would be by chance. Failing closed with values that select one device uniquely is safer.

check:affected --run, check:production-exports and check:di-seams pass locally.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/cli/react-profiler/react-profile-renders.ts Outdated
Comment thread website/docs/docs/commands.md Outdated
Comment thread src/cli/react-profiler/react-profile-renders.ts Outdated
Comment thread src/cli/react-profiler/react-profile-renders.ts Outdated
Comment thread src/cli/react-profiler/react-profile-renders.ts Outdated
Comment thread src/cli.ts
Comment thread src/cli/react-devtools/react-profile-analysis.ts
Comment thread src/cli/react-profiler/react-profile-analysis.ts Outdated
Comment thread docs/adr/0029-daemon-policy.md
Comment thread src/commands/perf/index.ts Outdated
@thymikee

Copy link
Copy Markdown
Member Author

At 6fb23c14fe, the 11 Cubic threads from the review at 54120679f8 are handled. 10 are fixed, with tests:

  • Renders the app could not name stay out of components and findings. The runtime no longer writes an Unknown entry.
  • Animated(...) wrappers keep their name, so they stay framework components.
  • Memo(Row) and Row are separate components.
  • A prop whose instances disagree on its value kind claims no new identity.
  • memo-defeated evidence counts each changed prop.
  • eager-mount is costed over every qualifying commit of a component.
  • Devices without a logical id are keyed by their target id, not their display name.
  • Docs name --bundle-url. The perf guidance and ADR 0029 are corrected.

The metroBearerToken thread does not apply: that token authorizes the companion proxy's control API, not the public dev-server URL that the profiler reads.

All 41 review threads have a reply and are resolved. check:affected --run, check:production-exports and check:di-seams pass locally.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 18 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/commands/schema/command-overrides.ts Outdated
Comment thread src/cli/react-profiler/react-profile-format.ts Outdated
@thymikee

Copy link
Copy Markdown
Member Author

The PR is ready at 639d73d. The issues from the earlier review (f25394b) are fixed: the device key now uses the target-id prefix, the 16 ms frame budget wording is correct, and the render analysis fixes landed. CI is green with 22 checks and none failing, and there are no conflicts.

Not blocking, and you can take or leave these: the waiver reason in scripts/fuzz/validation-arbitraries-cli.ts#L37 and the predicate name shouldPassThroughLocalToolFlag in src/cli/parser/args.ts#L138 still describe a local tool that this PR removed, so they should say the options go to the react-devtools profile parser. The removal of react-devtools status/start/stop/wait/get/find/errors (shipped since v0.21.x) is not marked as breaking, so feat!: or a BREAKING CHANGE footer would put it in the release notes. The report --json findings filter at src/cli/react-profiler/react-profile-command.ts#L465 repeats the name filter in react-profile-format.ts#L153, so one helper beside findComponents could serve both. PR #3618 is still open although its drill-down content is now commit 5412067 here, so it can be closed with a pointer to this PR.

On the open inline threads from the other reviewer: all of them are fixed at this head except one, #3613 (comment) (passing effectiveFlags in src/cli.ts), which is benign because the profiler reads the public dev-server URL and help and docs list remote bridges as unverified. These threads can be resolved: #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment).

I ran no device for this review. The Android RN 0.74-0.88, iOS 0.83 and drill-down runs come from the PR body, with no commit named for each. No run with two devices on one Metro was reported, so the target-id-prefix device key rests on unit tests over a synthetic /json/list. I could not confirm that public Metro URLs on remote bridges need no bearer token for /json/list. Nothing else is needed before merge.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 14, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 15 files (changes from recent commits).

Tip: Get a Slack DM when cubic finishes reviewing your PRs. Connect Slack

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread website/docs/docs/commands.md Outdated
Comment thread src/cli/react-devtools/react-devtools-runtime.ts Outdated
Comment thread src/cli/react-devtools/react-profile-analysis.ts Outdated
Comment thread src/cli/react-devtools/react-profile-analysis.ts Outdated
@thymikee

Copy link
Copy Markdown
Member Author

The fixes since the earlier review (639d73d) look good at a127024, and I found no new problems in the code. Smoke Tests is still running, not failed, and this change touches nothing on the daemon or device smoke route, so I see no overlap. There are no conflicts. The live evidence in the PR body is from the earlier head.

Before merge, please address or dismiss the four open Cubic P2 threads, which all still apply: the report component wording, the started-interface failure count, the quadratic finding filter, and the double-counted subtree time. Then let Smoke Tests finish.

Not blocking, and you can take or leave it: src/__tests__/cli-react-devtools-session.test.ts:65 still runs react-devtools profile status, which this change removed, and the test only passes because runReactDevtoolsCommand is mocked, so a current subcommand like profile start would test the real routing. I also did not check whether React DevTools getProfilingData throws or returns empty data on an interface whose startProfiling failed; only the throw case still reaches collect-failed at runtime.ts:296.

@thymikee thymikee removed the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 15, 2026
@thymikee thymikee changed the title feat(react-devtools): profile React renders through the Metro inspector feat(react-devtools): inspect React components and profile renders through the Metro inspector Oct 15, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 38 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/cli/react-devtools/react-devtools-runtime.test.ts Outdated
Comment thread src/cli/react-devtools/react-devtools-inspect-command.test.ts Outdated
Comment thread src/commands/schema/cli-help.ts Outdated
Comment thread src/cli/react-devtools/react-component-inspect.ts Outdated
Comment thread src/cli/react-devtools/react-devtools-inspect-command.ts Outdated
Comment thread src/cli/react-devtools/react-component-tree.ts Outdated
Comment thread website/docs/docs/commands.md Outdated
Comment thread src/cli/react-devtools/react-devtools-inspect-command.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 38 files (changes from recent commits).

Requires human review: Auto-approval blocked because this review re-detected 2 unresolved issues already reported by Cubic.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/cli/react-devtools/react-devtools-connection.ts
Comment thread src/cli/react-devtools/react-devtools-connection.ts Outdated
Comment thread src/cli/react-devtools/react-component-inspect.ts
Comment thread src/cli/react-devtools/react-component-tree.ts
Comment thread src/cli/react-devtools/react-component-tree.ts Outdated
@thymikee

Copy link
Copy Markdown
Member Author

The four profiler threads from the earlier review are still open at 77e9159, and the delta adds a second feature on top: component inspection (tree, find, inspect, errors). I found no new correctness problem in that code, but the profiler fixes are not done. The earlier review asked you to address or dismiss the four open profiler threads: #3613 (comment), #3613 (comment), #3613 (comment) and #3613 (comment). None has a reply or a fix yet.

Two more open P2 threads still apply and are worth fixing in this PR: #3613 (comment) (shared port parsing) and #3613 (comment) (host-only fallback). Nine more P2 threads and two P3 threads from the same reviewer still apply, and the sanitizer and inspect-error threads should be fixed through one shared helper. One thread does not apply and you can resolve it: #3613 (comment) (state data of null means a function component has no state, so printing nothing is correct).

Would it be simpler to ship the profiler alone with those fixes, then land component inspection as a stacked PR on the already extracted react-devtools-connection.ts? Each PR would then sit nearer the 700-line threshold, and the new surface would get its own review round. The split point is bc041b3: the profiler commits up to a127024 plus the connection-module extraction, then inspection on top. Either way, please export one control-character sanitizer from capture-kit instead of adding a third local copy. The transport ownership itself is well placed, and I found no smaller owner for it.

Not blocking, and you can take or leave these: the delta is roughly 720 new production lines, so if inspection stays here, a line in the PR body on why the two must ship together would help. Also, the Connection paragraph in cli-help.ts and the matching text in commands.md say every react-devtools command is verified on Android emulators and iOS simulators with RN 0.74+. The PR body says inspection was verified only on Android with RN 0.88-rc, so please scope that sentence to profiling, or run tree, inspect @ref and errors once on an iOS simulator and once on RN 0.74 and paste the output.

I did not run the live device commands, so the Android validation at 77e9159 is your report. No conflicts are known. The eager-closure-budgets Coverage failure looks unrelated: it lists rows that #3625 added on main and #3623 (8c01a37) later removed, and this PR touches neither the budget file nor those packages. Rebasing on 8c01a37 should clear it. The iOS Smoke failure in testRegularSnapshotStillRepairsBackgroundAndDisclosesPriorState also looks unrelated, since the diff changes no Swift, runner or daemon snapshot code. Before merge, please fix the four profiler threads, decide on the split, and rebase on main.

@thymikee

Copy link
Copy Markdown
Member Author

Addressed at 2acde20. All 17 Cubic threads have a reply and are resolved: 16 fixed and 1 dismissed (state data of null means no class state, as you noted).

  • Profiler. stop reads back only the renderers that started profiling. Findings are indexed once per analysis. A subtree that an accounted subtree contains is no longer counted twice, because profiles now carry each rendered element's ancestors. Each of these has a test that fails on the old code.
  • Inspection. Sibling runs collapse only when their whole subtrees match, logged problems included. App-provided names, keys, values and messages go through one printable() helper. errors reports messages unavailable with the reason, and an empty result keeps total.
  • Metro flags. The shared parsePort refuses 8081oops. A host or port flag on its own keeps the rest of the session binding.
  • Docs and help. Help limits the connection requirement to the commands that attach; the commands docs describe the report variants and when the tree collapses rows.
  • Session test. It now routes profile start and asserts the forwarded arguments.

Gates: check:affected --run (4,586 tests), check:production-exports and check:di-seams pass. Live re-check on the Android test app passed: profile stop, tree <Name>, errors and find.

On splitting: the maintainer asked to keep profiling and inspection in this one PR, so inspection stays here rather than in a stacked PR.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 21 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/cli/react-devtools/react-profile-renders.ts Outdated
Comment thread src/cli/react-devtools/react-profile-renders.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 15 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread website/docs/docs/debugging-profiling.md Outdated
@thymikee

Copy link
Copy Markdown
Member Author

Thanks for the update. At 4178ccd the code looks good: the issues from the earlier review (#3613 (comment)) are fixed, and no blocking findings remain in the code.

Not blocking, and you can take or leave it: src/cli/react-devtools/react-devtools-text.ts:5 adds a new local control-character regex in printable(). The earlier review asked for one shared sanitizer from capture-kit (collapseControlCharacters in packages/capture-kit/src/snapshot/snapshot-lines.ts:124, currently private). It is not a copy, since capture-kit folds whitespace, removes controls and trims, while this one maps each control to a space. Either export one helper and call it from react-devtools, or note in the PR why the two must differ.

Three Cubic threads still apply: #3613 (comment) (sanitize at output), #3613 (comment) (sanitize at output), and #3613 (comment) (docs Metro qualifier). The rule for the first two is to sanitize only where text leaves for output, and to keep raw text for identity and lookup. The third needs debugging-profiling.md:88 to say that bridge sessions fall back to metroPublicBaseUrl when no binding is recorded, as cli-help.ts:313 and commands.md:660 already do. The other threads are fixed at this head and can be resolved: #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment), #3613 (comment) and #3613 (comment). The thread at #3613 (comment) was dismissed on purpose: a null state.data means a function component has no class state, so printing nothing is correct.

The PR now has a merge conflict with main (35c21b6) in src/metro/client-metro-companion.test.ts. Main moved that test, and this PR edits it to drop the react-devtools companion cases. Only the CodeQL jobs are queued and the rest of CI has not started, so no check has failed yet.

I did not run any device, proxy or remote-bridge command, and I did not run the test suite. The live-validation rows in the PR body, including the RN 0.74/0.76 errors note and the bridge run through metroPublicBaseUrl, are your reports without pasted output. I also could not confirm that a bridged session's recorded binding always points at the host-local Metro, because recordMetroSessionHintsAfterOpen can record device-facing hints when no metro prepare ran. Reload already shares that path, so it is not new here. The logsTracked heuristic was checked only against the fake runtime, not real React DevTools 5 or 6 backends.

Before merge, please rebase on main to clear the conflict so CI can run. Then fix or resolve the three open Cubic threads above.

@thymikee
thymikee force-pushed the feat/react-devtools-profiler branch from 4178ccd to dc23e5a Compare October 15, 2026 17:22
… cost repeated eager mounts, and count changed props

- Elements the profile cannot name stay out of components and findings instead of folding into one Unknown.
- Animated(...) wrappers keep their name so they stay framework components; Memo(Row) and Row are separate components.
- A prop whose inspected instances disagree on its kind claims no new identity.
- memo-defeated evidence counts each changed prop; eager-mount costs every qualifying commit of a component.
- Devices without a logical id are keyed by target id before display name.
- Docs name --bundle-url; perf guidance and ADR 0029 no longer point at a daemon command react-devtools does not send.
… identity through analysis, and fold the views into report and commit

- profile stop saves the raw recording with prop kinds and notes; report and commit
  analyze it again, so a commit lists every component that rendered in it
- findings carry the component kind, so a finding about memo Row no longer explains
  a slow commit spent in plain Row
- a renderer whose profiling data cannot be read fails stop (collect-failed) or,
  when others were read, adds a note instead of saving an empty profile
- profile surface is start, stop, report [Component], commit [N]: report without a
  name replaces slow and rerenders, commit without an index replaces timeline;
  status and --sort are gone
…nnection

- react-devtools tree [@ref|Name], find <Name> [--exact], inspect <@ref|Name>, and errors
  read the component tree, props, state, hooks, owners, and errors and warnings logged
  while rendering through the same in-app runtime and Metro connection as profiling
- the runtime keeps keys, child order, and per-component error and warning counts from
  the operations stream; inspect calls the backend's inspectElement
- tree collapses wrapper chains onto one line and runs of same-shape siblings into one
  counted line; a name several components share fails closed with their refs
- the profiler moves to src/cli/react-devtools with one shared connection module
…g, tree runs, output safety, and Metro flags

- stop reads back only the renderers that started profiling
- slow-commit accounting indexes findings once and counts a subtree inside an accounted
  subtree only once (profiles now carry each rendered element's ancestors)
- tree collapses only siblings whose whole subtrees match, logged problems included
- app-provided names, keys, values, and messages print without control characters
- errors says when messages could not be read; an empty result keeps total
- --metro-port must be a whole number; a host or port flag alone keeps the rest of the
  session binding
…refer the session's Metro on bridges, and flag untracked logs

- the CDP socket opens on the server that listed the target: behind a tunnel Metro advertises
  its own loopback address, which a client elsewhere cannot reach
- a remote bridge session uses its recorded Metro server next to the agent before the public URL
- errors notes that its list can be incomplete when the backend does not attribute every log to
  a component (React DevTools 5, seen on React Native 0.74 and 0.76)
- help and docs: verified through agent-device proxy and on remote bridge sessions
…the local Metro on bridges

Profile analysis keys components and prop keys by their raw text; control
characters are replaced only in the printed lines, so names that differ only
by them no longer merge.

Metro accepts the debugger socket only from its own machine (local socket and
loopback Origin), so a device-facing URL such as metroPublicBaseUrl or the
remote bridge can never carry it. Drop that fallback: resolve explicit flags,
then the session's Metro binding, then localhost:8081.
…nd later

DevTools 7 added a name-prop field to each non-root ADD and Suspense tree and
Activity slice ops (8-13). The runtime stopped decoding at the first of them,
so tree, find, and errors failed as if another frontend owned the tree, and
profiles fell back to name lookups. Renderer interfaces gained
supportsTogglingSuspense in the same release; the runtime reads the ADD
length from it and skips the new ops by their encoded lengths.

Verified on an RN 0.83.1 iOS simulator with react-devtools-core 8.0.0 swapped
in, and again on its shipped 6.1.5.
@thymikee
thymikee force-pushed the feat/react-devtools-profiler branch from dc23e5a to af282ef Compare October 15, 2026 17:47
@thymikee

Copy link
Copy Markdown
Member Author

This PR is ready at af282ef. The fixes from the earlier review (#3613 (comment)) are in, and I found no new problems in the code.

Not blocking: in src/cli/react-devtools/react-devtools-runtime.ts (https://github.com/callstack/agent-device/blob/af282ef/src/cli/react-devtools/react-devtools-runtime.ts#L59), addLength picks the 7- or 8-field ADD layout from the supportsTogglingSuspense capability, not from a protocol or version fact. A backend that ships one without the other would shift the ops cursor, and a stray string id in 1..13 could be read as an op. Could the layout key on the bridge protocol or version if the hook exposes one, or check that the ADD length lands on a known op? You can take this or leave it.

The three automated review threads (#3613 (comment), #3613 (comment) and #3613 (comment)) are fixed at this commit, so please resolve them. The key sanitizing now happens at output, render identity uses raw display names, and the docs and help describe the flags, binding, then localhost:8081 order.

Smoke Tests is still running and has no failure. This change does not touch what Smoke drives. It only changes runReactDevtoolsCli in the shared src/cli.ts dispatch, and Smoke does not run react-devtools. I did not run the test suite or any device, proxy or bridge command.

I could not check the op lengths in skippedLength or the 8-field ADD against react-devtools-core 7 or 8 source, only against your fixture, which encodes the same assumptions as the parser. The live runs (8.0.0 in RN 0.83.1, and the remote bridge through the session binding) are your reports without pasted output. The PR body's gate run is stated at 4178ccd, not at af282ef.

As noted before, recordMetroSessionHintsAfterOpen can still record a device-facing host (for example from open --metro-host) as the session binding when no Metro prepare ran. react-devtools would then fail with a typed error against that host.

Smoke Tests must finish green at af282ef before merge. Nothing in the code holds it back.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 15, 2026
@thymikee
thymikee removed this pull request from stack #3614 October 15, 2026 19:21
@thymikee
thymikee added this pull request to stack #3695 October 15, 2026 19:22

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant