Skip to content

feat(seer): Add structured LLM context system for Seer Explorer#111554

Merged
azulus merged 16 commits intomasterfrom
mihirmavalankar/feat/seer-structured-page-context
Mar 31, 2026
Merged

feat(seer): Add structured LLM context system for Seer Explorer#111554
azulus merged 16 commits intomasterfrom
mihirmavalankar/feat/seer-structured-page-context

Conversation

@Mihir-Mavalankar
Copy link
Copy Markdown
Contributor

@Mihir-Mavalankar Mihir-Mavalankar commented Mar 25, 2026

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 datauseLLMContext(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 requirementuseLLMContextRegistry() 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 unregisterunregisterNode 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.

@Mihir-Mavalankar Mihir-Mavalankar requested a review from azulus March 25, 2026 20:19
@Mihir-Mavalankar Mihir-Mavalankar self-assigned this Mar 25, 2026
@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Mar 25, 2026
@Mihir-Mavalankar Mihir-Mavalankar changed the title feat(seer): Add structured page context provider for Seer Explorer Draft: feat(seer): Add structured page context provider for Seer Explorer Mar 25, 2026
Comment thread static/app/views/seerExplorer/contexts/pageContext.spec.tsx Outdated
Comment thread static/app/views/seerExplorer/contexts/pageContext.spec.tsx Outdated
Comment thread static/app/views/seerExplorer/contexts/pageContext.spec.tsx Outdated
@azulus azulus changed the title Draft: feat(seer): Add structured page context provider for Seer Explorer ref(seer): Replace PageContext with SeerContextProvider Mar 26, 2026
@azulus azulus marked this pull request as ready for review March 27, 2026 17:51
@azulus azulus requested a review from a team as a code owner March 27, 2026 17:51
Comment thread static/app/views/seerExplorer/contexts/seerContext.tsx Outdated
@azulus azulus force-pushed the mihirmavalankar/feat/seer-structured-page-context branch from 92c5ad0 to ad07f2c Compare March 27, 2026 17:59
@azulus azulus requested review from a team as code owners March 27, 2026 18:09
@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Mar 27, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🚨 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 #discuss-dev-infra channel.

Comment thread static/app/views/seerExplorer/contexts/seerContext.tsx Outdated
Comment thread static/app/views/seerExplorer/contexts/seerContextTypes.ts Outdated
@azulus azulus changed the title ref(seer): Replace PageContext with SeerContextProvider feat(seer): Add structured page context system for Seer Explorer Mar 27, 2026
Comment thread static/app/views/seerExplorer/contexts/llmContext.spec.tsx
Comment thread static/app/views/seerExplorer/contexts/registerSeerContext.tsx Outdated
Comment thread static/app/views/seerExplorer/contexts/registerSeerContext.tsx Outdated
Comment thread static/app/views/seerExplorer/contexts/seerContext.tsx Outdated
Comment thread static/app/views/seerExplorer/contexts/seerContext.tsx Outdated
Comment thread static/app/views/seerExplorer/contexts/seerContext.tsx Outdated
Comment thread static/app/views/seerExplorer/contexts/registerSeerContext.tsx Outdated
Comment thread static/app/views/seerExplorer/contexts/seerContext.tsx Outdated
@azulus azulus self-assigned this Mar 31, 2026
azulus and others added 10 commits March 31, 2026 11:57
…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>
@azulus azulus force-pushed the mihirmavalankar/feat/seer-structured-page-context branch from b14aa34 to f652a70 Compare March 31, 2026 18:57
Comment thread static/app/views/seerExplorer/contexts/llmContext.tsx
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>
Comment thread static/app/views/seerExplorer/contexts/llmContext.tsx Outdated
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>
Comment thread static/app/views/seerExplorer/contexts/llmContext.tsx Outdated
Comment thread static/app/views/seerExplorer/contexts/llmContext.tsx
Comment thread static/app/views/seerExplorer/contexts/llmContext.tsx
@azulus azulus changed the title feat(seer): Add structured page context system for Seer Explorer feat(seer): Add structured LLM context system for Seer Explorer Mar 31, 2026
…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>
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment thread static/app/views/seerExplorer/contexts/llmContext.tsx
@azulus azulus merged commit 193165a into master Mar 31, 2026
66 checks passed
@azulus azulus deleted the mihirmavalankar/feat/seer-structured-page-context branch March 31, 2026 23:25
dashed pushed a commit that referenced this pull request Apr 1, 2026
)

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>
@github-actions github-actions bot locked and limited conversation to collaborators Apr 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Scope: Backend Automatically applied to PRs that change backend components Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants