Skip to content

feat(connect-scope): add trace viewer extension with flamegraph UI#328

Open
tdstein wants to merge 41 commits intomainfrom
tdstein/trace-viewer
Open

feat(connect-scope): add trace viewer extension with flamegraph UI#328
tdstein wants to merge 41 commits intomainfrom
tdstein/trace-viewer

Conversation

@tdstein
Copy link
Contributor

@tdstein tdstein commented Feb 19, 2026

Summary

  • Adds the connect-scope extension: a Vue 3 + FastAPI app for viewing OpenTelemetry traces collected by Posit Connect
  • Provides a content browser with "My Content" / "All Content" tabs and search, filtered to content with trace collection enabled
  • Renders per-job flame graphs with a zoomable histogram, span detail panel, faceted filter sidebar, and duration range slider
  • Backend proxies Connect APIs (content, jobs, traces) using the Visitor API for per-user session auth with a TTL-cached client
  • Supports deep linking via ?content=<guid>&job=<key> URL query parameters

Test plan

  • Deploy to a Connect instance and verify the Visitor API auth flow (authorized vs. unauthorized states)
  • Confirm the content list loads, tabs filter correctly, and search works
  • Select a content item with trace collection enabled; verify jobs load in the right sidebar
  • Select a job and confirm the flame graph, histogram, and span detail panel render correctly
  • Test faceted filters (attribute facets, duration slider, search) and verify spans are highlighted/dimmed
  • Test shared zoom between the histogram brush and flame chart viewport
  • Verify deep link URLs persist and restore state on page reload
  • Confirm error states display correctly (no traces, failed fetch, unauthorized)

🤖 Generated with Claude Code

tdstein and others added 30 commits February 18, 2026 17:41
Add initial project structure for connect-scope including FastAPI
backend with posit-sdk, Vue 3 frontend with Pinia, Tailwind CSS 4,
TypeScript, and Vite 7.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements a 3-step drill-down: list trace-enabled content items,
select one to view its jobs, select a job to view raw traces JSON.

Adds GET /api/content, /api/content/{guid}/jobs, and
/api/content/{guid}/jobs/{job_key}/traces endpoints. Navigation is
driven by Pinia store selection state with a breadcrumb bar for
stepping back without re-fetching.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Decode response bytes as UTF-8 explicitly and parse each newline-
delimited record individually. Skips malformed or truncated trailing
lines that result from chunked-transfer errors in the Connect server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e viewer

- Group OTLP spans by traceId, sorted newest-first
- DFS tree traversal assigns depth for parent-child indentation
- Per-trace horizontal time axis with proportional bars
- Collapsible trace groups (collapsed by default)
- Breadcrumbs show actual content name instead of generic labels
- Page heading shows content name, job key, relative start time, status
- Error spans highlighted red (status.code === 2)
- Slow traces highlighted amber (>1.5× median trace duration)
- Slow child spans highlighted amber (>1.5× within-trace span median)
- Span count badge on each trace header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…form bars

Remove amber slow-span coloring and red error bars in favor of a
⚠ symbol next to errored span names. Bar width already communicates
duration, so color differentiation was redundant. Also removes the
isSlow field, median computation, and all conditional bar classes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a two-step dropdown (pick key → pick value) above the trace list
that builds AND-across-keys, OR-within-key filter chips. Active filters
show as removable chips; count line shows X / Y traces when filtering.
Facet options are sourced from unfiltered spans so they never disappear
while a filter is active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hydrates store state from ?content=<guid>&job=<jobKey> on mount so
any view is directly bookmarkable and linkable. Two watchers keep the
URL in sync as the user navigates, using history.replaceState so the
back button is unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… path routing

- Scope all API calls to the authenticated browser user via Visitor API
  using posit-sdk's with_user_session_token() with a 1-hour TTL cache
- Add /api/visitor-auth endpoint to detect missing OAuth integration
- Serve index.html dynamically, injecting window.__CONNECT_ROOT_PATH__
  from the ASGI root_path so the frontend can resolve API URLs correctly
  when deployed at a subpath on Connect
- Add src/api.ts utility that reads the injected root path and exports
  apiBase; update all stores to prefix fetch calls with it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…aluation

- Replace O(N²) per-span child scan with a pre-built childrenByParent map
- Parse all BigInt timestamps once per buildTraceGroup call instead of
  repeatedly in sort comparators and visit()
- Precompute activeFilterMap (Map<key, Set<values>>) once per filter change
  instead of allocating new Sets for every span in spanMatchesFilters
- Precompute pendingKeyActiveValues for O(1) lookups in value picker template
  instead of calling activeFilters.some() per rendered row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pping

Remove conflicting `relative` from filter picker panels so they float
over content instead of pushing it down. Add `whitespace-nowrap` to
duration cells on both trace header and span rows to prevent "123 ms"
from splitting across lines.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Waterfall/Flame toggle above the trace list. The flamegraph
renders each span as an absolutely-positioned rectangle using the
existing offsetPct, widthPct, and depth fields on FlatSpan — no
new data computation or third-party library required. Span labels
are suppressed when widthPct ≤ 2; a native title tooltip covers
narrow spans. Clicking a span opens the same detail panel as the
waterfall view via the shared expandedSpans set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an "Expand all" / "Collapse all" toggle next to the trace count.
The button operates on the currently filtered trace groups only, so
hidden traces are unaffected. Label updates dynamically based on
whether all visible traces are expanded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Apply Law of Common Region by wrapping expanded groups in a bordered
card so header and spans are perceived as a single unit. Use Law of
Proximity by increasing inter-group margin when expanded (mb-0.5 →
mb-2). Add a header separator line to chunk the summary row from the
span content within each card.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the trace-detail navigation level with a fixed slide-over
panel (62% width) that opens over the trace list when a row is
clicked. Backdrop click and Escape key both dismiss it, preserving
the list context. Rework TraceViewer into a sortable table (name,
time, duration bar, span count). Extract waterfall/flamegraph views
into a new TraceDetail component. Add selectedTraceId to the traces
store to drive panel state and deep-link (?trace=) support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…viewer

Reverts the architectural change from 25cddad that replaced inline
expandable trace groups with a sortable table + slide-over panel.
Restores the single-view inline layout while keeping all prior
enhancements (flamegraph toggle, expand/collapse all, filter fix,
bordered-card styling).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…dim non-matching spans

Migrate from the two-step dropdown filter picker to a persistent sidebar
with collapsible facet groups and checkboxes. Filters now highlight
matching spans (dimming non-matching ones at 30% opacity) instead of
removing them, preserving full trace context in both waterfall and
flamegraph views.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add four features to help surface performance bottlenecks: sort traces
by duration, compute and display span self-time, filter spans by minimum
duration in the sidebar, and an aggregate view with sortable columns
that groups spans by operation name showing p50/p95/max/avg-self stats.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Swap the pill buttons for a stepped range slider with notches at
0ms, 10ms, 50ms, 100ms, 500ms, 1s, and 10s. Move the Filters heading
below the duration slider.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…raceViewer

Move pure functions (formatting, OTLP parsing, trace-building) into
dedicated utility modules and shared types into types/index.ts. Replace
duplicated span detail templates in waterfall and flamegraph views with
a reusable SpanDetailPanel component. Reduces TraceViewer from 820 to
579 lines with no behavioral changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… align detail table

Remove selfTimeMs from the data model and all views. Color waterfall
timeline bars red for errored spans to match the flamegraph. Merge the
timing and attributes into a single table in SpanDetailPanel so the
duration value aligns with attribute values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…l view

Clicking a row in the aggregate table switches to the waterfall view and
filters to show only spans matching that operation name, dimming the rest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ub-components

Break the 590-line monolithic TraceViewer into 5 composables and 8+
extracted components. Add search filtering, data-driven duration stops,
URL state sync, summary stats, avg/% total columns, positioned tooltips,
CSS tree lines, SVG error icons, loading skeleton, empty states with
retry, close button on detail panels, keyboard navigation, and rename
flamegraph to timeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a profvis-style flame graph visualization accessible from the job
list. The flame graph renders all spans for a job on a single SVG
timeline with roots at the top expanding downward, drag-to-zoom,
sub-pixel span culling, and the same faceted filtering sidebar used
by the trace viewer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Selecting a job now goes directly to the flame graph. Clicking a span
in the global chart opens a trace-level flame chart, and clicking a
span there shows the detail panel. Removed the TraceViewer routing
and the per-job flame graph link from the job list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… sidebar

Integrate job selection directly into FlameGraphPage, removing the
separate jobs view step. Users can now switch between jobs via a
compact sidebar while viewing the flame graph.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move color legend out of FlameChart into FlameGraphPage so it appears
once. Improve error detail formatting in SpanDetailPanel with structured
layout. Sync selected span state between global and detail flame charts
via prop, and auto-open detail view on global span click.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use AbortController to abort in-flight requests when a new job is
selected, preventing race conditions where slow responses overwrite
newer results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lift viewport state into FlameGraphPage and pass it via v-model to both
TraceHistogram and the global FlameChart so drag-to-zoom in either
component keeps the other in sync. The detail FlameChart retains its own
independent zoom. Also fix deselecting a span in the detail view so it
no longer closes the entire detail panel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tdstein and others added 11 commits February 23, 2026 14:32
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BigInt division truncated very short spans to widthPct=0, making them
invisible in the global flame chart regardless of zoom level. Switch to
floating-point arithmetic for offset/width percentage calculations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete 14 files unreachable from the App.vue entry point, all part of
the old TraceViewer implementation replaced by the flame graph UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The base tsconfig enables noUncheckedIndexedAccess, which makes array
indexing return T | undefined. Add non-null assertions where the index
is guaranteed to be in bounds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Normalize alignment spacing, line wrapping, and arrow function
parentheses for consistency. Also fix percentile return type to
account for noUncheckedIndexedAccess.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add owner-aware content filtering so users see their own content by
default, with search and an "All Content" tab for browsing everything.

- Include owner info in /api/content response (include="owner")
- Add guid to User interface and owner fields to ContentItem type
- Add client-side search and owner filtering in content store
- Redesign ContentList.vue with search input, tabbed navigation with
  count badges, richer content cards, and contextual empty states

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Regenerate manifest with rsconnect to populate file checksums and
update pinned dependencies from uv export.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show a loading spinner instead of flashing the content list page while
resolving deep link parameters. Also explicitly trigger trace fetching
in FlameGraphPage when a job is already selected at mount time.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants