Skip to content

Commit 8288ea6

Browse files
fix(investigations): Hide the raw JSON query answer from the transcript (#123162)
A query block's final answer is always a single raw JSON object (per `QUERY_INSTRUCTIONS` in `agent.py`) meant to be parsed into a chart/table — never prose meant to be read as-is. The Seer transcript rendered it as a plain assistant message anyway: a wall of partial JSON while it streamed in, then a redundant full JSON dump once the parsed result was already showing above it in `QueryResult`. - While the structured result is still streaming in, the transcript now shows `Building chart…` instead of the raw partial JSON. - Once it settles, the row is dropped entirely — `QueryResult` already renders the parsed chart or table for it. - A query block's inline clarification questions (plain markdown, never JSON) are unaffected — detection is keyed on the message starting with `{`. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent ddc3a9a commit 8288ea6

2 files changed

Lines changed: 169 additions & 10 deletions

File tree

static/app/views/investigations/detail/cell.tsx

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
} from 'sentry/views/investigations/api';
3838
import type {
3939
InvestigationBlock,
40+
InvestigationBlockKind,
4041
InvestigationDetail,
4142
InvestigationExecutionDetail,
4243
InvestigationExecutionStatus,
@@ -46,6 +47,7 @@ import type {
4647
import {visibleCallRecords} from 'sentry/views/seerExplorer/callRecords';
4748
import {AskUserQuestionBlock} from 'sentry/views/seerExplorer/components/askUserQuestionBlock';
4849
import {BlockComponent} from 'sentry/views/seerExplorer/components/chat';
50+
import {MessagePlaceholder} from 'sentry/views/seerExplorer/components/chat/shared';
4951
import {usePendingUserInput} from 'sentry/views/seerExplorer/hooks/usePendingUserInput';
5052
import type {Block} from 'sentry/views/seerExplorer/types';
5153

@@ -732,6 +734,7 @@ function RefinementPanel({
732734
<RefinementDisclosureContent>
733735
<Stack gap="md">
734736
<Transcript
737+
blockKind={block.kind}
735738
blocks={execution?.blocks ?? []}
736739
completedAt={currentExecution?.completedAt ?? null}
737740
active={active}
@@ -867,17 +870,22 @@ function PendingInvestigationQuestion({
867870

868871
function Transcript({
869872
active,
873+
blockKind,
870874
blocks,
871875
completedAt,
872876
}: {
873877
active: boolean;
878+
blockKind: InvestigationBlockKind;
874879
blocks: InvestigationTranscriptBlock[];
875880
completedAt: string | null;
876881
}) {
877882
const now = useNow(active);
878883
const visibleBlocks = useMemo(
879-
() => blocks.filter(isRenderableTranscriptBlock),
880-
[blocks]
884+
() =>
885+
blocks.filter((block, index) =>
886+
isRenderableTranscriptBlock(block, index, blockKind)
887+
),
888+
[blocks, blockKind]
881889
);
882890
const explorerBlocks = useMemo(
883891
() => visibleBlocks.map(adaptTranscriptBlock),
@@ -897,18 +905,33 @@ function Transcript({
897905
data-test-id="investigation-transcript"
898906
>
899907
{explorerBlocks.map((block, index) => {
908+
const currentVisibleBlock = visibleBlocks[index];
909+
if (!currentVisibleBlock) {
910+
return null;
911+
}
900912
const end =
901913
visibleBlocks[index + 1]?.timestamp ?? completedAt ?? (active ? now : null);
902914
const duration = getElapsedMilliseconds(block.timestamp, end);
915+
// Still streaming in: the raw JSON is unreadable as prose, so this row is rendered as a
916+
// plain loading placeholder instead of routing it through `BlockComponent`, which would
917+
// otherwise show it as assistant markdown. `isRenderableTranscriptBlock` drops the row
918+
// outright once the object is complete, since `QueryResult` above already renders it.
919+
const isBuildingChart =
920+
currentVisibleBlock.loading &&
921+
isStructuredQueryResultBlock(currentVisibleBlock, blockKind);
903922
return (
904923
<Grid key={block.id} columns="minmax(0, 1fr) auto" align="start" gap="md">
905-
<BlockComponent
906-
block={block}
907-
blockIndex={index}
908-
blocks={explorerBlocks}
909-
readOnly
910-
showThinking
911-
/>
924+
{isBuildingChart ? (
925+
<MessagePlaceholder content={t('Building chart…')} />
926+
) : (
927+
<BlockComponent
928+
block={block}
929+
blockIndex={index}
930+
blocks={explorerBlocks}
931+
readOnly
932+
showThinking
933+
/>
934+
)}
912935
{duration === null ? null : <ElapsedDuration milliseconds={duration} />}
913936
</Grid>
914937
);
@@ -926,10 +949,36 @@ function isInternalPromptBlock(block: InvestigationTranscriptBlock, index: numbe
926949
);
927950
}
928951

929-
function isRenderableTranscriptBlock(block: InvestigationTranscriptBlock, index: number) {
952+
/**
953+
* A query block's final answer is always a single raw JSON object (see `QUERY_INSTRUCTIONS` in
954+
* `agent.py`), never prose meant to be read as-is — `QueryResult` above already renders its
955+
* parsed chart or table. Detected by role and a `{` prefix so a legitimate inline clarification
956+
* question (plain markdown, never JSON) still renders normally.
957+
*/
958+
function isStructuredQueryResultBlock(
959+
block: InvestigationTranscriptBlock,
960+
blockKind: InvestigationBlockKind
961+
): boolean {
962+
return (
963+
blockKind === 'query' &&
964+
block.message.role === 'assistant' &&
965+
Boolean(block.message.content?.trim().startsWith('{'))
966+
);
967+
}
968+
969+
function isRenderableTranscriptBlock(
970+
block: InvestigationTranscriptBlock,
971+
index: number,
972+
blockKind: InvestigationBlockKind
973+
) {
930974
if (isInternalPromptBlock(block, index)) {
931975
return false;
932976
}
977+
// Once it has finished streaming, this is the redundant raw JSON dump described above — the
978+
// rendered chart/table already stands in for it, so the row is dropped rather than shown.
979+
if (!block.loading && isStructuredQueryResultBlock(block, blockKind)) {
980+
return false;
981+
}
933982
if (block.loading || block.message.content?.trim()) {
934983
return true;
935984
}

static/app/views/investigations/detail/index.spec.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,6 +1287,116 @@ describe('Investigation detail', () => {
12871287
expect(screen.getByLabelText('Instructions for Seer')).toHaveValue('');
12881288
});
12891289

1290+
it('shows "Building chart…" for a query block\'s in-progress structured result instead of raw JSON', async () => {
1291+
const investigation = InvestigationDetailFixture();
1292+
MockApiClient.addMockResponse({url: detailUrl, body: investigation});
1293+
const blockUrl = `${detailUrl}blocks/block-2/`;
1294+
MockApiClient.addMockResponse({
1295+
url: blockUrl,
1296+
method: 'PUT',
1297+
body: {...investigation.blocks[1]!, version: 2},
1298+
});
1299+
const runUrl = `${blockUrl}executions/`;
1300+
MockApiClient.addMockResponse({
1301+
url: runUrl,
1302+
method: 'POST',
1303+
body: {id: 'execution-building', status: 'running'},
1304+
});
1305+
MockApiClient.addMockResponse({
1306+
url: `${runUrl}execution-building/`,
1307+
body: {
1308+
id: 'execution-building',
1309+
status: 'running',
1310+
blocks: [
1311+
{
1312+
id: 'step-1',
1313+
timestamp: '2026-08-18T20:00:01Z',
1314+
loading: true,
1315+
message: {
1316+
role: 'assistant',
1317+
content: '{"tableMarkdown":"| Fact | Value |\\n|---|---|\\n| Monit',
1318+
},
1319+
artifacts: [],
1320+
toolLinks: null,
1321+
toolResults: null,
1322+
},
1323+
],
1324+
transcriptTruncated: false,
1325+
pendingUserInput: null,
1326+
partialMarkdown: null,
1327+
error: null,
1328+
},
1329+
});
1330+
1331+
renderView();
1332+
await chooseCellAction('Latency query', 'Refine');
1333+
await userEvent.type(
1334+
screen.getByLabelText('Instructions for Seer'),
1335+
'Find slow spans'
1336+
);
1337+
fireEvent.keyDown(screen.getByLabelText('Instructions for Seer'), {key: 'Enter'});
1338+
1339+
expect(await screen.findByText('Building chart…')).toBeInTheDocument();
1340+
expect(screen.queryByText(/tableMarkdown/)).not.toBeInTheDocument();
1341+
});
1342+
1343+
it("hides a query block's completed structured result from the transcript, not just while streaming", async () => {
1344+
const investigation = InvestigationDetailFixture();
1345+
MockApiClient.addMockResponse({url: detailUrl, body: investigation});
1346+
const blockUrl = `${detailUrl}blocks/block-2/`;
1347+
MockApiClient.addMockResponse({
1348+
url: blockUrl,
1349+
method: 'PUT',
1350+
body: {...investigation.blocks[1]!, version: 2},
1351+
});
1352+
const runUrl = `${blockUrl}executions/`;
1353+
MockApiClient.addMockResponse({
1354+
url: runUrl,
1355+
method: 'POST',
1356+
body: {id: 'execution-built', status: 'running'},
1357+
});
1358+
MockApiClient.addMockResponse({
1359+
url: `${runUrl}execution-built/`,
1360+
body: {
1361+
id: 'execution-built',
1362+
status: 'completed',
1363+
blocks: [
1364+
{
1365+
id: 'step-1',
1366+
timestamp: '2026-08-18T20:00:04Z',
1367+
loading: false,
1368+
message: {
1369+
role: 'assistant',
1370+
content:
1371+
'{"tableMarkdown":"| Fact | Value |","chart":null,"preferredView":"table","isEmpty":false,"chartUnavailableReason":null}',
1372+
},
1373+
artifacts: [],
1374+
toolLinks: null,
1375+
toolResults: null,
1376+
},
1377+
],
1378+
transcriptTruncated: false,
1379+
pendingUserInput: null,
1380+
partialMarkdown: null,
1381+
error: null,
1382+
},
1383+
});
1384+
1385+
renderView();
1386+
await chooseCellAction('Latency query', 'Refine');
1387+
await userEvent.type(
1388+
screen.getByLabelText('Instructions for Seer'),
1389+
'Find slow spans'
1390+
);
1391+
fireEvent.keyDown(screen.getByLabelText('Instructions for Seer'), {key: 'Enter'});
1392+
1393+
// The transcript renders once the block settles, but the raw JSON answer is dropped —
1394+
// `QueryResult` above already presents its parsed chart/table.
1395+
expect(await screen.findByText('No steps')).toBeInTheDocument();
1396+
expect(screen.queryByText(/tableMarkdown/)).not.toBeInTheDocument();
1397+
expect(screen.queryByText('Building chart…')).not.toBeInTheDocument();
1398+
});
1399+
12901400
it('stops an active refinement and leaves the rendered result visible', async () => {
12911401
const investigation = InvestigationDetailFixture();
12921402
investigation.blocks = [

0 commit comments

Comments
 (0)