Skip to content

fix: render plain string message content without extra quotes in Trace Details#11294

Merged
RogerHYang merged 1 commit intomainfrom
fix/trace-message-content-quotes
Feb 9, 2026
Merged

fix: render plain string message content without extra quotes in Trace Details#11294
RogerHYang merged 1 commit intomainfrom
fix/trace-message-content-quotes

Conversation

@RogerHYang
Copy link
Contributor

@RogerHYang RogerHYang commented Feb 9, 2026

Root cause: #10941 fixed the tool-return React error by running all message content in SpanDetails through formatContentAsString(). That function was moved from playgroundUtils (where it was only used for tool content) and was designed to JSON.stringify plain strings. Using it for every message in the trace UI made system/user/model text show with extra quotes and escaped newlines.

Fix: return plain string content as-is from formatContentAsString when it is not double-stringified JSON or a non-string JSON value. Tool results (objects/arrays) still get pretty-printed via JSON.stringify. Other call sites (playground, messageSchemas, ChatTemplateMessageCard) only pass tool content, so behavior there is unchanged.

Before

After


Note

Low Risk
UI-only formatting changes to how message content is stringified/rendered; low risk aside from potential edge-case display differences for string-like JSON inputs.

Overview
Trace span details now render plain text LLM message content without extra JSON quotes by calling formatContentAsString with unquotePlainString: true for message bodies and multi-modal text items.

formatContentAsString is extended with an optional unquotePlainString flag, updated to keep double-stringified JSON pretty-printing while making string vs non-string handling more explicit (including a defined fallback for undefined), and tests are updated to cover the undefined case.

Written by Cursor Bugbot for commit 0c0fd74. This will update automatically on new commits. Configure here.

@RogerHYang RogerHYang requested a review from a team as a code owner February 9, 2026 05:04
@github-project-automation github-project-automation bot moved this to 📘 Todo in phoenix Feb 9, 2026
@dosubot dosubot bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Feb 9, 2026
@claude
Copy link

claude bot commented Feb 9, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@RogerHYang RogerHYang force-pushed the fix/trace-message-content-quotes branch from 5cea572 to 48935f6 Compare February 9, 2026 07:28
@RogerHYang RogerHYang force-pushed the fix/trace-message-content-quotes branch from 48935f6 to 9aa3485 Compare February 9, 2026 07:48
@dosubot dosubot bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Feb 9, 2026
Copy link

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

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

…e Details

Root cause: #10941 fixed the tool-return React error by running all
message content in SpanDetails through formatContentAsString(). That
function was moved from playgroundUtils (where it was only used for
tool content) and was designed to JSON.stringify plain strings. Using
it for every message in the trace UI made system/user/model text show
with extra quotes and escaped newlines.

Fix: return plain string content as-is from formatContentAsString when
it is not double-stringified JSON or a non-string JSON value. Tool
results (objects/arrays) still get pretty-printed via JSON.stringify.
Other call sites (playground, messageSchemas, ChatTemplateMessageCard)
only pass tool content, so behavior there is unchanged.
@github-project-automation github-project-automation bot moved this from 📘 Todo to 👍 Approved in phoenix Feb 9, 2026
@RogerHYang RogerHYang merged commit 511775f into main Feb 9, 2026
23 checks passed
@RogerHYang RogerHYang deleted the fix/trace-message-content-quotes branch February 9, 2026 20:23
@github-project-automation github-project-automation bot moved this from 👍 Approved to ✅ Done in phoenix Feb 9, 2026
@RogerHYang
Copy link
Contributor Author

Full history of formatContentAsString

This document traces the evolution of the function that today lives as formatContentAsString in app/src/utils/jsonUtils.ts. It went through two renames and one move; the following is in chronological order.


1. Introduction: normalizeMessageAttributeValue

Commit: e97155905
Author: Tony Powell
Date: 2024-11-19
Subject: Improve code readability in PlaygroundChatTemplate

Location: app/src/pages/playground/playgroundUtils.ts

What changed: The function was introduced to centralize logic that had been inlined in PlaygroundChatTemplate.tsx:

  • Signature: normalizeMessageAttributeValue(content?: string | null | Record<string, unknown>): string
  • Behavior:
    • content == null or content === "" → return "{}"
    • typeof content === "string" → return content as-is
    • Otherwise (object) → return JSON.stringify(content, null, 2)

Use sites: Tool message content for the JSON editor (MessageEditor) and for the copy button (SortableMessageItem).

Purpose: Provide a single "normalized JSON string" for playground chat message attribute values so they can be passed into the JSON editor or copied consistently.


2. Rename + double-stringified handling: normalizeMessageContent

Commit: 3acd71299 (same change in 3cddab44, 7f4d6bdd)
Author: Anthony Powell
Date: 2025-01-16
PR: #6084 – feat(prompts): Save and display playground instances as multi-part content prompts

Location: app/src/pages/playground/playgroundUtils.ts

What changed:

  • Rename: normalizeMessageAttributeValuenormalizeMessageContent
  • Signature: content?: unknown (accept any type).
  • Behavior:
    • Still: content == null || content === "" → return "{}".
    • New string branch: If content is a string:
      • Double-stringified detection: content.startsWith('"{') or content.startsWith('"[') or content.startsWith('"\\"').
      • If double-stringified: parse with JSON.parse(content), then if result is a string parse again; finally JSON.stringify(secondParse, null, 2).
      • Otherwise: return string as-is; on parse error return content as-is.
    • Non-string: JSON.stringify(content, null, 2).

Use sites: In the same PR, tool message editor was changed to use message.content || "" (no normalization). normalizeMessageContent continued to be used for the copy-button text and for AI message display in SortableMessageItem.

Purpose: Support multi-part content prompts and fix "text normalization inconsistencies." The double-stringified handling unwraps content that was stored as a JSON string containing another JSON value (e.g. "{\"foo\":1}") so it can be displayed as pretty-printed JSON. The issue #6084 does not explicitly describe the double-stringified case; it was added as part of normalizing message content for the new prompt/playground behavior.


3. Remove empty/undefined → "{}"; use when loading from span attributes

Commit: 7ff1dbc84
Author: Anthony Powell
Date: 2025-03-27
PR: #6914 – fix: Remove unpredictable playground transformations

Location: app/src/pages/playground/playgroundUtils.ts

What changed:

  • Removed the early return: if (content === "" || typeof content === "undefined") return "{}". So empty string and undefined now fall through (empty string returns "" in the string branch; undefined would eventually hit JSON.stringify(undefined, null, 2), which is undefined in JS, so the return type could be undefined).
  • New use site: In transformSpanAttributesToPlaygroundInstance, when building messages from rawMessages, tool-role messages now get their content normalized: content: message.role === "tool" ? normalizeMessageContent(message.content) : message.content.

Purpose: Avoid mangling tool results and "unpredictable playground transformations" when loading a span into the playground; normalize tool message content only when mapping from span attributes.


4. Move to jsonUtils + rename: formatContentAsString; fix Trace UI React error

Commit: 3ce4ca80a
Author: Mikyo King
Date: 2026-01-21
PR: #10941 – fix: normalize tool return content before rendering
Issue: #10921 – [BUG] Tool return is not rendered in the phoenix UI

Location: Function moved from app/src/pages/playground/playgroundUtils.ts to app/src/utils/jsonUtils.ts and renamed to formatContentAsString.

What changed:

  • Move: Entire implementation moved into jsonUtils.ts (and removed from playgroundUtils.ts).
  • Rename: normalizeMessageContentformatContentAsString to reflect that it "takes content of any type" and "formats it as a string suitable for rendering."
  • New behavior (in jsonUtils version): For string content that is not double-stringified, added a nonStringStart check: if content matches /^\s*[0-9{[]|true|false|null/ (valid top-level JSON that is not a string), return it as-is; otherwise fall through to the final JSON.stringify(content, null, 2).
  • New use site: In SpanDetails.tsx, messageContent is passed through formatContentAsString(messageContent) before being rendered by ConnectedMarkdownBlock, so tool returns that are objects/arrays are converted to strings and no longer trigger React error #31.

Purpose: Fix the bug where tools returning dictionaries (or other non-string values) caused a React error in the Trace UI; reuse the existing normalization utility by moving it to a shared module and applying it in the trace page.


5. Plain-string display fix: unquotePlainString and behavior cleanup

Commit: 0c0fd74c2
Author: Roger Yang
Date: 2026-02-08
Subject: fix: render plain string message content without extra quotes in Trace Details

Location: app/src/utils/jsonUtils.ts

What changed:

  • New option: formatContentAsString(content, options) with options.unquotePlainString?: boolean. When true, plain string content (that is not double-stringified) is returned as-is (unquoted) for readable display.
  • Removed: The nonStringStart regex branch. Plain strings are no longer special-cased by that regex.
  • String branch (plain strings): If not double-stringified and not unquote: return JSON.stringify(content) (valid JSON-quoted string). If unquotePlainString: return content as-is.
  • Bottom of function: For non-string or non-serializable values, use JSON.stringify(content, null, 2) when possible; otherwise String(content) (e.g. undefined"undefined").
  • Use site: In SpanDetails.tsx, the main message display calls formatContentAsString(messageContent, { unquotePlainString: true }) so system/user/model text is shown without extra quotes and escaped newlines; tool results (objects/arrays) still get pretty-printed.

Purpose: After #10941, all message content in SpanDetails went through formatContentAsString, which was designed for tool content and would JSON.stringify plain strings. That made normal chat text show with extra quotes and escaped newlines. This fix keeps tool-result normalization but allows plain string content to be shown unquoted in the trace UI.


Usage throughout history (by file)

app/src/pages/playground/PlaygroundChatTemplate.tsx

From commit To commit Usage
e97155905 (2024-11-19) 7f4d6bdd (#6084) 1. MessageEditor: for message.role === "tool", toolMessageContent = normalizeMessageAttributeValue(message.content) passed as value={toolMessageContent} to JSONEditor (so the tool message content is normalized before showing in the editor). 2. SortableMessageItem: CopyToClipboardButton text={...} when not toolCalls used normalizeMessageAttributeValue(message.content) (so copied text is normalized).
7f4d6bdd (#6084, 2025-01-16) 7fb4ef6cd (#6454) 1. MessageEditor for tool role was changed to message.content || "" (no normalization). 2. Copy button still used normalizeMessageContent(message.content).
7fb4ef6cd (#6454, 2025-02-19) 7ff1dbc84 (#6914) 1. MessageEditor for tool role was changed back to toolMessageContent = normalizeMessageContent(message.content) for the JSONEditor value. 2. Copy still used normalizeMessageContent(message.content).
7ff1dbc84 (#6914, 2025-03-27) present No usage. Both uses removed: MessageEditor uses value={message.content ?? '""'}, Copy uses (message.content ?? ""). So this file no longer imports or calls the function.

app/src/pages/playground/playgroundUtils.ts

From commit To commit Usage
7ff1dbc84 (#6914, 2025-03-27) present Internal: In transformSpanAttributesToPlaygroundInstance, when building messages from getTemplateMessagesFromAttributes result: messages = rawMessages?.map((message) => ({ ...message, content: message.role === "tool" ? normalizeMessageContent(message.content) : message.content })). So when loading a span into the playground, tool-role message content is normalized. After #10941 the call is formatContentAsString(message.content) (import from jsonUtils).

app/src/schemas/messageSchemas.ts

From commit To commit Usage
4a74ff1b5 (#6132, 2025-01-21) present In promptMessageToOpenAI transform: when prompt.role === "TOOL", the OpenAI-form message's content is set to normalizeMessageContent(toolResult.toolResult.result) (so tool result is formatted to a string for the API). After #10941 the call is formatContentAsString(toolResult.toolResult.result) (import from jsonUtils).

app/src/pages/prompt/ChatTemplateMessageCard.tsx

From commit To commit Usage
7f4d6bdd (#6084, 2025-01-16) present In ChatTemplateMessageToolResultPart: value = useMemo(() => normalizeMessageContent(convertedToolResult), [toolResult]) (or formatContentAsString(convertedToolResult) after #10941). That value is used for the displayed tool result content in the prompt template message card.

app/src/pages/trace/SpanDetails.tsx

From commit To commit Usage
3ce4ca80a (#10941, 2026-01-21) 0c0fd74c2 1. In LLMMessage: normalizedContent = formatContentAsString(messageContent) (no options), passed as children to ConnectedMarkdownBlock. 2. In MessageContentListItem (multi-modal message_content): normalizedText = text ? formatContentAsString(text) : undefined, then passed to ConnectedMarkdownBlock. Both call sites added in #10941; no options.
0c0fd74c2 (2026-02-08) present Same two call sites, now both with { unquotePlainString: true }: 1. LLMMessage: formatContentAsString(messageContent, { unquotePlainString: true }). 2. MessageContentListItem: formatContentAsString(text, { unquotePlainString: true }).

Tests

From commit To commit Usage
(various) 3ce4ca80a (#10941) normalizeMessageContent was tested in app/src/pages/playground/__tests__/playgroundUtils.test.ts.
3ce4ca80a (#10941) present Tests moved to app/src/utils/__tests__/jsonUtils.test.ts as formatContentAsString tests (strings, numbers, booleans, null, arrays, objects, double-quoted strings, undefined, unquotePlainString).

Summary timeline

Date Commit Name (at time) Location Main change
2024-11-19 e97155905 normalizeMessageAttributeValue playgroundUtils Introduced; string as-is, object pretty-printed; null/empty → "{}".
2025-01-16 3acd71299 normalizeMessageContent playgroundUtils Rename; accept unknown; add double-stringified parse-twice handling. (#6084)
2025-03-27 7ff1dbc84 normalizeMessageContent playgroundUtils Remove empty/undefined→"{}"; use when mapping span→playground tool messages. (#6914)
2026-01-21 3ce4ca80a formatContentAsString jsonUtils Move + rename; add nonStringStart; use in SpanDetails for #10921. (#10941)
2026-02-08 0c0fd74c2 formatContentAsString jsonUtils Add unquotePlainString; remove nonStringStart; fix plain-string display in Trace.

Current call sites

  • app/src/pages/trace/SpanDetails.tsx – Normalize message content for LLM messages (with unquotePlainString: true for display); format tool/other text for display.
  • app/src/pages/playground/playgroundUtils.ts – When transforming span attributes to playground instance, normalize tool message content.
  • app/src/schemas/messageSchemas.ts – When converting prompt message to OpenAI format, format tool result content.
  • app/src/pages/prompt/ChatTemplateMessageCard.tsx – Format converted tool result for display.
  • Tests: app/src/utils/__tests__/jsonUtils.test.tsformatContentAsString tests (including double-quoted string and unquotePlainString).

Notes

RogerHYang added a commit to jash0803/phoenix that referenced this pull request Feb 9, 2026
…e Details (Arize-ai#11294)

Root cause: Arize-ai#10941 fixed the tool-return React error by running all
message content in SpanDetails through formatContentAsString(). That
function was moved from playgroundUtils (where it was only used for
tool content) and was designed to JSON.stringify plain strings. Using
it for every message in the trace UI made system/user/model text show
with extra quotes and escaped newlines.

Fix: return plain string content as-is from formatContentAsString when
it is not double-stringified JSON or a non-string JSON value. Tool
results (objects/arrays) still get pretty-printed via JSON.stringify.
Other call sites (playground, messageSchemas, ChatTemplateMessageCard)
only pass tool content, so behavior there is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

2 participants