Skip to content

feat(seer): Record when a markdown embed is rendered - #123912

Merged
billyvg merged 2 commits into
masterfrom
worktree-seer-embed-render-telemetry
Sep 9, 2026
Merged

feat(seer): Record when a markdown embed is rendered#123912
billyvg merged 2 commits into
masterfrom
worktree-seer-embed-render-telemetry

Conversation

@billyvg

@billyvg billyvg commented Sep 9, 2026

Copy link
Copy Markdown
Member

Why?

Nothing tracked which Seer markdown embeds people actually see, so there was no way to tell which embed types are worth investing in. A naive count won't answer it: reopening a conversation re-renders every embed in it, and streaming re-renders each one on every chunk. What's needed is a count of distinct embeds, groupable by type.

How?

Each render emits a Sentry log carrying the embed's type and a conversation:message:index identity, so count_unique over the pre-composed keys gives both distinct embeds and the messages that contained them, grouped by type. Producing a per-embed index required Markdown to stamp tag tokens with their document-order position — the Tag component previously received nothing positional, so two inline embeds in one paragraph were indistinguishable.

Decisions

  • Logs, not metrics. The question needs count_unique over an identifier, which the logs dataset supports and a pre-aggregated counter cannot.
  • The index is assigned after lexing, not in the tokenizer. marked defers inline tokenization to a second pass, so tokenizer invocation order is not document order.
  • Only the settled render is tracked. While a block is loading its id is the optimistic client-side one, which the server replaces on the next poll — tracking both would count one embed twice. Nothing is lost, since the settled render follows immediately.
  • A surface opts in by supplying scope. Only the explorer does today; stories, demos and previews stay silent rather than emitting rows that cannot be deduplicated.

Closes https://linear.app/getsentry/issue/CW-2000/make-sure-we-have-analytics-for-each-embed-type

Generated with Claude Code

https://claude.ai/code/session_01TEetW4KdYbgin31P9WWG5s

Answers "which embed types do people actually see", which nothing tracked.
A render is emitted as a Sentry log, one per embed instance per page load.

Counting embeds needs an identity, and until now an embed had none. The Tag
component receives `{name, data, level, attrs, raw}` -- nothing positional --
so two embeds could only be told apart by their source text, which collapses
byte-identical duplicates and cannot be sent as-is (an embed body is a live
query, which may carry customer data).

So `Markdown` now stamps each tag token with its position among all tags in the
message, in document order, and passes it to the Tag component. It is assigned
after lexing rather than in the tokenizer because marked defers inline
tokenization to a second pass: a counter there would number an inline tag in the
first paragraph after a block tag in the second. Counting tags rather than
blocks is what distinguishes two inline embeds sharing a paragraph, which the
existing top-level token index cannot.

The index holds still while streaming. Content only ever grows by appending, so
a newly closed tag can only appear after the existing ones, and a tag whose
closing marker has not arrived is not a tag token at all -- it claims no index
early and displaces nothing when it completes.

That gives `conversation:message:index`, stable across viewers and reloads
because every part is server-assigned or derived from the settled message. It
is both the dedup key and the key the query counts distinct on, which makes
client-side dedup an optimisation rather than a correctness requirement: a
render missed by the Set -- a reopened conversation, a second tab -- collapses
again at query time. Dedup is needed at all because seer markdown re-lexes on
every streamed chunk and a paragraph holding an inline embed remounts each time
text lands after it.

Logs rather than metrics: the question is "how many distinct embeds", which
needs count_unique over an identifier, an aggregate the logs dataset offers and
a pre-aggregated counter cannot. Both composites are pre-composed as attributes
since the query layer cannot concatenate them -- count_unique on message_key
counts messages that showed a type, on embed_key counts embeds. The conversation
uses the gen_ai.conversation.id convention name; the message id stays bespoke,
since gen_ai.response.id means the provider's completion id, not a Seer block.

Two deliberate limits:

  - Only the settled render is tracked. While a block is loading its id is the
    optimistic client-side one, which the server replaces on the next poll, so
    tracking both would count one embed twice. Nothing is lost -- the settled
    render fires immediately after.
  - A surface supplies the scope or its embeds go untracked. Stories, demos and
    previews stay silent, and a surface that cannot name both ids records
    nothing rather than rows that cannot be deduplicated.

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

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📊 Type Coverage Diff

Metric Before After Delta
Coverage 95.47% 95.46% 🔴 -0.01%
Typed 138,897 138,920 🟢 +23
Untyped 6,598 6,602 🔴 +4
🔍 5 new type safety issues introduced

Type assertions (as) (5 new)

File Line Detail
static/app/components/core/markdown/markdown.tsx 101 as ExtendedToken[]token.tokens as ExtendedToken[]
static/app/components/core/markdown/markdown.tsx 104 as ExtendedToken[]token.items as ExtendedToken[]
static/app/components/core/markdown/markdown.tsx 109 as ExtendedToken[]cell.tokens as ExtendedToken[]
static/app/components/core/markdown/markdown.tsx 115 as ExtendedToken[]cell.tokens as ExtendedToken[]
static/app/components/core/markdown/markdown.tsx 130 as ExtendedToken[]MarkedLexer.lex(raw) as ExtendedToken[]

This is informational only and does not block the PR.

@linear-code

linear-code Bot commented Sep 9, 2026

Copy link
Copy Markdown

CW-2000

@billyvg

billyvg commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

bugbot review

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e0a0b4c. Configure here.

The convention name is what correlates a render with everything else
describing the same conversation -- spans, other producers -- but it leaves one
field of this log namespaced away from the rest, so a query for embeds has to
know that its conversation lives under a different prefix. Writing both keeps
`seer_embed.*` self-contained without giving up the correlation.

The message id gets no such pair. `gen_ai.response.id` means the provider's
completion id, not a Seer block id, so writing a block id there would put two
meanings behind one key.

Claude-Session: https://claude.ai/code/session_01TEetW4KdYbgin31P9WWG5s
@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-97nwpf7cd.sentry.dev

@billyvg
billyvg marked this pull request as ready for review September 9, 2026 16:50
@billyvg
billyvg requested review from a team as code owners September 9, 2026 16:50
@billyvg
billyvg requested a review from ryan953 September 9, 2026 16:58
@billyvg
billyvg merged commit 97e75e6 into master Sep 9, 2026
80 checks passed
@billyvg
billyvg deleted the worktree-seer-embed-render-telemetry branch September 9, 2026 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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