diff --git a/frontend/console/src/features/calls/CallList.tsx b/frontend/console/src/features/calls/CallList.tsx
index 56a7900008..a45fdd1f74 100644
--- a/frontend/console/src/features/calls/CallList.tsx
+++ b/frontend/console/src/features/calls/CallList.tsx
@@ -16,8 +16,7 @@ export const CallList = ({ calls }: { calls: Event[] | undefined }) => {
return
}
setSelectedCallId(event.id)
- const call = event.entry.value as CallEvent
- openPanel()
+ openPanel()
}
return (
diff --git a/frontend/console/src/features/timeline/Timeline.tsx b/frontend/console/src/features/timeline/Timeline.tsx
index 59709988a8..c5c7f1aea5 100644
--- a/frontend/console/src/features/timeline/Timeline.tsx
+++ b/frontend/console/src/features/timeline/Timeline.tsx
@@ -1,4 +1,3 @@
-import type { Timestamp } from '@bufbuild/protobuf'
import { useContext, useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { timeFilter, useTimeline } from '../../api/timeline/index.ts'
@@ -56,7 +55,7 @@ export const Timeline = ({ timeSettings, filters }: { timeSettings: TimeSettings
switch (entry.entry?.case) {
case 'call':
- openPanel(, handlePanelClosed)
+ openPanel(, handlePanelClosed)
break
case 'log':
openPanel(, handlePanelClosed)
@@ -68,7 +67,7 @@ export const Timeline = ({ timeSettings, filters }: { timeSettings: TimeSettings
openPanel(, handlePanelClosed)
break
case 'ingress':
- openPanel(, handlePanelClosed)
+ openPanel(, handlePanelClosed)
break
default:
break
diff --git a/frontend/console/src/features/timeline/TimelinePage.tsx b/frontend/console/src/features/timeline/TimelinePage.tsx
index 1655e24f33..f56c829f7c 100644
--- a/frontend/console/src/features/timeline/TimelinePage.tsx
+++ b/frontend/console/src/features/timeline/TimelinePage.tsx
@@ -18,7 +18,7 @@ export const TimelinePage = () => {
useEffect(() => {
if (initialEventId) {
// if we're loading a specific event, we don't want to tail.
- setSelectedTimeRange(TIME_RANGES['5m'])
+ setSelectedTimeRange(TIME_RANGES['24h'])
setIsTimelinePaused(true)
}
}, [])
diff --git a/frontend/console/src/features/timeline/details/TimelineCallDetails.tsx b/frontend/console/src/features/timeline/details/TimelineCallDetails.tsx
index ca731380f4..8710153ef3 100644
--- a/frontend/console/src/features/timeline/details/TimelineCallDetails.tsx
+++ b/frontend/console/src/features/timeline/details/TimelineCallDetails.tsx
@@ -1,4 +1,3 @@
-import type { Timestamp } from '@bufbuild/protobuf'
import { useContext } from 'react'
import { AttributeBadge } from '../../../components/AttributeBadge'
import { CloseButton } from '../../../components/CloseButton'
@@ -8,10 +7,11 @@ import { SidePanelContext } from '../../../providers/side-panel-provider'
import { formatDuration } from '../../../utils/date.utils'
import { DeploymentCard } from '../../deployments/DeploymentCard'
import { TraceGraph } from '../../traces/TraceGraph'
+import { TraceGraphHeader } from '../../traces/TraceGraphHeader'
import { verbRefString } from '../../verbs/verb.utils'
import { TimelineTimestamp } from './TimelineTimestamp'
-export const TimelineCallDetails = ({ timestamp, event }: { timestamp?: Timestamp; event: Event }) => {
+export const TimelineCallDetails = ({ event }: { event: Event }) => {
const { closePanel } = useContext(SidePanelContext)
const call = event.entry.value as CallEvent
@@ -26,12 +26,13 @@ export const TimelineCallDetails = ({ timestamp, event }: { timestamp?: Timestam
)}
-
+
-
+
+
diff --git a/frontend/console/src/features/timeline/details/TimelineIngressDetails.tsx b/frontend/console/src/features/timeline/details/TimelineIngressDetails.tsx
index c2c09bf8e3..7a404133da 100644
--- a/frontend/console/src/features/timeline/details/TimelineIngressDetails.tsx
+++ b/frontend/console/src/features/timeline/details/TimelineIngressDetails.tsx
@@ -1,4 +1,3 @@
-import type { Timestamp } from '@bufbuild/protobuf'
import { useContext } from 'react'
import { AttributeBadge } from '../../../components/AttributeBadge'
import { CloseButton } from '../../../components/CloseButton'
@@ -8,10 +7,11 @@ import { SidePanelContext } from '../../../providers/side-panel-provider'
import { formatDuration } from '../../../utils/date.utils'
import { DeploymentCard } from '../../deployments/DeploymentCard'
import { TraceGraph } from '../../traces/TraceGraph'
+import { TraceGraphHeader } from '../../traces/TraceGraphHeader'
import { verbRefString } from '../../verbs/verb.utils'
import { TimelineTimestamp } from './TimelineTimestamp'
-export const TimelineIngressDetails = ({ timestamp, event }: { timestamp?: Timestamp; event: Event }) => {
+export const TimelineIngressDetails = ({ event }: { event: Event }) => {
const { closePanel } = useContext(SidePanelContext)
const ingress = event.entry.value as IngressEvent
@@ -26,12 +26,13 @@ export const TimelineIngressDetails = ({ timestamp, event }: { timestamp?: Times
)}
-
+
-
+
+
diff --git a/frontend/console/src/features/traces/TraceDetails.tsx b/frontend/console/src/features/traces/TraceDetails.tsx
new file mode 100644
index 0000000000..9f145051af
--- /dev/null
+++ b/frontend/console/src/features/traces/TraceDetails.tsx
@@ -0,0 +1,97 @@
+import type React from 'react'
+import { useNavigate } from 'react-router-dom'
+import type { TraceEvent } from '../../api/timeline/use-request-trace-events'
+import { CallEvent, type Event, IngressEvent } from '../../protos/xyz/block/ftl/v1/console/console_pb'
+import { TimelineIcon } from '../timeline/TimelineIcon'
+
+interface TraceDetailsProps {
+ requestKey: string
+ events: Event[]
+ selectedEventId?: bigint
+}
+
+export const TraceDetails: React.FC
= ({ events, selectedEventId, requestKey }) => {
+ const navigate = useNavigate()
+
+ const firstTimeStamp = events[0]?.timeStamp
+ const firstEvent = events[0].entry.value as TraceEvent
+ const firstDuration = firstEvent?.duration
+ const totalDurationMillis = (firstDuration?.nanos ?? 0) / 1000000
+
+ const handleEventClick = (eventId: bigint) => {
+ navigate(`/traces/${requestKey}?event_id=${eventId}`)
+ }
+
+ return (
+
+
+
+ Total Duration: {totalDurationMillis} ms
+
+
+ Start Time: {firstTimeStamp?.toDate().toLocaleString()}
+
+
+
+
+ {events.map((event, index) => {
+ const traceEvent = event.entry.value as TraceEvent
+ const durationInMillis = (traceEvent.duration?.nanos ?? 0) / 1000000
+
+ let width = (durationInMillis / totalDurationMillis) * 100
+ if (width < 1) width = 1
+
+ const callTime = traceEvent.timeStamp?.toDate() ?? new Date()
+ const initialTime = firstTimeStamp?.toDate() ?? new Date()
+ const offsetInMillis = callTime.getTime() - initialTime.getTime()
+ const leftOffsetPercentage = (offsetInMillis / totalDurationMillis) * 100
+
+ let barColor = 'bg-pink-500'
+ let action = ''
+ let eventName = ''
+ const icon =
+
+ if (traceEvent instanceof CallEvent) {
+ barColor = 'bg-indigo-500'
+ action = 'Call'
+ eventName = `${traceEvent.destinationVerbRef?.module}.${traceEvent.destinationVerbRef?.name}`
+ } else if (traceEvent instanceof IngressEvent) {
+ barColor = 'bg-yellow-500'
+ action = `HTTP ${traceEvent.method}`
+ eventName = `${traceEvent.path}`
+ }
+
+ if (event.id === selectedEventId) {
+ barColor = 'bg-pink-500'
+ }
+
+ const isSelected = event.id === selectedEventId
+ const listItemClass = isSelected
+ ? 'flex items-center justify-between p-2 bg-indigo-100/50 dark:bg-indigo-700 rounded cursor-pointer'
+ : 'flex items-center justify-between p-2 hover:bg-indigo-500/10 rounded cursor-pointer'
+
+ return (
+ - handleEventClick(event.id)}>
+
+ {icon}
+ {action}
+ {eventName}
+
+
+
+ {durationInMillis} ms
+
+ )
+ })}
+
+
+ )
+}
diff --git a/frontend/console/src/features/traces/TraceGraph.tsx b/frontend/console/src/features/traces/TraceGraph.tsx
index 2509223b75..165826cf26 100644
--- a/frontend/console/src/features/traces/TraceGraph.tsx
+++ b/frontend/console/src/features/traces/TraceGraph.tsx
@@ -35,7 +35,7 @@ const EventBlock = ({
barColor = 'bg-indigo-500'
eventTarget = `${event.destinationVerbRef?.module}.${event.destinationVerbRef?.name}`
} else if (event instanceof IngressEvent) {
- barColor = 'bg-blue-500'
+ barColor = 'bg-yellow-500'
eventTarget = event.path
}
diff --git a/frontend/console/src/features/traces/TraceGraphHeader.tsx b/frontend/console/src/features/traces/TraceGraphHeader.tsx
new file mode 100644
index 0000000000..9752953ac1
--- /dev/null
+++ b/frontend/console/src/features/traces/TraceGraphHeader.tsx
@@ -0,0 +1,39 @@
+import { Activity03Icon } from 'hugeicons-react'
+import { useNavigate } from 'react-router-dom'
+import { type TraceEvent, useRequestTraceEvents } from '../../api/timeline/use-request-trace-events'
+
+export const TraceGraphHeader = ({ requestKey, eventId }: { requestKey?: string; eventId: bigint }) => {
+ const navigate = useNavigate()
+ const requestEvents = useRequestTraceEvents(requestKey)
+ const events = requestEvents.data?.reverse() ?? []
+
+ if (events.length === 0) {
+ return null
+ }
+
+ const firstTimeStamp = events[0].timeStamp
+ const traceEvent = events[0].entry.value as TraceEvent
+ const firstDuration = traceEvent.duration
+ if (firstTimeStamp === undefined || firstDuration === undefined) {
+ return null
+ }
+
+ const totalDurationMillis = (firstDuration.nanos ?? 0) / 1000000
+
+ return (
+
+
+ Total {totalDurationMillis}ms
+
+
+
+
+ )
+}
diff --git a/frontend/console/src/features/traces/TracesPage.tsx b/frontend/console/src/features/traces/TracesPage.tsx
new file mode 100644
index 0000000000..b22a54cc2c
--- /dev/null
+++ b/frontend/console/src/features/traces/TracesPage.tsx
@@ -0,0 +1,75 @@
+import { ArrowLeft02Icon } from 'hugeicons-react'
+import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
+import { useRequestTraceEvents } from '../../api/timeline/use-request-trace-events'
+import { Loader } from '../../components/Loader'
+import { TraceDetails } from './TraceDetails'
+import { TraceDetailsCall } from './details/TraceDetailsCall'
+import { TraceDetailsIngress } from './details/TraceDetailsIngress'
+
+export const TracesPage = () => {
+ const navigate = useNavigate()
+
+ const { requestKey } = useParams<{ requestKey: string }>()
+ const requestEvents = useRequestTraceEvents(requestKey)
+ const events = requestEvents.data?.reverse() ?? []
+
+ const [searchParams] = useSearchParams()
+ const eventIdParam = searchParams.get('event_id')
+ const selectedEventId = eventIdParam ? BigInt(eventIdParam) : undefined
+
+ if (events.length === 0) {
+ return
+ }
+
+ if (requestKey === undefined) {
+ return
+ }
+
+ const handleBack = () => {
+ if (window.history.length > 1) {
+ navigate(-1)
+ } else {
+ navigate('/modules')
+ }
+ }
+
+ if (requestEvents.isLoading) {
+ return (
+
+
+
+ )
+ }
+
+ const selectedEvent = events.find((event) => event.id === selectedEventId)
+ let eventDetailsComponent: React.ReactNode
+ switch (selectedEvent?.entry.case) {
+ case 'call':
+ eventDetailsComponent =
+ break
+ case 'ingress':
+ eventDetailsComponent =
+ break
+ default:
+ eventDetailsComponent = No details available for this event type.
+ break
+ }
+
+ return (
+
+
+
+
+
+
{eventDetailsComponent}
+
+ )
+}
diff --git a/frontend/console/src/features/traces/details/TraceDetailsCall.tsx b/frontend/console/src/features/traces/details/TraceDetailsCall.tsx
new file mode 100644
index 0000000000..91600650b5
--- /dev/null
+++ b/frontend/console/src/features/traces/details/TraceDetailsCall.tsx
@@ -0,0 +1,60 @@
+import { AttributeBadge } from '../../../components/AttributeBadge'
+import { CodeBlock } from '../../../components/CodeBlock'
+import type { CallEvent, Event } from '../../../protos/xyz/block/ftl/v1/console/console_pb'
+import { formatDuration } from '../../../utils/date.utils'
+import { DeploymentCard } from '../../deployments/DeploymentCard'
+import { verbRefString } from '../../verbs/verb.utils'
+
+export const TraceDetailsCall = ({ event }: { event: Event }) => {
+ const call = event.entry.value as CallEvent
+ return (
+ <>
+ Call Details
+ Request
+
+
+ {call.response !== 'null' && (
+ <>
+ Response
+
+ >
+ )}
+
+ {call.error && (
+ <>
+ Error
+
+ {call.stack && (
+ <>
+ Stack
+
+ >
+ )}
+ >
+ )}
+
+
+
+
+ {call.requestKey && (
+ -
+
+
+ )}
+ -
+
+
+ {call.destinationVerbRef && (
+ -
+
+
+ )}
+ {call.sourceVerbRef && (
+ -
+
+
+ )}
+
+ >
+ )
+}
diff --git a/frontend/console/src/features/traces/details/TraceDetailsIngress.tsx b/frontend/console/src/features/traces/details/TraceDetailsIngress.tsx
new file mode 100644
index 0000000000..2d70bc6f3b
--- /dev/null
+++ b/frontend/console/src/features/traces/details/TraceDetailsIngress.tsx
@@ -0,0 +1,72 @@
+import { AttributeBadge } from '../../../components/AttributeBadge'
+import { CodeBlock } from '../../../components/CodeBlock'
+import type { Event, IngressEvent } from '../../../protos/xyz/block/ftl/v1/console/console_pb'
+import { formatDuration } from '../../../utils/date.utils'
+import { DeploymentCard } from '../../deployments/DeploymentCard'
+import { verbRefString } from '../../verbs/verb.utils'
+
+export const TraceDetailsIngress = ({ event }: { event: Event }) => {
+ const ingress = event.entry.value as IngressEvent
+ return (
+ <>
+ Call Details
+ Request
+
+
+ {ingress.response !== 'null' && (
+ <>
+ Response
+
+ >
+ )}
+
+ {ingress.requestHeader !== 'null' && (
+ <>
+ Request Header
+
+ >
+ )}
+
+ {ingress.responseHeader !== 'null' && (
+ <>
+ Response Header
+
+ >
+ )}
+
+ {ingress.error && (
+ <>
+ Error
+
+ >
+ )}
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ {ingress.requestKey && (
+ -
+
+
+ )}
+ -
+
+
+ {ingress.verbRef && (
+ -
+
+
+ )}
+
+ >
+ )
+}
diff --git a/frontend/console/src/providers/routing-provider.tsx b/frontend/console/src/providers/routing-provider.tsx
index 9e1025652d..7e6494e6a5 100644
--- a/frontend/console/src/providers/routing-provider.tsx
+++ b/frontend/console/src/providers/routing-provider.tsx
@@ -5,6 +5,7 @@ import { ModulePanel } from '../features/modules/ModulePanel'
import { ModulesPage, ModulesPanel } from '../features/modules/ModulesPage'
import { DeclPanel } from '../features/modules/decls/DeclPanel'
import { TimelinePage } from '../features/timeline/TimelinePage'
+import { TracesPage } from '../features/traces/TracesPage'
import { Layout } from '../layout/Layout'
import { NotFoundPage } from '../layout/NotFoundPage'
@@ -20,6 +21,7 @@ const router = createBrowserRouter(
} />
} />
} />
+ } />
} />