Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-evlog-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@prisma/studio-core": patch
---

Improve observability stream previews with concise evlog request summaries and otel span summaries.
2 changes: 2 additions & 0 deletions Architecture/stream-event-view.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ Summary derivation rules:
- key MAY be derived from explicit key/routing-key fields in the decoded payload
- indexed fields MAY be derived only from explicit indexed-field payload shapes; do not invent synthetic indexed metadata
- preview SHOULD prefer the payload's primary content object when one exists (for example a top-level `value` field), otherwise fall back to the full decoded event
- when the active stream profile is `evlog`, preview SHOULD render a compact request summary from request-like fields before falling back to JSON, using `METHOD path` for successful info-level web requests and adding non-success status plus warning/error message context when present
- when the active stream profile is `otel-traces`, preview SHOULD render a compact span summary from OTEL span fields before falling back to JSON, preferring HTTP semantic attributes for request spans and including service, duration, and error status when available
- expanded content SHOULD pretty-print structured JSON payloads
- when stream search is active and a row is expanded, the pretty-printed expanded content SHOULD highlight matching fields and values using the same yellow search treatment used by table search
- unfielded search clauses SHOULD highlight only the matched value text for the configured default fields, not the names of every default field that participated in matching
Expand Down
2 changes: 2 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ When that deep link lands on a `state-protocol` WAL stream, the stream view also

Selecting a stream opens a dedicated event log view in the main pane instead of the table grid.
The view uses TanStack DB-backed infinite scroll to load the newest events first, shows summary columns for time, key, indexed fields, preview text, and payload size, and lets users expand one event at a time to inspect the full formatted content.
For `evlog` streams, the preview column summarizes request-shaped events as readable request lines such as `GET /product/acme-mug`, adding status and diagnostic text for failures or warning/error events while keeping the full JSON available in the expanded row.
For `otel-traces` streams, preview text summarizes spans from semantic OTEL fields, including request path, service, duration, and error status when available, so trace rows can be scanned without opening raw span JSON.
When a stream advertises a search schema with a primary timestamp field, the event log uses that configured timestamp for the row time column before falling back to legacy timestamp field names. This keeps schema-driven streams like GH Archive from showing `Unknown time` even when their canonical timestamp lives under a non-legacy field such as `eventTime`.
The stream chrome now mirrors the table view more closely: the header is reserved for controls, while a fixed footer summary box shows the latest event count and total logical payload bytes in human-readable units.
That footer count uses grouped digits like `12,345 events`, while the byte total stays compact by scaling units such as `MB` and `GB` instead of showing a raw comma-separated byte count.
Expand Down
123 changes: 123 additions & 0 deletions ui/hooks/use-stream-events.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
createStreamReadUrl,
encodeStreamOffset,
getStreamEventsWindow,
normalizeStreamEvents,
useStreamEvents,
} from "./use-stream-events";
import type { StudioStream } from "./use-streams";
Expand Down Expand Up @@ -262,6 +263,128 @@ describe("useStreamEvents", () => {
);
});

it("summarizes evlog request previews without showing raw JSON", () => {
const events = normalizeStreamEvents({
events: [
{
timestamp: "2026-06-12T17:05:10.935Z",
level: "info",
service: "storefront",
method: "GET",
path: "/product/acme-mug",
status: 200,
duration: 0,
message: "Storefront request",
},
{
timestamp: "2026-06-12T17:09:18.912Z",
level: "error",
service: "storefront",
method: "POST",
path: "/api/observability/simulate",
status: 500,
message: "Synthetic diagnostic: Trace fanout",
},
],
startExclusiveSequence: 0n,
stream: {
epoch: 0,
name: "webshop-production-events",
profile: "evlog",
},
});

expect(events.map((event) => event.preview)).toEqual([
"GET /product/acme-mug",
"POST /api/observability/simulate 500 Synthetic diagnostic: Trace fanout",
]);
});

it("keeps generic previews for non-evlog streams", () => {
const events = normalizeStreamEvents({
events: [
{
method: "GET",
path: "/product/acme-mug",
status: 200,
message: "Storefront request",
},
],
startExclusiveSequence: 0n,
stream: {
epoch: 0,
name: "generic-json-events",
profile: null,
},
});

expect(events[0]?.preview).toBe(
'{"method":"GET","path":"/product/acme-mug","status":200,"message":"Storefront request"}',
);
});

it("summarizes otel trace span previews without showing raw JSON", () => {
const events = normalizeStreamEvents({
events: [
{
attributes: {
"http.request.method": "POST",
"http.response.status_code": 200,
"url.path": "/api/query-insights/snapshot",
},
endUnixNano: "1810000003486000000",
kind: "server",
name: "fetchHandler POST",
resource: {
attributes: {
"service.name": "console",
},
},
spanId: "086e83747d0e381e",
startUnixNano: "1810000000000000000",
status: { code: "ok", message: null },
traceId: "5b8efff798038103d269b633813fc60c",
},
{
attributes: { "url.full": "https://payments.internal/charges" },
endUnixNano: "1810000000192000000",
events: [
{
attributes: {
"exception.message": "Card declined by issuer",
"exception.type": "CardDeclinedError",
},
name: "exception",
timeUnixNano: "1810000000190000000",
},
],
kind: "client",
name: "POST payments /charges",
resource: {
attributes: {
"service.name": "payments",
},
},
spanId: "22dd83747d0e3822",
startUnixNano: "1810000000041000000",
status: { code: "error", message: "402 from issuer" },
traceId: "5b8efff798038103d269b633813fc60c",
},
],
startExclusiveSequence: 0n,
stream: {
epoch: 0,
name: "webshop-production-traces",
profile: "otel-traces",
},
});

expect(events.map((event) => event.preview)).toEqual([
"POST /api/query-insights/snapshot | console | 3.49s",
"POST payments /charges | payments | 151ms | error: 402 from issuer",
]);
});

it("loads a tail window and normalizes events into newest-first rows", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(
Expand Down
Loading