Skip to content

feat(seer): give the profile embed a real block level - #123901

Merged
billyvg merged 9 commits into
masterfrom
claude/seer-embed-profile
Sep 9, 2026
Merged

feat(seer): give the profile embed a real block level#123901
billyvg merged 9 commits into
masterfrom
claude/seer-embed-profile

Conversation

@billyvg

@billyvg billyvg commented Sep 9, 2026

Copy link
Copy Markdown
Member

The profile schema already declared level: ['inline', 'block'], but both levels rendered the same bare link. This gives the block level a real preview: a metadata strip above a 200px flamechart, from a single fetch of the profile payload.

One of four independent branches adding single-resource Seer embeds. The others (event, log, conversation) touch the same shared files — schemas.ts, index.ts, the stories page, and the generated widget JSON — so whichever merges first will make the rest need a trivial rebase.

What the block renders

  • Header — the existing inline ProfileLink, a local Left-heavy / Time-ordered toggle, and an "Open in Profiling" button that deep-links the previewed viewport.
  • Metadata strip — transaction, duration, threads, environment, release, OS, device, received.
  • FlamechartFlamegraphPreview in a fixed-height container.

Notes for review

Two page-level couplings are deliberately avoided, per the rule in embeds/README.md that an embed must not touch the host page's URL or history:

  • importProfile is called directly instead of mounting ProfileGroupProvider, which reads ?tid= off the host URL — in a Seer conversation that would silently pick the thread from an unrelated query param.
  • The metadata comes from the already-fetched payload rather than useProfileEvents, which resolves its scope from host page filters that have nothing to do with a specific profile. There is no metadata-only endpoint, so the full payload is fetched either way and the flamechart is close to free once it is.

The view toggle is local useState, with a test asserting it never writes to the router.

Splitting the embed into a directory follows the convention in .claude/skills/seer-embed, and keeps the flamegraph renderer and speedscope model layer out of the bundle until a block actually renders.

Testing

profile.spec.tsx covers the inline href, the block metadata strip and flamechart, the toggle staying local, and degrading to the link on a 404 and on a continuous-chunk payload. The whole embed suite passes (19 suites / 101 tests); oxlint, oxfmt and typecheck are clean.

The flamechart renders unmocked under jsdom — jest-canvas-mock is already in setupFiles.

Closes https://linear.app/getsentry/issue/CW-1945/add-profile-embed

🤖 Generated with Claude Code

https://claude.ai/code/session_016YUmhMZLo8geXJa93daPEQ

The `profile` schema already declared `level: ['inline', 'block']`, but
both levels rendered the same bare link. The block now previews the
profile: a metadata strip (transaction, duration, threads, environment,
release, OS, device, received) above a 200px flamechart, from a single
fetch of the profile payload.

Splits the embed into a directory per the embeds convention, so the
flamegraph renderer and speedscope model layer stay out of the bundle
until a block actually renders.

Two page-level couplings are deliberately avoided: `importProfile` is
called directly rather than mounting `ProfileGroupProvider`, which reads
`?tid=` off the host URL, and the metadata comes from the payload rather
than `useProfileEvents`, which reads host page filters that have nothing
to do with a specific profile. The left-heavy/time-ordered toggle keeps
its state local, with a test asserting it never touches the router.

Claude-Session: https://claude.ai/code/session_016YUmhMZLo8geXJa93daPEQ
The profile embed's story rendered the schema's static example, whose
profile id is made up — so the new block level always 404'd there and
fell back to the bare link. Swap it for a `ProfileEmbedStory` that pulls
the org's most recent transaction-based profile, matching what the other
block-level embeds (release, replay, savedQuery) already do.

`has:profile.id` keeps the search off continuous profiles, which are
addressed by profiler id plus a time range and so cannot be expressed in
this embed's schema.

Claude-Session: https://claude.ai/code/session_01PHE7yFBmGH1Rv1nmzGqwys
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Story previews

Preview the stories changed in this PR on the Vercel deployment:

Preview deployment: https://sentry-6hq4imsc6.sentry.dev

The profile schema was written when both levels rendered the same link, so
its description says nothing about the levels and its single example is
implicitly inline. Level is decided by placement -- a tag alone in a
paragraph is block, a tag inside a sentence is inline -- so the description
and the per-level examples are the only things that teach the agent to put
a profile on its own line. Without them the new block preview would rarely
be what renders.

Follows how `issue` and `replay` document their levels, including the "do
NOT duplicate any of that data as text" instruction that keeps the agent
from restating the metadata strip in prose.

Claude-Session: https://claude.ai/code/session_01PHE7yFBmGH1Rv1nmzGqwys
@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Sep 9, 2026
@billyvg

billyvg commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

bugbot review

@linear-code

linear-code Bot commented Sep 9, 2026

Copy link
Copy Markdown

CW-1945

@cursor cursor 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.

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

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 8009a56. Configure here.

Comment thread static/app/components/seer/markdown/embeds/components/profile/profileBlock.tsx Outdated
"Open in Profiling" encoded the previewed viewport as `fov` alongside
`type: 'flamechart'`, but the flamegraph page's query decoder has no `type`
key -- it picks call-order vs left-heavy from `sorting`, which defaults to
call order. The preview defaults to left-heavy, so the rect was replayed
against a differently sorted tree and pointed at unrelated frames.

Send `sorting` instead, from a single `VIEW_MODE_SORT` map that also feeds
the preview's own FlamegraphModel, so the two cannot drift apart again.

The spec only asserted the button existed, which is how this got through;
it now asserts the href carries the sort, in both toggle positions.

Claude-Session: https://claude.ai/code/session_01PHE7yFBmGH1Rv1nmzGqwys
@billyvg
billyvg marked this pull request as ready for review September 9, 2026 16:51
@billyvg
billyvg requested a review from a team as a code owner September 9, 2026 16:51
billyvg added a commit that referenced this pull request Sep 9, 2026
Adds an `event` Seer markdown embed for a single error event inside an
issue. Inline renders a compact link; block renders the event's title,
message, culprit and context.

One of four independent branches adding single-resource Seer embeds —
see also #123901 (profile). They all touch `schemas.ts`, `index.ts`, the
stories page and the generated widget JSON, so whichever merges first
leaves the rest needing a rebase plus a `pnpm gen:embed-widgets` re-run.

### The `view` field

A single event holds exactly one value per tag, so showing "just that
tag" on its own would say nothing. `view` therefore controls what the
block adds *beneath* the summary:

| `view` | Renders |
| --- | --- |
| `summary` (default) | the event on its own |
| `tags` | the event's full tag list |
| `tag` + `tagKey` | how that tag is distributed across the whole issue
|

`tag` without a `tagKey` falls back to `summary` and issues no request —
there's a test for that.

### Notes for review

- Tag links point at `distributions/`, not the legacy `tags/` paths,
which only redirect there.
- The block fetches via `groupEventApiOptions` with an explicitly empty
`environments`, rather than the `useGroupEvent` hook, which reads
`useLocation()` — an embed must not inherit the host page's filters.
Same reasoning as the README rule about not touching host page state.
- `EventTagsView` falls back to a plain key/value list when the events
API omits `projectSlug`, since `EventTagsTree` needs one to load the
project it renders rows against.
- `TagDistribution` is pure, so the `tag` view cannot reach the host
page's URL.

### Testing

8 tests covering both inline title forms, the block summary, both tag
views, the no-`tagKey` fallback, the missing-`projectSlug` fallback, and
the error state. The whole `seer/markdown` suite passes (24 suites / 113
tests); oxlint and oxfmt are clean, and `pnpm gen:embed-widgets`
produces no diff.

Closes
https://linear.app/getsentry/issue/CW-1958/add-error-instance-embed

🤖 Generated with [Claude Code](https://claude.ai/code)

https://claude.ai/code/session_016YUmhMZLo8geXJa93daPEQ
@billyvg
billyvg requested a review from ryan953 September 9, 2026 18:55
billyvg added a commit that referenced this pull request Sep 9, 2026
Adds two Seer markdown embeds for Explore > Agents:

- **`conversation`** — one AI agent conversation. Inline renders a link;
block renders the transcript with its LLM call, token, cost and tool
totals.
- **`conversationsQuery`** — the conversations list filtered by a
search. Inline renders a link; block previews the first five matching
rows with duration, message count, errors and cost.

Both are gated on `organizations:gen-ai-conversations`, which already
exists in `temporary.py`, so neither reaches the agent until an org has
it.

The last of four independent branches adding single-resource Seer embeds
— see also #123901 (profile), #123905 (event) and #123906 (log). They
all touch `schemas.ts`, `index.ts`, the stories page and the generated
widget JSON, so whichever merges first leaves the rest needing a rebase
plus a `pnpm gen:embed-widgets` re-run.

### Notes for review

Both blocks avoid the route-level machinery, because it is URL-coupled
and an embed must not touch the host page's history
(`embeds/README.md`):

- The list block builds its own query rather than using
`useConversations`, which takes no arguments and reads cursor/filters
through nuqs.
- The transcript composes `MessagesPanel` + `ConversationAggregatesBar`
directly rather than `ConversationViewContent`, which writes its tab and
detail-tab state back to the URL. Message selection is local `useState`,
with a test asserting the router never changes.

Two shape details worth a look:

- **Content flattening.** `useConversations` normalizes
`firstInput`/`lastOutput` before handing rows out, but the raw endpoint
returns `string | {type, text}[] | null`. Since the block calls the
endpoint directly, it flattens them itself, then strips markdown to a
single line for the cell.
- **The agent filter lives in two places.** The list view keeps it in
its own `agent` URL param and folds it into the span query only when
calling the API. The link mirrors the former, the block the latter, so
both are filtered identically.

Ordering also differs: the endpoint returns by relevance, and the list
view sorts newest-first before rendering, so the preview re-sorts to
match.

### Testing

10 tests across both embeds — inline hrefs and their padded time
windows, the transcript, aggregates, API-title-wins-over-tag-title,
selection staying local, the error state, the row preview with ordering
and formatting, content-part flattening, and the empty state. The whole
`seer/markdown` suite passes; oxlint and oxfmt are clean, and `pnpm
gen:embed-widgets` produces no diff.

Closes https://linear.app/getsentry/issue/CW-2003/add-ai-conversations

🤖 Generated with [Claude Code](https://claude.ai/code)

https://claude.ai/code/session_016YUmhMZLo8geXJa93daPEQ
Comment on lines +297 to +301
<Container
data-test-id="seer-profile-flamechart"
height={PREVIEW_HEIGHT}
position="relative"
>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i think we should hit this with a border. i loaded up an example and it was scrolled down to the bottom by default. looked weird

Image

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fwiw the full page is also scrolled to the bottom, but it has a minimap view to see where you are. I'll play around and see what I can do

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hrm yeah. that sounds like a better setup, but for a bigger viewport...

i'm also getting used to this external mouse, so i do see scrollbars all the time now. none in this spot.

also i noticed that in the example i clicked time-ordered didn't load. i can't tell if that's just the specific profile though or something else.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

woo, anchorAtRoot does it for me

The 200px preview opened scrolled to the bottom of the stack, showing a slab
of leaf frames with no root context -- hard to even recognize as a flamechart,
and worse in Time-ordered, where it was reliably wrong.

Two causes:

`FlamegraphPreview`'s window props are in seconds -- it converts them with
`formatTo(v, 'second', flamegraph.unit)`. We passed `configSpace.width`, which
is already in the flamegraph's unit, inflating the window (1000ms became
1000s). No frame could then wrap it, so the "innermost parent frame" bias
never applied and the view fell through to the max-depth branch.

That branch is the second cause: `computePreviewConfigView` biases towards
the innermost frames by design, which is right for a preview scoped to a span
but backwards for a preview of a whole profile. Add an opt-in `anchorAtRoot`
that takes the existing y = 0 / anchorTop path unconditionally. The four
existing call sites are unchanged.

On a 40-deep profile whose samples share no root, the preview went from
y = 32 (bottom 8 rows) to y = 0, in both view modes.

Claude-Session: https://claude.ai/code/session_01PHE7yFBmGH1Rv1nmzGqwys
Clicking Time-ordered could raise "TypeError: Flamegraph does not support
call order sorting". The import type and the sort are a single choice --
`Flamegraph` rejects 'call order' on a profile imported as 'flamegraph' --
but the block derived them from `viewMode` in two separate memos, so nothing
enforced the pair.

Two changes:

Derive both from one `VIEW_MODES` table and build the group and the model in
the same memo, so the halves cannot come from different renders.

Do that work inside the existing try/catch. The model was previously
constructed in the component body while the ErrorBoundary sits below it in
the JSX, so a throw escaped the embed and took the surrounding conversation
down with it. A payload either step rejects now degrades to the metadata
strip, which is what the catch already promised for the import.

Claude-Session: https://claude.ai/code/session_01PHE7yFBmGH1Rv1nmzGqwys
@billyvg
billyvg merged commit a24507a into master Sep 9, 2026
75 checks passed
@billyvg
billyvg deleted the claude/seer-embed-profile branch September 9, 2026 19:42
billyvg added a commit that referenced this pull request Sep 9, 2026
Adds a `log` Seer markdown embed for one row in Explore > Logs. Inline
renders a link that opens the row in Explore; block renders its
severity, message and timestamp.

One of four independent branches adding single-resource Seer embeds —
see also #123901 (profile) and #123905 (event). They all touch
`schemas.ts`, `index.ts`, the stories page and the generated widget
JSON, so whichever merges first leaves the rest needing a rebase plus a
`pnpm gen:embed-widgets` re-run.

### The `view` field

| `view` | Renders |
| --- | --- |
| `summary` (default) | the row on its own |
| `attributes` | the full attribute tree |
| `attribute` + `attribute` key | that attribute's distribution across
nearby logs |

`attribute` without a key falls back to `summary` and issues no request.

### Notes for review

**Only the log id is required.** When `traceId`/`projectId` are missing
the block resolves them first via an events query. Log ids are UUIDv7,
so `logItemIdToTimestamp` decodes the creation time from the id itself
and the lookup scans a ±5min window rather than the org's whole
retention; it falls back to a wide `statsPeriod` only when neither Seer
nor the id yields a timestamp.

**The attribute breakdown is a plain aggregate logs query**, not the
trace-item stats endpoint — that endpoint's `SupportedItemType` is spans
and occurrences only, so it cannot serve logs today.

**`LogRowContent` is deliberately not reused.** It hard-requires three
contexts that throw without providers (`QueryParamsContext`,
`LogsAutoRefreshContext`, and the analytics page source), and it emits
`<tr>` markup that must live inside a table. Mounting
`LogsQueryParamsProvider` inside a Seer conversation is more machinery
than a single row warrants, so the block renders its own card from the
same severity/color helpers.

**The attribute tree is inert** (`config={{disableActions: true}}`) and
its renderers get an inert location/navigate pair, since the row actions
exist to filter the logs table the tree normally lives in — an embed has
no query params to write, and the README forbids touching the host
page's URL.

### Testing

6 tests covering the inline href and its time window, the block summary,
both attribute views, the no-key fallback, and the id-only path
resolving trace/project before fetching details. The whole
`seer/markdown` suite passes; oxlint and oxfmt are clean, and `pnpm
gen:embed-widgets` produces no diff.

Closes https://linear.app/getsentry/issue/CW-1946/add-log-embed

🤖 Generated with [Claude Code](https://claude.ai/code)

https://claude.ai/code/session_016YUmhMZLo8geXJa93daPEQ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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.

2 participants