feat(middleware): aggregate React render reports and surface component errors - #472
Conversation
…ter host nodes Add getComponentRenders, getProfileTimeline and getErrors to the React agent domain, hide plain host components from tree reads by default, and cap the width of serialized inspected values.
End-to-end verification: completeRan against the playground on an iPhone 17 Pro simulator, driven through the The check that mattered
Session: root Aggregate row for {
"rootId": 2417, "fiberId": 2498, "label": "@c82",
"displayName": "Forget(HomeScreen)",
"renderCount": 4,
"totalDurationMs": 69.30300000000001,
"avgDurationMs": 17.326,
"maxDurationMs": 19.034,
"totalSelfDurationMs": 14.353000000000002,
"slowRenderCount": 4,
"slowestCommitIndex": 0
}
Other results
Two findings unrelated to this PR
|
Description
Makes the
reactagent domain answer render-performance questions in one call, and surfaces the error/warning counts React already reports.getComponentRendersaggregates a whole profiling session into one row per component — render count, total/average/max render time, self time, and why it rendered — sortable five ways and filterable to a single component. Ports the useful part ofagent-react-devtools'slow,rerendersandreportcommands as one tool.getProfileTimelinelists every commit with its duration and rendered-fiber count, chronologically or slowest first, and stays queryable for the life of the session.getErrorslists the components React logged errors or warnings against.TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGSwas previously parsed over and discarded; the counts now also appear on ordinary tree and node reads.noHost(opt-in) ongetTree/getChildren/searchNodeshides plain host components, promoting their children to the nearest visible ancestor.maxValueLength(default 512) ongetComponent/getProps/getState/getHookscaps serialized string width.Related Issue
Closes #473
It implements the profiling and errors/warnings briefs already in
docs/react-agent-features/(04-errors-warnings.md,05-profile-convenience-tools.md,06-host-filtering.md).Context
Why the aggregation tool. Getting from
stopProfilingto "which component is slow" previously meant readingtopSlowCommits, callinggetRenderDataonce per commit at 20 items a page, and aggregating across commits in the agent's head. On a 40-commit session that is 40+ round trips and tens of thousands of tokens, and cross-commit aggregation done by a model is exactly the kind of thing that silently double-counts or drops fibers.getComponentRendersdoes it server-side in one call.getRenderDatais unchanged and remains the way to drill into a single commit.Aggregates are keyed by
(rootId, fiberId), notfiberIdalone — fiber IDs are only unique within a renderer, so merging on ID would invent a component that rendered under two roots. Roots whose data arrived from more than one renderer are excluded and reported insummary.skippedRootIdsrather than dropped silently.Why
noHostis opt-in. It was built default-on, on the assumption that host components dominate a React Native tree. They do not reach us at all:getDefaultComponentFilters()inreact-devtools-corehides host components at the backend, and Rozenite never overrides that, so host fibers are filtered before they hit the wire. Verified on a live session —getTreereturnsViewaselementType: "function"with no host fibers present. The filter is kept as a safety net for the one case where it fires (a DevTools frontend on the same backend having re-enabled host components), defaults to off so existing behaviour is unchanged, and the tool description and docs say plainly that it is usually a no-op. Making it genuinely useful would mean sendingupdateComponentFilters, which re-sends the whole tree and invalidates node IDs mid-session — worth its own change, not this one.Why
maxValueLength.valueDepthbounded nesting but nothing bounded width, so a base64 image or serialized blob in a single prop passed through whole.Refactors, kept to what the feature touched. The cursor encode/decode/validate/slice block was duplicated six times in
store.ts; it is nowpaginateReactListin a newreact/pagination.ts, which the three new tools reuse.ReactCommitDataandReactChangeDescriptionwere defined identically in bothstore.tsandprofiling-store.ts, andReactProfilingCursorPayloadduplicated the local cursor type — all deduplicated. TypinggetProfilingDataSnapshot/getCommitDataon the bridge removed a hand-written structural cast instopProfilingand the deadNumber(x) || 0coercion around valuesgetNumberMapalready normalizes at ingest. Test bridge stubs were three divergent inline objects and are now one builder.Testing
Automated, from the repository root after
git fetch origin main:pnpm checks:affected— typecheck, lint, format all passpnpm test:affected— 50 tasks pass; 60 tests in the React agent domain, 36 of them newNew unit coverage: operations parsing for errors/warnings (including a malformed update that must not abort the batch),
getErrorsordering/pagination/scoping, host filtering (promotion, significant hosts, cursor scoping), the profiling aggregates (cross-commit unioning of change descriptions, per-root fiber-ID isolation, unmounted-fiber fallback, sorting, pagination, missing-data errors), and value truncation.Manual, against the playground on an iPhone 17 Pro simulator driven through the
rozeniteCLI:getComponentRenders' schema is correct.errorCount/warningCountin the default projection, and that paginatednextcommands carry their cursor inside--args.noHostbecoming opt-in.End-to-end verification against a real React Native backend is complete and reported in full in a comment below. The headline: for
Forget(HomeScreen)over a 4-commit session, per-commitactualDurationMsof[19.034, 16.407, 16.989, 16.873]sums to69.30300000000001— bit-for-bit equal to the aggregate'stotalDurationMs— andslowestCommitIndexresolves to the commit reporting the aggregate'smaxDurationMs.getErrors, pagination to exhaustion, truncation, and timeline totals were all confirmed live too.One gap worth naming: the
noHostfiltering path is covered by unit tests only. It cannot execute on a real device, because React DevTools filters host components at the backend before they reach Rozenite — which is why the flag ships defaulting to off.