feat(seer): Add structured LLM context system for Seer Explorer#111554
feat(seer): Add structured LLM context system for Seer Explorer#111554
Conversation
92c5ad0 to
ad07f2c
Compare
|
🚨 Warning: This pull request contains Frontend and Backend changes! It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently. Have questions? Please ask in the |
…et data Silences lint warnings on chained property access in snapshot assertions while still failing the test if a node is unexpectedly absent. Adds type/unit fields to DummyWidget's useSeerContext call so the nesting test verifies that multi-field payloads survive the full register → write → snapshot round-trip. Co-Authored-By: Claude <noreply@anthropic.com>
Replaces the per-property assertions in the nesting test with a single deep equality check against the expected tree, so a failure prints the full actual vs expected shape in one diff. Co-Authored-By: Claude <noreply@anthropic.com>
…eer contexts static/app/components/dnd/ was introduced by the explore team but missed a CODEOWNERS entry. Attributed to @getsentry/explore. seerExplorer/contexts/ files are infrastructure with no production callers yet. Added as knip entry points (same pattern as components/pipeline/) to suppress unused-file/unused-export warnings until feature adoption begins. Co-Authored-By: Claude <noreply@anthropic.com>
…on update Two bugs caught by Cursor: 1. unregisterNode only dispatched to the reducer (removing nodes from state) but never cleaned nodeDataRef. With SeerContextProvider at the app root, each mount/unmount cycle permanently leaked the node's data entry. Fix: mirror the reducer's collectDescendantIds removal against nodeDataRef before dispatching. 2. updateNodeData shallow-merged incoming data with the existing entry. Since useSeerContext(data) always passes the complete data object for the component, merging retains keys dropped between renders. Fix: replace the entry outright (nodeDataRef.current.set(nodeId, data)). Co-Authored-By: Claude <noreply@anthropic.com>
SeerContextNode.data was always initialized to {} in the REGISTER_NODE
case and never read — buildTree and serializeState both read from the
provider's imperative nodeDataRef instead. Removing it eliminates the
false signal that data lives in the reducer state.
Co-Authored-By: Claude <noreply@anthropic.com>
…ield
registerSeerContext used P extends Record<string, unknown> which forced
all call sites to cast their component as any to satisfy the constraint.
Relaxing to P extends Record<PropertyKey, unknown> lets TypeScript infer
the wrapped component's prop types correctly and removes all the as any
casts in the HOC and tests.
Also removes the dead data field from SeerContextNode. It was always
initialized to {} in REGISTER_NODE and never consulted — buildTree and
serializeState read exclusively from the provider's nodeDataRef.
Co-Authored-By: Claude <noreply@anthropic.com>
…st in registerSeerContext P extends Record<PropertyKey, unknown> caused a TypeScript error when spreading props in JSX, because PropertyKey includes symbol keys which are not valid JSX prop keys. Switch to Record<string, unknown> to constrain to JSX-compatible keys. Restore the `as P as any` cast on WrappedComponent JSX spread — this is the established pattern across all Sentry HOCs due to a known Emotion incompatibility (emotion-js/emotion#3261). The cast was incorrectly removed in a prior commit. Co-Authored-By: Claude <noreply@anthropic.com>
Pure file renames to make the context system generic and reusable beyond Seer. Also removes the now-inlined seerContextReducer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename Seer* to LLM* exports for reusability beyond Seer. Replace useReducer with pure ref-based state since reactivity is not needed. Switch to React useId, add strict LLMContextNodeType union, support polymorphic data arguments, add try/catch for JSON.stringify, and make useLLMContextRegistry throw outside provider. Co-Authored-By: Claude <noreply@anthropic.com>
b14aa34 to
f652a70
Compare
When JSON.stringify threw (e.g. circular reference), serialized was set to '' which matched prevDataRef's initial value, so updateNodeData was never called. Use null sentinel for the catch path so non-serializable data is always written through. Co-Authored-By: Claude <noreply@anthropic.com>
Use Map.has() check instead of ?? operator when reading from
nodeDataRef, so explicit null values written via useLLMContext(null)
are preserved in snapshots rather than silently converted to {}.
Co-Authored-By: Claude <noreply@anthropic.com>
…lues updateNodeData now increments version so consumers using it as a change token detect data-only mutations. Non-serializable values (e.g. circular references) are replaced with a placeholder object so getSnapshot() remains JSON-serializable. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
) Adds infrastructure for Seer Explorer to read structured semantic state from the currently rendered page — without scraping the DOM. ## What Three cooperating primitives: **`LLMContextProvider`** — drop-in root provider (mounted in the app shell). Owns a flat node registry stored in refs and exposes read/write operations via internal React context. Uses pure ref-based state — no reducer, no re-renders on registration — since consumers read data imperatively via `getSnapshot()`. **`registerLLMContext(nodeType, Component)`** — HOC that auto-registers a component as a named node on mount and removes it (plus all descendants) on unmount. `nodeType` is strictly typed (`LLMContextNodeType`) for typeahead and to prevent naming drift. Nesting follows React component hierarchy automatically via `LLMNodeContext`, which carries each component's `useId()`-generated node ID downward so child HOC wrappers can declare their `parentId` synchronously during render. **`useLLMContext(data)`** / **`useLLMContext()`** — write and read overloads. The write overload accepts any non-undefined value (objects, arrays, strings, numbers — polymorphic) and pushes it into the nearest registered context node. The read overload returns `getLLMContext(componentOnly?)` for full-tree or subtree snapshots. Named `LLMContext` (not `SeerContext`) since the system is generic and could be used by any LLM integration. ## Why Seer Explorer needs to understand what the user is currently looking at (dashboard, widgets, charts, etc.) to give grounded AI responses. This system lets any component opt in by wrapping with `registerLLMContext` and calling `useLLMContext(data)` — no manual tree wiring, no DOM inspection. ## Design notes - **Flat storage, lazy tree assembly** — nodes stored as `Map<id, {nodeType, parentId}>` in a ref; tree assembled at `getSnapshot()` time. Avoids ordering dependencies: a child can declare its `parentId` before the parent's registration effect has fired. - **Imperative ref for data** — `useLLMContext(data)` writes to a `useRef<Map>` rather than dispatching state updates. This sidesteps a fundamental timing issue: child effects fire before parent effects, so data writes happen before `registerNode`. The ref is always read fresh at `getSnapshot()` time. - **Zero re-renders** — the provider uses refs for all state and `useCallback(fn, [])` for all operations. The memoized context value is referentially stable, so neither the provider nor its consumers ever re-render from context changes. - **Strict context requirement** — `useLLMContextRegistry()` throws if called outside the provider (which lives at the app root), treating missing context as a bug rather than silently returning undefined. - **JSON dedup with circular-reference safety** — write path uses `JSON.stringify` equality to skip redundant writes, with a `try/catch` that falls back to always-write for non-serializable values. - **Cleanup on unregister** — `unregisterNode` removes descendant entries from both the node map and the data ref so stale entries don't accumulate. ## Tests 9 integration tests covering: empty state, nesting (Dashboard → Widget → Chart with full shape assertion), unmount cleanup, data updates across re-renders, non-object data types (strings, arrays, numbers), full-tree vs `componentOnly` subtree reads. --------- Co-authored-by: Claude Sonnet 4 <noreply@anthropic.com> Co-authored-by: Jeremy Stanley <git@azulus.com> Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>

Adds infrastructure for Seer Explorer to read structured semantic state from the currently rendered page — without scraping the DOM.
What
Three cooperating primitives:
LLMContextProvider— drop-in root provider (mounted in the app shell). Owns a flat node registry stored in refs and exposes read/write operations via internal React context. Uses pure ref-based state — no reducer, no re-renders on registration — since consumers read data imperatively viagetSnapshot().registerLLMContext(nodeType, Component)— HOC that auto-registers a component as a named node on mount and removes it (plus all descendants) on unmount.nodeTypeis strictly typed (LLMContextNodeType) for typeahead and to prevent naming drift. Nesting follows React component hierarchy automatically viaLLMNodeContext, which carries each component'suseId()-generated node ID downward so child HOC wrappers can declare theirparentIdsynchronously during render.useLLMContext(data)/useLLMContext()— write and read overloads. The write overload accepts any non-undefined value (objects, arrays, strings, numbers — polymorphic) and pushes it into the nearest registered context node. The read overload returnsgetLLMContext(componentOnly?)for full-tree or subtree snapshots.Named
LLMContext(notSeerContext) since the system is generic and could be used by any LLM integration.Why
Seer Explorer needs to understand what the user is currently looking at (dashboard, widgets, charts, etc.) to give grounded AI responses. This system lets any component opt in by wrapping with
registerLLMContextand callinguseLLMContext(data)— no manual tree wiring, no DOM inspection.Design notes
Map<id, {nodeType, parentId}>in a ref; tree assembled atgetSnapshot()time. Avoids ordering dependencies: a child can declare itsparentIdbefore the parent's registration effect has fired.useLLMContext(data)writes to auseRef<Map>rather than dispatching state updates. This sidesteps a fundamental timing issue: child effects fire before parent effects, so data writes happen beforeregisterNode. The ref is always read fresh atgetSnapshot()time.useCallback(fn, [])for all operations. The memoized context value is referentially stable, so neither the provider nor its consumers ever re-render from context changes.useLLMContextRegistry()throws if called outside the provider (which lives at the app root), treating missing context as a bug rather than silently returning undefined.JSON.stringifyequality to skip redundant writes, with atry/catchthat falls back to always-write for non-serializable values.unregisterNoderemoves descendant entries from both the node map and the data ref so stale entries don't accumulate.Tests
9 integration tests covering: empty state, nesting (Dashboard → Widget → Chart with full shape assertion), unmount cleanup, data updates across re-renders, non-object data types (strings, arrays, numbers), full-tree vs
componentOnlysubtree reads.