feat(seer): Record when a markdown embed is rendered - #123912
Merged
Merged
Conversation
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
Contributor
📊 Type Coverage Diff
🔍 5 new type safety issues introducedType assertions (
This is informational only and does not block the PR. |
Member
Author
|
bugbot review |
Contributor
There was a problem hiding this comment.
✅ 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
Contributor
Story previewsPreview the stories changed in this PR on the Vercel deployment: Preview deployment: https://sentry-97nwpf7cd.sentry.dev |
ryan953
approved these changes
Sep 9, 2026
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.
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:indexidentity, socount_uniqueover the pre-composed keys gives both distinct embeds and the messages that contained them, grouped by type. Producing a per-embed index requiredMarkdownto 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
count_uniqueover an identifier, which the logs dataset supports and a pre-aggregated counter cannot.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