feat(react-grab): event log + session replay (devtools)#365
Draft
aidenybai wants to merge 3 commits into
Draft
Conversation
Adds an internal event log that captures every dispatched store action,
serializes element references as stable handles (with selector fallback
for replay), and supports exporting/importing the session for bug repros.
Highlights:
- createEventLog() ring buffer wired into createGrabStore via an action
proxy; nothing logs unless an action is invoked from outside the store
(internal action-to-action calls are not double-logged).
- Pointer / detected-element / viewport-version churn is coalesced
on dispatch so idle hover does not flood the buffer.
- Element args are walked recursively and replaced with { __rgHandle }
markers; on replay we resolve via WeakRef first and selector second.
- New ReactGrabAPI methods: getSession(), replaySession(), clearEventLog(),
setEventLogRecording().
- Optional floating devtools panel mounted into its own shadow root when
window.__REACT_GRAB_DEVTOOLS__ === true. Lists recent events with
coalescing counts and offers copy / download / replay / pause / clear.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Copy as promptReviewed by reactreview for commit 174d66b. Configure here. |
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
commit: |
solid-js/web's delegateEvents() runs at module evaluation and touches
window, which crashed Node imports of the dist bundle. Mirror the
renderer's existing pattern: import('../components/devtools-panel.js')
only when window.__REACT_GRAB_DEVTOOLS__ is set. The panel becomes its
own code-split chunk (~2.25 kB gzip) that never loads otherwise.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…ools-f3b7 Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds an internal event log that records every dispatched store action so a session can be exported as JSON, pasted into a bug report, and replayed against the live store to reproduce the same UI state.
Driven by the brainstorm in the linked chat — scoped intentionally tight. No architectural rewrite, no Foldkit-ification, no plugin restructuring, no Subscriptions/Managed Resources/Submodels. Just the log and the repro mechanism.
How
Action proxy as the log boundary
createGrabStorenow accepts an optionaleventLogand wraps the returnedactionsobject with a small proxy that callseventLog.dispatch(name, args)before invoking the underlying action body. Internal action-to-action calls (e.g.toggle→deactivate) use the unwrapped local reference, so the log never double-counts.Element handles instead of live refs
Element args can't be JSON-serialized, so the log mints a stable
rg-el-Nhandle the first time it sees each element. The registry keeps aWeakMap<Element, handle>for forward lookup, aWeakRef<Element>for reverse lookup, and a unique CSS selector (via the existingcreateElementSelector) as a fallback for replay.On
replaySession, handles are resolved by trying:WeakRefif still alive and connected.document.querySelector(selector)against the live DOM.null(actions are tolerant ofnullelement args today).Online coalescing
Without compaction,
setPointeralone would fill the 5000-entry ring buffer in a few seconds. Dispatch checks the previous entry: if the name is in a small "idempotent setter" allowlist (setPointer,setDetectedElement,setInputText,setFrozenDragRect,updateContextMenuPosition,incrementViewportVersion) or the new args structurally equal the previous, the latest call replaces the prior entry and acoalescedCountis bumped. The dev panel renders this assetPointer ×127.Things that depend on call sequence or count (
addGrabbedBox,toggleFrozenElement,increment/decrementlock depth pairs, etc.) are intentionally not in the allowlist — they stay every call.Public API additions
ReactGrabAPIgains four methods:getSession(): ReactGrabSession— deep-cloned snapshot for export.replaySession(session, options?): Promise<void>— resets the store (deactivate+ clears) then dispatches each event in order;options.realtime: truehonors original timestamps viasetTimeout.clearEventLog(): voidsetEventLogRecording(value): void— pause/resume without losing the buffer.The
noop-apistub implements the same surface.Optional devtools panel
Gated by
window.__REACT_GRAB_DEVTOOLS__ = trueset beforeinit(). Lazy-imported only when the flag is set (mirrors the renderer's existing pattern, sincesolid-js/webtoucheswindowat module load and would break SSR). Mounts its own shadow root (independent of the main overlay) with inline styles and renders a floating event list. Buttons: pause/record, copy JSON, download JSON, replay from clipboard, clear. Each row showsrelativeMs · actionName(args…) · ×count.What this gets you
applyEvents(store, [...])after a session is loaded; assert on the resulting Model.What this explicitly does NOT do
Date.now()/document.activeElement/window.scroll*reads out of action bodies. Replay reproduces the structural state (selection, drag, freezes, context menu) but not byte-exact timestamps inside the Model. Good enough for ~95% of repros.Verified
pnpm --filter react-grab typecheck✓pnpm --filter react-grab build✓pnpm lint✓pnpm format✓pnpm test✓ — all 511 Playwright e2e tests pass (including SSR compatibility — initial run had 13 SSR failures from the eager devtools-panel import; fixed with the lazy import).Bundle impact
__REACT_GRAB_DEVTOOLS__is set.Files
packages/react-grab/src/core/event-log.ts(new) — registry + ring buffer + dispatch + replay.packages/react-grab/src/components/devtools-panel.tsx(new) — gated, lazy-loaded Solid panel.packages/react-grab/src/core/store.ts— acceptseventLog, wrapsactionsif present.packages/react-grab/src/core/index.tsx— creates the log, exposes API methods, lazy-imports the panel when flagged.packages/react-grab/src/core/noop-api.ts— implements the new methods.packages/react-grab/src/types.ts— public types:ReactGrabSession,ReactGrabLoggedEvent,ReactGrabReplayOptions, etc.packages/react-grab/src/index.ts— declareswindow.__REACT_GRAB_DEVTOOLS__.packages/react-grab/src/constants.ts—EVENT_LOG_RING_BUFFER_SIZE,EVENT_LOG_SCHEMA_VERSION.Try it
Summary by cubic
Add an internal event log with session export/replay and an optional, lazy-loaded devtools panel in
react-grabto make bug repros and debugging easy. No end-user impact by default; the devtools panel only loads when enabled and stays SSR-safe.New Features
rg-el-1) with WeakRef and selector fallback; replay tolerates nulls.setPointer) and identical args to reduce noise; rows show ×count.ReactGrabAPImethods:getSession,replaySession(session, options?),clearEventLog,setEventLogRecording.window.__REACT_GRAB_DEVTOOLS__ = true.Bug Fixes
windowaccess during SSR; loads only whenwindow.__REACT_GRAB_DEVTOOLS__is true.Written for commit 174d66b. Summary will update on new commits. Review in cubic