Skip to main content
Custom trace views transform complex traces into interfaces anyone on your team can use. Describe what you want in natural language and Loop generates an interactive React component you can customize or embed anywhere.

Common use cases

Build custom annotation interfaces for large-scale human review tasks, surfacing only relevant information for annotators and subject matter experts.
Replace JSON with intuitive UI components like carousels, playlists, or structured summaries to make traces accessible to PMs, legal reviewers, and domain experts.
Create views that mirror your product experience:
  • Playlist-style views for music applications
  • Interactive source-and-answer layouts
  • Custom dashboards for internal evaluations
Aggregate and display data across conversation turns to analyze dialogue flow and long-running interactions.

Create trace views

To create a custom trace view using Loop:
  1. Select a trace in your logs, experiments, or during human review.
  2. Select Views.
  3. Describe how you want to view your trace data.
After Loop generates your view, refine the view by describing additional changes or edit the React component code directly. Example prompts:
  • “Create a view that renders a list of all tools available in this trace and their outputs”
  • “Build an interface to review each trace one by one with easy switching between traces”
  • “Create a conversation-style view that highlights user messages and assistant responses”
  • “Render the video url from the trace’s metadata field and show simple thumbs up/down buttons”
Self-hosted deployments: If you restrict outbound access, allowlist https://www.braintrustsandbox.dev to enable custom views. This domain hosts the sandboxed iframe that securely renders custom view code.

Share trace views

By default, a custom trace view is only visible and editable by the user who created it. To share your view with all users in the project:
  1. Select Save in the view editor.
  2. Choose Save as new view version.
  3. Select Update to make it available project-wide.
All team members can then use the shared view when reviewing traces. Custom views integrate with Braintrust workflows — use them during human review, write annotations that flow into datasets, and combine with Loop for analysis.

Edit trace view React code

Custom trace views are React components that run inside Braintrust. You can edit the component code directly to customize behavior beyond what Loop generates. To edit the React code:
  1. Go to the custom trace view.
  2. Select in the lower left of the view.
  3. Select Edit.
Your React component receives the following props:
PropTypeDescription
traceobjectContains all spans and methods for the trace. Attachment URLs in span data are automatically signed for rendering.
spanobjectThe currently selected span with full data
updatefunctionUpdate span metadata: update('field', value)
selectSpanfunctionNavigate to a different span: selectSpan(spanId)
The trace object includes:
  • rootSpanId, selectedSpanId - Current span context
  • spanOrder - All span IDs in execution order
  • spans - Map of span_id → span (IDs/relationships only)
  • fetchSpanFields - Fetch full data for multiple spans (see Access data from multiple spans)
The component can be copied and embedded in your own applications, enabling you to:
  • Reuse custom views outside of Braintrust
  • Integrate review interfaces into internal tools
  • Build standalone annotation applications
  • Create consistent review experiences across different contexts

Add interactive controls

Custom views support interactive elements that write data back to traces. Add buttons, inputs, or custom controls to collect:
  • Human review scores
  • Thumbs up/down feedback
  • Custom metadata fields
  • Annotation notes
Use the update function to write metadata back to the trace. This enables annotation workflows where review and data collection happen in the same interface.
Example: Add thumbs up/down buttons
function FeedbackView({ trace, span, update }) {
  const handleFeedback = (isPositive) => {
    update('metadata', {
      ...span.metadata,
      user_feedback: isPositive ? 'positive' : 'negative',
      reviewed_at: new Date().toISOString(),
    });
  };

  return (
    <div className="p-4">
      <h3 className="text-lg font-semibold mb-4">Review this output</h3>
      <pre className="bg-slate-100 p-2 rounded mb-4">
        {JSON.stringify(span.output, null, 2)}
      </pre>
      <div className="flex gap-2">
        <button onClick={() => handleFeedback(true)}>👍 Good</button>
        <button onClick={() => handleFeedback(false)}>👎 Bad</button>
      </div>
    </div>
  );
}

module.exports = FeedbackView;

Access data from multiple spans

By default, only the selected span has full data (input, output, expected, metadata). To access data from other spans, use fetchSpanFields:
// Fetch all fields for one span
const data = await trace.fetchSpanFields(spanId);

// Fetch specific fields for multiple spans
const data = await trace.fetchSpanFields(trace.spanOrder, ['input', 'output']);
Example: Display all span inputs
function AllInputsView({ trace, span }) {
  const [spanData, setSpanData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    if (!trace?.fetchSpanFields) return;

    trace.fetchSpanFields(trace.spanOrder, ['input'])
      .then(setSpanData)
      .catch((err) => {
        console.error('Failed to fetch span data:', err.message);
        setError(err.message);
      })
      .finally(() => setLoading(false));
  }, [trace]);

  if (loading) return <div className="p-4">Loading...</div>;
  if (error) return <div className="p-4 text-red-600">Error: {error}</div>;

  return (
    <div className="p-4 space-y-2">
      {trace.spanOrder.map((id) => (
        <pre key={id} className="text-xs bg-slate-100 p-2 rounded text-wrap">
          {JSON.stringify(spanData?.[id]?.input, null, 2)}
        </pre>
      ))}
    </div>
  );
}

module.exports = AllInputsView;

Render attachments

Attachments (images, videos, audio, and other binary data) logged in your traces can be displayed directly in custom views. When span data is fetched, Braintrust automatically converts attachment references to inline_attachment objects with pre-signed URLs ready for rendering. Attachments in span data are automatically transformed into objects with this structure:
{
  type: "inline_attachment",
  src: "https://signed-url...",      // Pre-signed URL ready to use
  content_type: "image/jpeg",        // MIME type
  filename: "example.jpg"            // Optional filename
}
The type field identifies the object as an attachment, src contains a pre-signed URL that works directly in image, video, or audio tags, and content_type indicates the media type. For example, the following code creates an input/output verification view that automatically detects and renders attachments alongside regular data:
Example: Input/output verification
function InputOutputVerification({ trace, span }) {
  // Helper to check if a value is an attachment
  const isAttachment = (value) => {
    return value && typeof value === 'object' && value.type === 'inline_attachment' && value.src;
  };

  // Helper to render a value, handling attachments and regular data
  const renderValue = (value, label) => {
    if (!value && value !== 0 && value !== false) {
      return (
        <div className="text-slate-400 italic">No {label.toLowerCase()}</div>
      );
    }

    // Check if it's an attachment
    if (isAttachment(value)) {
      const { src, filename, content_type } = value;
      if (content_type?.startsWith('image/')) {
        return (
          <div className="space-y-2">
            <img src={src} alt={filename || 'attachment'} className="max-w-full rounded-lg border border-slate-200 shadow-sm" />
            {filename && <p className="text-xs text-slate-500">{filename}</p>}
          </div>
        );
      }
      if (content_type?.startsWith('video/')) {
        return <video src={src} controls className="max-w-full rounded-lg" />;
      }
      if (content_type?.startsWith('audio/')) {
        return <audio src={src} controls className="w-full" />;
      }
      return <a href={src} download={filename} className="text-blue-600 underline">{filename || 'Download attachment'}</a>;
    }

    // Handle objects that might contain attachments
    if (typeof value === 'object') {
      const entries = Object.entries(value);
      return (
        <div className="space-y-3">
          {entries.map(([key, val]) => (
            <div key={key}>
              <div className="text-xs font-semibold text-slate-600 uppercase mb-1">{key}</div>
              <div className="pl-2 border-l-2 border-slate-200">
                {renderValue(val, key)}
              </div>
            </div>
          ))}
        </div>
      );
    }

    // Handle strings and primitives
    if (typeof value === 'string') {
      return <div className="whitespace-pre-wrap text-slate-800">{value}</div>;
    }

    return <pre className="text-sm bg-slate-50 p-3 rounded border border-slate-200 overflow-auto">{JSON.stringify(value, null, 2)}</pre>;
  };

  const input = span?.data?.input;
  const output = span?.data?.output;
  const expected = span?.data?.expected;

  return (
    <div className="w-[100vw] min-h-screen bg-slate-50 p-6">
      <div className="max-w-7xl mx-auto">
        <h1 className="text-2xl font-bold text-slate-900 mb-6">Input & Output Verification</h1>

        <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
          {/* Input Section */}
          <div className="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
            <div className="flex items-center gap-2 mb-4">
              <div className="w-2 h-2 bg-blue-500 rounded-full"></div>
              <h2 className="text-lg font-semibold text-slate-900">Input</h2>
            </div>
            <div className="space-y-4">
              {renderValue(input, 'Input')}
            </div>
          </div>

          {/* Output Section */}
          <div className="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
            <div className="flex items-center gap-2 mb-4">
              <div className="w-2 h-2 bg-green-500 rounded-full"></div>
              <h2 className="text-lg font-semibold text-slate-900">Output</h2>
            </div>
            <div className="space-y-4">
              {renderValue(output, 'Output')}
            </div>
          </div>
        </div>

        {/* Expected Section (if exists) */}
        {expected && (
          <div className="mt-6 bg-white rounded-lg shadow-sm border border-slate-200 p-6">
            <div className="flex items-center gap-2 mb-4">
              <div className="w-2 h-2 bg-amber-500 rounded-full"></div>
              <h2 className="text-lg font-semibold text-slate-900">Expected</h2>
            </div>
            <div className="space-y-4">
              {renderValue(expected, 'Expected')}
            </div>
          </div>
        )}

        {/* Metadata Section */}
        {span?.data?.metadata && Object.keys(span.data.metadata).length > 0 && (
          <div className="mt-6 bg-white rounded-lg shadow-sm border border-slate-200 p-6">
            <div className="flex items-center gap-2 mb-4">
              <div className="w-2 h-2 bg-purple-500 rounded-full"></div>
              <h2 className="text-lg font-semibold text-slate-900">Metadata</h2>
            </div>
            <div className="space-y-4">
              {renderValue(span.data.metadata, 'Metadata')}
            </div>
          </div>
        )}

        {/* Scores Section */}
        {span?.data?.scores && Object.keys(span.data.scores).length > 0 && (
          <div className="mt-6 bg-white rounded-lg shadow-sm border border-slate-200 p-6">
            <div className="flex items-center gap-2 mb-4">
              <div className="w-2 h-2 bg-rose-500 rounded-full"></div>
              <h2 className="text-lg font-semibold text-slate-900">Scores</h2>
            </div>
            <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
              {Object.entries(span.data.scores).map(([name, score]) => (
                <div key={name} className="bg-slate-50 rounded p-3 border border-slate-200">
                  <div className="text-xs text-slate-600 mb-1">{name}</div>
                  <div className="text-2xl font-bold text-slate-900">
                    {typeof score === 'number' ? score.toFixed(2) : score}
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

module.exports = InputOutputVerification;
Input/output verification example

Next steps