Skip to content

Commit 5fd2996

Browse files
committed
Add requests observability view
1 parent 1a3731f commit 5fd2996

14 files changed

Lines changed: 1365 additions & 6 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@prisma/studio-core": minor
3+
---
4+
5+
Add a Requests observability view with dummy request rows, trace spans, and associated logs.

Architecture/navigation-url-state.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Navigation state MUST be URL-driven and managed through `useNavigation` + Nuqs.
88

99
This architecture governs:
1010

11-
- active Studio view (`table`, `schema`, `console`, `sql`, `stream`)
11+
- active Studio view (`table`, `schema`, `console`, `requests`, `sql`, `stream`)
1212
- active schema/table/stream
1313
- active stream follow mode
1414
- active stream aggregation-panel visibility
@@ -86,7 +86,7 @@ Adding a new URL key requires updating `StateKey` in `nuqs.ts` first.
8686
When Studio is running without a database connection but with Streams enabled:
8787

8888
- the resolved default `view` MUST become `"stream"` instead of `"table"`
89-
- stale database-oriented views such as `table`, `schema`, `console`, and `sql` MUST resolve back to the stream view instead of trying to render database-only UI against a disabled database session
89+
- stale database-oriented views such as `table`, `schema`, `console`, `requests`, and `sql` MUST resolve back to the stream view instead of trying to render database-only UI against a disabled database session
9090

9191
When URL params are stale from a previous DB, invalid `schema`/`table` values MUST be resolved to valid current defaults.
9292
Shared table page size and infinite-scroll mode are not derived from URL defaults; they are restored through Studio UI state and then mirrored into query behavior by `usePagination`.

Architecture/non-standard-ui.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,18 @@ It deliberately excludes:
145145
- The storage breakdowns also need collapsible ledger-style accounting boxes whose headers surface the section totals when folded shut, plus faint shared-cap annotations that sit beside right-aligned byte values and one shared cap marker spanning both Routing and Exact cache rows, which is not a stock ShadCN pattern.
146146
- No stock ShadCN pattern covers that descriptor-driven observability layout, especially when the UI must distinguish logical bytes from physical storage signals, separate search coverage from historical run indexes, hide unconfigured routing rows, and keep the remaining cost caveats explicit instead of inventing unavailable totals.
147147

148+
### Requests Trace Timeline
149+
150+
- Canonical component:
151+
- [`ui/studio/views/requests/RequestsView.tsx`](ui/studio/views/requests/RequestsView.tsx)
152+
- Closest standard ShadCN alternatives:
153+
- `Table`
154+
- `Badge`
155+
- `ToggleGroup`
156+
- Why it stays non-standard:
157+
- The Requests view needs a dense request-log row that expands in place into a span waterfall. Standard ShadCN components cover the table, badges, and view toggle, but there is no stock ShadCN timeline primitive that can display OpenTelemetry-style nested spans with proportional start offsets and durations.
158+
- The custom portion is limited to the timeline grid and span bars; the surrounding list and controls remain built from ShadCN primitives.
159+
148160
## Standardization Candidates
149161

150162
These are the current high-signal places where Studio is bypassing a plausible standard ShadCN component or composition pattern.

Architecture/requests-view.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Requests View Architecture
2+
3+
This document is normative for the Studio Requests view.
4+
5+
The Requests view is a database-session view that presents request-level observability data in the existing Studio shell. The first implementation uses deterministic dummy data, but its UI contract is shaped so real request, span, and log data can replace that source later without changing navigation.
6+
7+
## Scope
8+
9+
This architecture governs:
10+
11+
- routing into `view=requests`
12+
- sidebar and command-palette entry points
13+
- request list ordering and summary columns
14+
- in-place request expansion
15+
- trace and log detail toggling
16+
- dummy request/span/log data shape
17+
18+
## Canonical Components
19+
20+
- [`ui/studio/Navigation.tsx`](../ui/studio/Navigation.tsx)
21+
- [`ui/studio/CommandPalette.tsx`](../ui/studio/CommandPalette.tsx)
22+
- [`ui/studio/Studio.tsx`](../ui/studio/Studio.tsx)
23+
- [`ui/studio/views/requests/RequestsView.tsx`](../ui/studio/views/requests/RequestsView.tsx)
24+
- [`ui/hooks/use-navigation.tsx`](../ui/hooks/use-navigation.tsx)
25+
- [`ui/hooks/use-ui-state.ts`](../ui/hooks/use-ui-state.ts)
26+
27+
## Non-Negotiable Rules
28+
29+
- Requests routing MUST use the existing URL-backed `view` param with the value `requests`.
30+
- The left navigation MUST render Requests in the Studio block immediately after Console and before SQL while database-backed Studio views are available.
31+
- The request list MUST render most recent requests first.
32+
- Summary rows MUST include timestamp, service, path, message, and duration.
33+
- Request expansion MUST happen in place under the clicked row instead of navigating away or opening a modal.
34+
- Expanded request state and the selected detail view MUST use `useUiState` with deterministic request-scoped keys.
35+
- Trace details MUST show proportional span durations for external requests and Prisma OpenTelemetry-style subsystem spans when dummy data includes them.
36+
- Log details MUST handle both string messages and structured object payloads without dropping the original structured object.
37+
38+
## Dummy Data Contract
39+
40+
Until Studio is wired to a real request source, dummy rows live in the Requests view module. Each request row MUST include:
41+
42+
- a stable request id
43+
- timestamp
44+
- service
45+
- method and path
46+
- status
47+
- message
48+
- duration in milliseconds
49+
- trace id
50+
- zero or more trace spans
51+
- zero or more associated log lines
52+
53+
Each span MUST include:
54+
55+
- stable span id
56+
- display name
57+
- service
58+
- kind (`request`, `framework`, `external`, or `prisma`)
59+
- start offset in milliseconds
60+
- duration in milliseconds
61+
- nesting depth
62+
- status
63+
64+
Each log line MUST include:
65+
66+
- stable log id
67+
- timestamp
68+
- level
69+
- service
70+
- message as either a string or structured object
71+
72+
## UI State Contract
73+
74+
The expanded request id MUST be stored through `useUiState` using:
75+
76+
- `requests:expanded-request`
77+
78+
The active detail view for each expanded request MUST be stored through `useUiState` using:
79+
80+
- `requests:${requestId}:detail-view`
81+
82+
These keys keep request-detail interaction consistent with the existing Studio UI state architecture and avoid introducing component-local shared view state.
83+
84+
## Forbidden Patterns
85+
86+
- Do not introduce a second routing system for requests.
87+
- Do not store expanded request state in module globals.
88+
- Do not replace structured log objects with stringified summaries as the source data.
89+
- Do not open trace or log details in a modal or side panel for the default row click behavior.
90+
91+
## Testing Requirements
92+
93+
Requests view changes MUST include tests for:
94+
95+
- `view=requests` routing in `Studio`
96+
- sidebar placement directly under Console
97+
- newest-first dummy request list rendering with the required summary columns
98+
- in-place row expansion
99+
- trace detail rendering for external and Prisma spans
100+
- log detail rendering for both string and structured object payloads

Architecture/ui-state.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ The following are valid examples of UI state and where they belong:
136136
- Scoped by active schema plus the current visualized table set so returning to the same schema graph restores dragged positions without leaking across schemas.
137137
- Includes the stored ELK baseline positions and reset-layout request token used by the header action.
138138
- Command-palette `x more...` handoff into table browsing: the same navigation table-name search `useUiState` entry, not a second command-palette-specific table-filter store
139+
- Requests view expanded request id and selected request detail view: `uiLocalStateCollection` via `useUiState`
139140

140141
If new UI state is shared across components, it MUST be assigned to one of these stores (or a new TanStack DB collection added in Studio context).
141142
Container-level fullscreen controls are now host-owned rather than Studio-owned, so they MUST NOT be reintroduced as implicit package-level shared UI state unless the architecture is updated first.

FEATURES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ In table view it surfaces context-aware actions like row search, AI filtering, i
155155
Table navigation stays intentionally short by showing only the first 3 tables by default and the top 3 matches while filtering. If more tables exist, the palette shows an `x more...` entry that hands off into the existing sidebar table search so users keep one consistent table-filtering UI.
156156
`Search rows` and `Filter with AI` work in two modes: typing the command name keeps them as focus actions for the existing toolbar inputs, while free text turns them into direct `Search rows: ...` and `Filter with AI: ...` actions that execute immediately. Keyboard selection stays active from the moment the palette opens, so arrow keys can move through results before any typing, and the list auto-scrolls the active result into view as you move into lower sections.
157157

158+
## Request Observability
159+
160+
The Requests view gives Studio a request-centered observability surface for inspecting dummy request data in the same shell as tables, SQL, and Console.
161+
Each request row shows timestamp, service, path, message, and duration with newest requests first, then expands in place to show an OpenTelemetry-style trace timeline or the log lines associated with that request.
162+
The trace view highlights external calls and Prisma subsystem spans, while the logs view preserves both plain string messages and structured JSON objects for detailed inspection.
163+
158164
## Column Controls and Metadata
159165

160166
Columns support drag-and-drop reordering, resizing, sorting, and pinning to keep important fields anchored during wide-table review.

ui/studio/CommandPalette.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ interface NavigationMockValue {
2020
schemaParam: string;
2121
setSchemaParam: () => Promise<URLSearchParams>;
2222
setTableParam: () => Promise<URLSearchParams>;
23-
viewParam: "table" | "schema" | "console" | "sql";
23+
viewParam: "table" | "schema" | "console" | "requests" | "sql";
2424
}
2525

2626
interface IntrospectionMockValue {
@@ -150,8 +150,11 @@ vi.mock("../hooks/use-ui-state", async () => {
150150

151151
vi.mock("./context", () => ({
152152
useStudio: () => ({
153+
hasDatabase: true,
153154
isDarkMode,
154155
isNavigationOpen,
156+
navigationWidth: 192,
157+
setNavigationWidth: vi.fn(),
155158
setThemeMode: setThemeModeMock,
156159
themeMode,
157160
toggleNavigation: toggleNavigationMock,
@@ -352,6 +355,7 @@ describe("Studio command palette", () => {
352355
expect(document.body.textContent).not.toContain("incidents");
353356
expect(document.body.textContent).toContain("Visualizer");
354357
expect(document.body.textContent).toContain("Console");
358+
expect(document.body.textContent).toContain("Requests");
355359
expect(document.body.textContent).toContain("SQL");
356360
expect(document.body.textContent).toContain("Studio theme");
357361
expect(document.body.textContent).toContain("Light");

ui/studio/CommandPalette.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
FileCode2,
66
GalleryVerticalEnd,
77
Laptop,
8+
ListTree,
89
Moon,
910
Search,
1011
Sun,
@@ -164,7 +165,9 @@ function AppearanceCommandItem(props: {
164165
value={value}
165166
className={cn(
166167
"justify-between gap-3",
167-
disabled ? "text-muted-foreground/55" : "text-foreground hover:bg-secondary/85",
168+
disabled
169+
? "text-muted-foreground/55"
170+
: "text-foreground hover:bg-secondary/85",
168171
)}
169172
>
170173
<span className="flex min-w-0 items-center gap-2.5">
@@ -508,6 +511,17 @@ function StudioCommandPalette() {
508511
},
509512
section: "views",
510513
},
514+
{
515+
disabled: false,
516+
icon: ListTree,
517+
id: "view:requests",
518+
keywords: ["requests", "logs", "traces", "observability"],
519+
label: "Requests",
520+
onSelect: () => {
521+
window.location.hash = createUrl({ viewParam: "requests" });
522+
},
523+
section: "views",
524+
},
511525
{
512526
disabled: false,
513527
icon: FileCode2,

ui/studio/Navigation.test.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ interface NavigationMockValue {
1717
setSchemaParam: () => Promise<URLSearchParams>;
1818
setTableParam: () => Promise<URLSearchParams>;
1919
streamParam: string | null;
20-
viewParam: "table" | "schema" | "console" | "sql" | "stream";
20+
viewParam: "table" | "schema" | "console" | "requests" | "sql" | "stream";
2121
}
2222

2323
interface IntrospectionMockValue {
@@ -476,6 +476,37 @@ describe("Navigation", () => {
476476
container.remove();
477477
});
478478

479+
it("renders Requests directly under Console in the Studio menu", () => {
480+
const container = document.createElement("div");
481+
document.body.appendChild(container);
482+
const root = createRoot(container);
483+
484+
act(() => {
485+
root.render(<Navigation />);
486+
});
487+
488+
const studioLinks = [
489+
...container.querySelectorAll<HTMLAnchorElement>(
490+
'nav[aria-label="Studio"] a',
491+
),
492+
].map((link) => ({
493+
href: link.getAttribute("href"),
494+
text: link.textContent?.trim(),
495+
}));
496+
497+
expect(studioLinks).toEqual([
498+
{ href: "#viewParam=schema", text: "Visualizer" },
499+
{ href: "#viewParam=console", text: "Console" },
500+
{ href: "#viewParam=requests", text: "Requests" },
501+
{ href: "#viewParam=sql", text: "SQL" },
502+
]);
503+
504+
act(() => {
505+
root.unmount();
506+
});
507+
container.remove();
508+
});
509+
479510
it("closes table search on blur when the search input is empty", () => {
480511
const container = document.createElement("div");
481512
document.body.appendChild(container);

ui/studio/Navigation.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,15 @@ export function Navigation({ className }: NavigationProps) {
526526
Console
527527
</a>
528528
</Navigation.Item>
529+
<Navigation.Item
530+
asChild
531+
isActive={viewParam === "requests"}
532+
className={navigationItemClasses}
533+
>
534+
<a href={createUrl({ viewParam: "requests" })} className="w-full">
535+
Requests
536+
</a>
537+
</Navigation.Item>
529538
<Navigation.Item
530539
asChild
531540
isActive={viewParam === "sql"}

0 commit comments

Comments
 (0)