Enhance Traces Dashboard with latency percentiles and service health metrics

- Added global P50, P95, and P99 latency calculations to the TracesDashboard component.
- Introduced a new formatDuration function to format latency values for display.
- Updated service summaries to include per-service P50 and P95 latencies.
- Improved the layout of the dashboard to display overall requests, error rates, and latency metrics.
- Refactored service health indicators to visually represent error rates with color coding.
- Adjusted the display of recent errors and slow requests for better user experience.
- Updated E2E tests for admin and public dashboards to include response type checks.
This commit is contained in:
Nawaz Dhandala
2026-04-03 11:34:06 +01:00
parent 577d8d2fba
commit 26bcc69fa2
6 changed files with 1471 additions and 666 deletions

View File

@@ -16,6 +16,7 @@ import SortOrder from "Common/Types/BaseDatabase/SortOrder";
import ListResult from "Common/Types/BaseDatabase/ListResult";
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import ObjectID from "Common/Types/ObjectID";
import OneUptimeDate from "Common/Types/Date";
import TelemetryServiceElement from "../TelemetryService/TelemetryServiceElement";
import TelemetryExceptionElement from "./ExceptionElement";
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
@@ -36,6 +37,9 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
const [topExceptions, setTopExceptions] = useState<Array<TelemetryException>>(
[],
);
const [recentExceptions, setRecentExceptions] = useState<
Array<TelemetryException>
>([]);
const [serviceSummaries, setServiceSummaries] = useState<
Array<ServiceExceptionSummary>
>([]);
@@ -49,12 +53,12 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
const projectId: ObjectID = ProjectUtil.getCurrentProjectId()!;
// Load counts, top exceptions, and services in parallel
const [
unresolvedResult,
resolvedResult,
archivedResult,
topExceptionsResult,
recentExceptionsResult,
servicesResult,
] = await Promise.all([
ModelAPI.count({
@@ -108,6 +112,34 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
occuranceCount: SortOrder.Descending,
},
}),
ModelAPI.getList({
modelType: TelemetryException,
query: {
projectId,
isResolved: false,
isArchived: false,
},
select: {
message: true,
exceptionType: true,
fingerprint: true,
isResolved: true,
isArchived: true,
occuranceCount: true,
lastSeenAt: true,
firstSeenAt: true,
environment: true,
service: {
name: true,
serviceColor: true,
} as any,
},
limit: 8,
skip: 0,
sort: {
lastSeenAt: SortOrder.Descending,
},
}),
ModelAPI.getList({
modelType: Service,
query: {
@@ -129,6 +161,7 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
setResolvedCount(resolvedResult);
setArchivedCount(archivedResult);
setTopExceptions(topExceptionsResult.data || []);
setRecentExceptions(recentExceptionsResult.data || []);
const loadedServices: Array<Service> = servicesResult.data || [];
@@ -136,7 +169,6 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
const serviceExceptionCounts: Array<ServiceExceptionSummary> = [];
for (const service of loadedServices) {
// Get unresolved exceptions for this service
const serviceExceptions: ListResult<TelemetryException> =
await ModelAPI.getList({
modelType: TelemetryException,
@@ -161,11 +193,9 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
if (exceptions.length > 0) {
let totalOccurrences: number = 0;
for (const ex of exceptions) {
totalOccurrences += ex.occuranceCount || 0;
}
serviceExceptionCounts.push({
service,
unresolvedCount: exceptions.length,
@@ -174,11 +204,9 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
}
}
// Sort by unresolved count descending
serviceExceptionCounts.sort(
(a: ServiceExceptionSummary, b: ServiceExceptionSummary) => {
return b.unresolvedCount - a.unresolvedCount;
},
(a: ServiceExceptionSummary, b: ServiceExceptionSummary) =>
b.unresolvedCount - a.unresolvedCount,
);
setServiceSummaries(serviceExceptionCounts);
@@ -212,69 +240,145 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
if (totalCount === 0) {
return (
<div className="rounded-lg border border-gray-200 bg-white p-12 text-center">
<div className="text-gray-400 text-5xl mb-4">
<div className="rounded-xl border border-gray-200 bg-white p-16 text-center">
<div className="mx-auto w-16 h-16 rounded-full bg-green-50 flex items-center justify-center mb-5">
<svg
className="mx-auto h-12 w-12"
className="h-8 w-8 text-green-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 12.75c1.148 0 2.278.08 3.383.237 1.037.146 1.866.966 1.866 2.013 0 3.728-2.35 6.75-5.25 6.75S6.75 18.728 6.75 15c0-1.046.83-1.867 1.866-2.013A24.204 24.204 0 0112 12.75zm0 0c2.883 0 5.647.508 8.207 1.44a23.91 23.91 0 01-1.152 6.06M12 12.75c-2.883 0-5.647.508-8.208 1.44.125 2.104.52 4.136 1.153 6.06M12 12.75a2.25 2.25 0 002.248-2.354M12 12.75a2.25 2.25 0 01-2.248-2.354M12 8.25c.995 0 1.971-.08 2.922-.236.403-.066.74-.358.795-.762a3.778 3.778 0 00-.399-2.25M12 8.25c-.995 0-1.97-.08-2.922-.236-.402-.066-.74-.358-.795-.762a3.734 3.734 0 01.4-2.253M12 8.25a2.25 2.25 0 00-2.248 2.146M12 8.25a2.25 2.25 0 012.248 2.146M8.683 5a6.032 6.032 0 01-1.155-1.002c.07-.63.27-1.222.574-1.747m.581 2.749A3.75 3.75 0 0115.318 5m0 0c.427-.283.815-.62 1.155-.999a4.471 4.471 0 00-.575-1.752M4.921 12s-.148-.277-.277-.5M19.08 12s.147-.277.277-.5"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
No exceptions caught yet
</h3>
<p className="text-sm text-gray-500 max-w-md mx-auto">
Once your services start reporting exceptions, you{"'"}ll see a
summary of bugs, their frequency, and which services are most
affected.
<p className="text-sm text-gray-500 max-w-sm mx-auto leading-relaxed">
Once your services start reporting exceptions, you{"'"}ll see bug
frequency, affected services, and resolution status here.
</p>
</div>
);
}
const resolutionRate: number =
totalCount > 0
? Math.round(((resolvedCount + archivedCount) / totalCount) * 100)
: 0;
// Count how many of the top exceptions were first seen in last 24h
const now: Date = OneUptimeDate.getCurrentDate();
const oneDayAgo: Date = OneUptimeDate.addRemoveHours(now, -24);
const newTodayCount: number = topExceptions.filter(
(e: TelemetryException) => {
return e.firstSeenAt && new Date(e.firstSeenAt) > oneDayAgo;
},
).length;
const maxServiceBugs: number =
serviceSummaries.length > 0 ? serviceSummaries[0]!.unresolvedCount : 1;
return (
<Fragment>
{/* Unresolved Alert Banner */}
{unresolvedCount > 0 && (
<AppLink
className="block mb-6"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.EXCEPTIONS_UNRESOLVED] as Route,
)}
>
<div
className={`rounded-xl p-4 flex items-center justify-between ${unresolvedCount > 20 ? "bg-red-50 border border-red-200" : unresolvedCount > 5 ? "bg-amber-50 border border-amber-200" : "bg-blue-50 border border-blue-200"}`}
>
<div className="flex items-center gap-3">
<div
className={`w-10 h-10 rounded-lg flex items-center justify-center ${unresolvedCount > 20 ? "bg-red-100" : unresolvedCount > 5 ? "bg-amber-100" : "bg-blue-100"}`}
>
<svg
className={`h-5 w-5 ${unresolvedCount > 20 ? "text-red-600" : unresolvedCount > 5 ? "text-amber-600" : "text-blue-600"}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 12.75c1.148 0 2.278.08 3.383.237 1.037.146 1.866.966 1.866 2.013 0 3.728-2.35 6.75-5.25 6.75S6.75 18.728 6.75 15c0-1.046.83-1.867 1.866-2.013A24.204 24.204 0 0112 12.75zm0 0c2.883 0 5.647.508 8.207 1.44a23.91 23.91 0 01-1.152 6.06M12 12.75c-2.883 0-5.647.508-8.208 1.44.125 2.104.52 4.136 1.153 6.06M12 12.75a2.25 2.25 0 002.248-2.354M12 12.75a2.25 2.25 0 01-2.248-2.354M12 8.25c.995 0 1.971-.08 2.922-.236.403-.066.74-.358.795-.762a3.778 3.778 0 00-.399-2.25M12 8.25c-.995 0-1.97-.08-2.922-.236-.402-.066-.74-.358-.795-.762a3.734 3.734 0 01.4-2.253M12 8.25a2.25 2.25 0 00-2.248 2.146M12 8.25a2.25 2.25 0 012.248 2.146M8.683 5a6.032 6.032 0 01-1.155-1.002c.07-.63.27-1.222.574-1.747m.581 2.749A3.75 3.75 0 0115.318 5m0 0c.427-.283.815-.62 1.155-.999a4.471 4.471 0 00-.575-1.752M4.921 12s-.148-.277-.277-.5M19.08 12s.147-.277.277-.5"
/>
</svg>
</div>
<div>
<p
className={`text-sm font-semibold ${unresolvedCount > 20 ? "text-red-800" : unresolvedCount > 5 ? "text-amber-800" : "text-blue-800"}`}
>
{unresolvedCount} unresolved{" "}
{unresolvedCount === 1 ? "bug" : "bugs"} need attention
</p>
<p
className={`text-xs mt-0.5 ${unresolvedCount > 20 ? "text-red-600" : unresolvedCount > 5 ? "text-amber-600" : "text-blue-600"}`}
>
{newTodayCount > 0
? `${newTodayCount} new in the last 24 hours`
: "Click to view and triage"}
</p>
</div>
</div>
<svg
className={`h-5 w-5 ${unresolvedCount > 20 ? "text-red-400" : unresolvedCount > 5 ? "text-amber-400" : "text-blue-400"}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.25 4.5l7.5 7.5-7.5 7.5"
/>
</svg>
</div>
</AppLink>
)}
{/* Summary Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<AppLink
className="block"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.EXCEPTIONS_UNRESOLVED] as Route,
)}
>
<div className="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow">
<div className="rounded-xl border border-gray-200 bg-white p-5 hover:border-red-200 hover:shadow-sm transition-all">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">Unresolved Bugs</p>
<p className="text-3xl font-bold text-red-600 mt-1">
{unresolvedCount.toLocaleString()}
</p>
</div>
<div className="h-12 w-12 rounded-full bg-red-50 flex items-center justify-center">
<p className="text-sm font-medium text-gray-500">Unresolved</p>
<div className="h-8 w-8 rounded-lg bg-red-50 flex items-center justify-center">
<svg
className="h-6 w-6 text-red-500"
className="h-4 w-4 text-red-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
/>
</svg>
</div>
</div>
<p className="text-xs text-gray-400 mt-2">Needs attention</p>
<p className="text-3xl font-bold text-red-600 mt-2">
{unresolvedCount.toLocaleString()}
</p>
<p className="text-xs text-gray-400 mt-1">needs attention</p>
</div>
</AppLink>
@@ -284,31 +388,29 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
RouteMap[PageMap.EXCEPTIONS_RESOLVED] as Route,
)}
>
<div className="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow">
<div className="rounded-xl border border-gray-200 bg-white p-5 hover:border-green-200 hover:shadow-sm transition-all">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">Resolved</p>
<p className="text-3xl font-bold text-green-600 mt-1">
{resolvedCount.toLocaleString()}
</p>
</div>
<div className="h-12 w-12 rounded-full bg-green-50 flex items-center justify-center">
<p className="text-sm font-medium text-gray-500">Resolved</p>
<div className="h-8 w-8 rounded-lg bg-green-50 flex items-center justify-center">
<svg
className="h-6 w-6 text-green-500"
className="h-4 w-4 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
<p className="text-xs text-gray-400 mt-2">Fixed and verified</p>
<p className="text-3xl font-bold text-green-600 mt-2">
{resolvedCount.toLocaleString()}
</p>
<p className="text-xs text-gray-400 mt-1">fixed</p>
</div>
</AppLink>
@@ -318,49 +420,73 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
RouteMap[PageMap.EXCEPTIONS_ARCHIVED] as Route,
)}
>
<div className="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow">
<div className="rounded-xl border border-gray-200 bg-white p-5 hover:border-gray-300 hover:shadow-sm transition-all">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">Archived</p>
<p className="text-3xl font-bold text-gray-600 mt-1">
{archivedCount.toLocaleString()}
</p>
</div>
<div className="h-12 w-12 rounded-full bg-gray-50 flex items-center justify-center">
<p className="text-sm font-medium text-gray-500">Archived</p>
<div className="h-8 w-8 rounded-lg bg-gray-100 flex items-center justify-center">
<svg
className="h-6 w-6 text-gray-400"
className="h-4 w-4 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
/>
</svg>
</div>
</div>
<p className="text-xs text-gray-400 mt-2">
Dismissed or won{"'"}t fix
<p className="text-3xl font-bold text-gray-600 mt-2">
{archivedCount.toLocaleString()}
</p>
<p className="text-xs text-gray-400 mt-1">dismissed</p>
</div>
</AppLink>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Resolution Rate</p>
<div className="h-8 w-8 rounded-lg bg-indigo-50 flex items-center justify-center">
<svg
className="h-4 w-4 text-indigo-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 18L9 11.25l4.306 4.307a11.95 11.95 0 015.814-5.519l2.74-1.22m0 0l-5.94-2.28m5.94 2.28l-2.28 5.941"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{resolutionRate}%
</p>
<div className="w-full h-1.5 bg-gray-100 rounded-full overflow-hidden mt-2">
<div
className="h-full rounded-full bg-indigo-400"
style={{ width: `${resolutionRate}%` }}
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Most Frequent Exceptions */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Most Frequent Exceptions - takes 2 columns */}
{topExceptions.length > 0 && (
<div>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">
<div className="lg:col-span-2">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-red-500" />
<h3 className="text-base font-semibold text-gray-900">
Most Frequent Bugs
</h3>
<p className="text-sm text-gray-500 mt-1">
Unresolved exceptions with the highest occurrence count
</p>
</div>
<AppLink
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
@@ -371,8 +497,8 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
View all
</AppLink>
</div>
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-100">
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-50">
{topExceptions.map(
(exception: TelemetryException, index: number) => {
const maxOccurrences: number =
@@ -380,6 +506,11 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
const barWidth: number =
((exception.occuranceCount || 0) / maxOccurrences) * 100;
const isNewToday: boolean = !!(
exception.firstSeenAt &&
new Date(exception.firstSeenAt) > oneDayAgo
);
return (
<AppLink
key={exception.id?.toString() || index.toString()}
@@ -393,7 +524,10 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
] as Route,
)
.toString()
.replace(/\/$/, `/${exception.fingerprint}`),
.replace(
/\/?$/,
`/${exception.fingerprint}`,
),
)
: RouteUtil.populateRouteParams(
RouteMap[
@@ -402,39 +536,60 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
)
}
>
<div className="flex items-start justify-between mb-1">
<div className="flex items-start justify-between mb-1.5">
<div className="min-w-0 flex-1 mr-3">
<TelemetryExceptionElement
message={
exception.message ||
exception.exceptionType ||
"Unknown exception"
}
isResolved={exception.isResolved || false}
isArchived={exception.isArchived || false}
className="text-sm"
/>
<div className="flex items-center space-x-3 mt-1">
<div className="flex items-center gap-2 mb-1">
<TelemetryExceptionElement
message={
exception.message ||
exception.exceptionType ||
"Unknown exception"
}
isResolved={exception.isResolved || false}
isArchived={exception.isArchived || false}
className="text-sm"
/>
{isNewToday && (
<span className="flex-shrink-0 text-xs bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded font-medium">
New
</span>
)}
</div>
<div className="flex items-center gap-2 mt-1">
{exception.service && (
<span className="text-xs text-gray-500">
{exception.service.name?.toString()}
</span>
)}
{exception.exceptionType && (
<span className="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded font-mono">
{exception.exceptionType}
</span>
)}
{exception.environment && (
<span className="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded">
{exception.environment}
</span>
)}
{exception.lastSeenAt && (
<span className="text-xs text-gray-400">
{OneUptimeDate.fromNow(
new Date(exception.lastSeenAt),
)}
</span>
)}
</div>
</div>
<div className="text-right flex-shrink-0">
<p className="text-sm font-semibold text-gray-900">
{(exception.occuranceCount || 0).toLocaleString()}
<p className="text-sm font-bold text-gray-900">
{(
exception.occuranceCount || 0
).toLocaleString()}
</p>
<p className="text-xs text-gray-400">occurrences</p>
<p className="text-xs text-gray-400">hits</p>
</div>
</div>
<div className="mt-1">
<div className="mt-1.5">
<div className="w-full h-1 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full bg-red-400"
@@ -451,51 +606,136 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => {
</div>
)}
{/* Services Affected */}
{serviceSummaries.length > 0 && (
<div>
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-900">
Affected Services
</h3>
<p className="text-sm text-gray-500 mt-1">
Services with unresolved exceptions
</p>
</div>
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-100">
{serviceSummaries.map((summary: ServiceExceptionSummary) => {
return (
<div
key={summary.service.id?.toString()}
className="px-4 py-4"
>
<div className="flex items-center justify-between mb-2">
<TelemetryServiceElement
telemetryService={summary.service}
/>
<div className="flex items-center space-x-4">
<div className="text-right">
<p className="text-sm font-semibold text-red-600">
{summary.unresolvedCount}
</p>
<p className="text-xs text-gray-400">unresolved</p>
{/* Right sidebar: Affected Services + Recently Seen */}
<div className="space-y-6">
{/* Affected Services */}
{serviceSummaries.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 rounded-full bg-amber-500" />
<h3 className="text-base font-semibold text-gray-900">
Affected Services
</h3>
</div>
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-50">
{serviceSummaries.map(
(summary: ServiceExceptionSummary) => {
const barWidth: number =
(summary.unresolvedCount / maxServiceBugs) * 100;
return (
<div
key={summary.service.id?.toString()}
className="px-4 py-3"
>
<div className="flex items-center justify-between mb-2">
<TelemetryServiceElement
telemetryService={summary.service}
/>
<div className="flex items-center gap-3">
<div className="text-right">
<span className="text-sm font-bold text-red-600">
{summary.unresolvedCount}
</span>
<span className="text-xs text-gray-400 ml-1">
bugs
</span>
</div>
</div>
</div>
<div className="text-right">
<p className="text-sm font-semibold text-gray-700">
{summary.totalOccurrences.toLocaleString()}
</p>
<p className="text-xs text-gray-400">total hits</p>
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full bg-red-400"
style={{
width: `${Math.max(barWidth, 3)}%`,
}}
/>
</div>
<span className="text-xs text-gray-400 flex-shrink-0">
{summary.totalOccurrences.toLocaleString()} hits
</span>
</div>
</div>
</div>
</div>
);
})}
);
},
)}
</div>
</div>
</div>
</div>
)}
)}
{/* Recently Active */}
{recentExceptions.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 rounded-full bg-blue-500" />
<h3 className="text-base font-semibold text-gray-900">
Recently Active
</h3>
</div>
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-50">
{recentExceptions
.slice(0, 5)
.map(
(exception: TelemetryException, index: number) => {
return (
<AppLink
key={
exception.id?.toString() || index.toString()
}
className="block px-4 py-3 hover:bg-gray-50 transition-colors"
to={
exception.fingerprint
? new Route(
RouteUtil.populateRouteParams(
RouteMap[
PageMap.EXCEPTIONS_VIEW_ROOT
] as Route,
)
.toString()
.replace(
/\/?$/,
`/${exception.fingerprint}`,
),
)
: RouteUtil.populateRouteParams(
RouteMap[
PageMap.EXCEPTIONS_UNRESOLVED
] as Route,
)
}
>
<p className="text-sm text-gray-900 truncate font-medium">
{exception.message ||
exception.exceptionType ||
"Unknown"}
</p>
<div className="flex items-center gap-2 mt-1">
{exception.service && (
<span className="text-xs text-gray-500">
{exception.service.name?.toString()}
</span>
)}
{exception.lastSeenAt && (
<span className="text-xs text-gray-400">
{OneUptimeDate.fromNow(
new Date(exception.lastSeenAt),
)}
</span>
)}
</div>
</AppLink>
);
},
)}
</div>
</div>
</div>
)}
</div>
</div>
</Fragment>
);

View File

@@ -13,7 +13,6 @@ import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
import ListResult from "Common/Types/BaseDatabase/ListResult";
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import ObjectID from "Common/Types/ObjectID";
import ServiceElement from "../Service/ServiceElement";
@@ -26,6 +25,17 @@ interface ServiceMetricSummary {
service: Service;
metricCount: number;
metricNames: Array<string>;
metricUnits: Array<string>;
metricDescriptions: Array<string>;
hasSystemMetrics: boolean;
hasAppMetrics: boolean;
}
interface MetricCategory {
name: string;
count: number;
color: string;
bgColor: string;
}
const MetricsDashboard: FunctionComponent = (): ReactElement => {
@@ -33,59 +43,143 @@ const MetricsDashboard: FunctionComponent = (): ReactElement => {
Array<ServiceMetricSummary>
>([]);
const [totalMetricCount, setTotalMetricCount] = useState<number>(0);
const [metricCategories, setMetricCategories] = useState<
Array<MetricCategory>
>([]);
const [servicesWithNoMetrics, setServicesWithNoMetrics] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const categorizeMetric: (name: string) => string = (
name: string,
): string => {
const lower: string = name.toLowerCase();
if (
lower.includes("cpu") ||
lower.includes("memory") ||
lower.includes("disk") ||
lower.includes("network") ||
lower.includes("system") ||
lower.includes("process") ||
lower.includes("runtime") ||
lower.includes("gc")
) {
return "System";
}
if (
lower.includes("http") ||
lower.includes("request") ||
lower.includes("response") ||
lower.includes("latency") ||
lower.includes("duration") ||
lower.includes("rpc")
) {
return "Request";
}
if (
lower.includes("db") ||
lower.includes("database") ||
lower.includes("query") ||
lower.includes("connection") ||
lower.includes("pool")
) {
return "Database";
}
if (
lower.includes("queue") ||
lower.includes("message") ||
lower.includes("kafka") ||
lower.includes("rabbit") ||
lower.includes("publish") ||
lower.includes("consume")
) {
return "Messaging";
}
return "Custom";
};
const loadDashboard: () => Promise<void> = async (): Promise<void> => {
try {
setIsLoading(true);
setError("");
// Load services
const servicesResult: ListResult<Service> = await ModelAPI.getList({
modelType: Service,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
select: {
serviceColor: true,
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
});
// Load services and metrics in parallel
const [servicesResult, metricsResult] = await Promise.all([
ModelAPI.getList({
modelType: Service,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
select: {
serviceColor: true,
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
}),
ModelAPI.getList({
modelType: MetricType,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
select: {
name: true,
unit: true,
description: true,
services: {
_id: true,
name: true,
serviceColor: true,
} as any,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
}),
]);
const services: Array<Service> = servicesResult.data || [];
// Load all metric types with their services
const metricsResult: ListResult<MetricType> = await ModelAPI.getList({
modelType: MetricType,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
select: {
name: true,
unit: true,
description: true,
services: {
_id: true,
name: true,
serviceColor: true,
} as any,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
});
const metrics: Array<MetricType> = metricsResult.data || [];
setTotalMetricCount(metrics.length);
// Build category counts
const categoryMap: Map<string, number> = new Map();
for (const metric of metrics) {
const cat: string = categorizeMetric(metric.name || "");
categoryMap.set(cat, (categoryMap.get(cat) || 0) + 1);
}
const categoryColors: Record<string, { color: string; bgColor: string }> =
{
System: { color: "text-blue-700", bgColor: "bg-blue-50" },
Request: { color: "text-purple-700", bgColor: "bg-purple-50" },
Database: { color: "text-amber-700", bgColor: "bg-amber-50" },
Messaging: { color: "text-green-700", bgColor: "bg-green-50" },
Custom: { color: "text-gray-700", bgColor: "bg-gray-50" },
};
const categories: Array<MetricCategory> = Array.from(
categoryMap.entries(),
)
.map(([name, count]: [string, number]) => {
return {
name,
count,
color: categoryColors[name]?.color || "text-gray-700",
bgColor: categoryColors[name]?.bgColor || "bg-gray-50",
};
})
.sort((a: MetricCategory, b: MetricCategory) => {
return b.count - a.count;
});
setMetricCategories(categories);
// Build per-service summaries
const summaryMap: Map<string, ServiceMetricSummary> = new Map();
@@ -95,11 +189,16 @@ const MetricsDashboard: FunctionComponent = (): ReactElement => {
service,
metricCount: 0,
metricNames: [],
metricUnits: [],
metricDescriptions: [],
hasSystemMetrics: false,
hasAppMetrics: false,
});
}
for (const metric of metrics) {
const metricServices: Array<Service> = metric.services || [];
const cat: string = categorizeMetric(metric.name || "");
for (const metricService of metricServices) {
const serviceId: string =
@@ -108,31 +207,42 @@ const MetricsDashboard: FunctionComponent = (): ReactElement => {
summaryMap.get(serviceId);
if (!summary) {
// Service exists in metric but wasn't in our services list
summary = {
service: metricService,
metricCount: 0,
metricNames: [],
metricUnits: [],
metricDescriptions: [],
hasSystemMetrics: false,
hasAppMetrics: false,
};
summaryMap.set(serviceId, summary);
}
summary.metricCount += 1;
if (cat === "System") {
summary.hasSystemMetrics = true;
} else {
summary.hasAppMetrics = true;
}
const metricName: string = metric.name || "";
if (metricName && summary.metricNames.length < 5) {
if (metricName && summary.metricNames.length < 6) {
summary.metricNames.push(metricName);
}
}
}
// Only show services that have metrics
const summariesWithData: Array<ServiceMetricSummary> = Array.from(
summaryMap.values(),
).filter((s: ServiceMetricSummary) => {
return s.metricCount > 0;
});
const noMetricsCount: number = services.length - summariesWithData.length;
setServicesWithNoMetrics(noMetricsCount);
// Sort by metric count descending
summariesWithData.sort(
(a: ServiceMetricSummary, b: ServiceMetricSummary) => {
@@ -169,102 +279,225 @@ const MetricsDashboard: FunctionComponent = (): ReactElement => {
if (serviceSummaries.length === 0) {
return (
<div className="rounded-lg border border-gray-200 bg-white p-12 text-center">
<div className="text-gray-400 text-5xl mb-4">
<div className="rounded-xl border border-gray-200 bg-white p-16 text-center">
<div className="mx-auto w-16 h-16 rounded-full bg-indigo-50 flex items-center justify-center mb-5">
<svg
className="mx-auto h-16 w-16 text-indigo-200"
className="h-8 w-8 text-indigo-400"
fill="none"
viewBox="0 0 48 48"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M6 38 L6 20 L12 20 L12 38"
fill="currentColor"
opacity={0.4}
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M16 38 L16 14 L22 14 L22 38"
fill="currentColor"
opacity={0.6}
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M26 38 L26 24 L32 24 L32 38"
fill="currentColor"
opacity={0.5}
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M36 38 L36 10 L42 10 L42 38"
fill="currentColor"
opacity={0.8}
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M4 38 L44 38"
opacity={0.3}
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
/>
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
No metrics data yet
</h3>
<p className="text-sm text-gray-500 max-w-md mx-auto">
<p className="text-sm text-gray-500 max-w-sm mx-auto leading-relaxed">
Once your services start sending metrics via OpenTelemetry, you{"'"}ll
see a summary of which services are reporting, what metrics they
collect, and more.
see coverage, categories, and per-service breakdowns here.
</p>
</div>
);
}
const maxMetrics: number = Math.max(
...serviceSummaries.map((s: ServiceMetricSummary) => {
return s.metricCount;
}),
);
return (
<Fragment>
{/* Summary Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div className="rounded-lg border border-gray-200 bg-white p-5">
<p className="text-sm text-gray-500">Total Metrics</p>
<p className="text-3xl font-bold text-gray-900 mt-1">
{/* Hero Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Total Metrics</p>
<div className="h-9 w-9 rounded-lg bg-indigo-50 flex items-center justify-center">
<svg
className="h-4.5 w-4.5 text-indigo-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{totalMetricCount}
</p>
<p className="text-xs text-gray-400 mt-1">unique metric types</p>
</div>
<div className="rounded-lg border border-gray-200 bg-white p-5">
<p className="text-sm text-gray-500">Services Reporting</p>
<p className="text-3xl font-bold text-gray-900 mt-1">
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">
Services Reporting
</p>
<div className="h-9 w-9 rounded-lg bg-green-50 flex items-center justify-center">
<svg
className="h-4.5 w-4.5 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{serviceSummaries.length}
</p>
<p className="text-xs text-gray-400 mt-1">actively sending data</p>
</div>
<div className="rounded-lg border border-gray-200 bg-white p-5">
<p className="text-sm text-gray-500">Avg Metrics per Service</p>
<p className="text-3xl font-bold text-gray-900 mt-1">
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">
Avg per Service
</p>
<div className="h-9 w-9 rounded-lg bg-blue-50 flex items-center justify-center">
<svg
className="h-4.5 w-4.5 text-blue-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.5 14.25v2.25m3-4.5v4.5m3-6.75v6.75m3-9v9M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{serviceSummaries.length > 0
? Math.round(totalMetricCount / serviceSummaries.length)
: 0}
</p>
<p className="text-xs text-gray-400 mt-1">metrics per service</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">
No Metrics
</p>
<div
className={`h-9 w-9 rounded-lg flex items-center justify-center ${servicesWithNoMetrics > 0 ? "bg-amber-50" : "bg-gray-50"}`}
>
<svg
className={`h-4.5 w-4.5 ${servicesWithNoMetrics > 0 ? "text-amber-600" : "text-gray-400"}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
/>
</svg>
</div>
</div>
<p
className={`text-3xl font-bold mt-2 ${servicesWithNoMetrics > 0 ? "text-amber-600" : "text-gray-900"}`}
>
{servicesWithNoMetrics}
</p>
<p className="text-xs text-gray-400 mt-1">
{servicesWithNoMetrics > 0
? "services not instrumented"
: "all services covered"}
</p>
</div>
</div>
{/* Metric Categories */}
{metricCategories.length > 0 && (
<div className="rounded-xl border border-gray-200 bg-white p-5 mb-6">
<h3 className="text-sm font-semibold text-gray-900 mb-3">
Metric Categories
</h3>
<div className="flex flex-wrap gap-3">
{metricCategories.map((cat: MetricCategory) => {
const pct: number =
totalMetricCount > 0
? Math.round((cat.count / totalMetricCount) * 100)
: 0;
return (
<div
key={cat.name}
className={`flex items-center gap-2.5 px-3.5 py-2 rounded-lg ${cat.bgColor}`}
>
<span className={`text-sm font-semibold ${cat.color}`}>
{cat.count}
</span>
<span className={`text-sm ${cat.color}`}>{cat.name}</span>
<span
className={`text-xs ${cat.color} opacity-60`}
>
{pct}%
</span>
</div>
);
})}
</div>
{/* Category distribution bar */}
<div className="flex h-2 rounded-full overflow-hidden mt-3">
{metricCategories.map((cat: MetricCategory) => {
const pct: number =
totalMetricCount > 0
? (cat.count / totalMetricCount) * 100
: 0;
const barColorMap: Record<string, string> = {
System: "bg-blue-400",
Request: "bg-purple-400",
Database: "bg-amber-400",
Messaging: "bg-green-400",
Custom: "bg-gray-300",
};
return (
<div
key={cat.name}
className={`${barColorMap[cat.name] || "bg-gray-300"}`}
style={{ width: `${Math.max(pct, 1)}%` }}
/>
);
})}
</div>
</div>
)}
{/* Service Cards */}
<div className="mb-8">
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">
<h3 className="text-base font-semibold text-gray-900">
Services Reporting Metrics
</h3>
<p className="text-sm text-gray-500 mt-1">
Each service and the metrics it collects
<p className="text-sm text-gray-500 mt-0.5">
Coverage and instrumentation per service
</p>
</div>
<AppLink
@@ -278,67 +511,84 @@ const MetricsDashboard: FunctionComponent = (): ReactElement => {
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{serviceSummaries.map((summary: ServiceMetricSummary) => {
const coverage: number =
maxMetrics > 0
? Math.round((summary.metricCount / maxMetrics) * 100)
: 0;
return (
<div
<AppLink
key={
summary.service.id?.toString() ||
summary.service._id?.toString()
}
className="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow"
className="block"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_VIEW_METRICS] as Route,
{
modelId: new ObjectID(
(summary.service._id as string) ||
summary.service.id?.toString() ||
"",
),
},
)}
>
<div className="flex items-start justify-between mb-3">
<ServiceElement service={summary.service} />
<span className="text-xs bg-green-100 text-green-800 px-2 py-0.5 rounded-full font-medium">
Active
</span>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5 hover:border-indigo-200 hover:shadow-md transition-all duration-200">
<div className="flex items-start justify-between mb-4">
<ServiceElement service={summary.service} />
<div className="flex items-center gap-1.5">
{summary.hasSystemMetrics && (
<span className="text-xs bg-blue-50 text-blue-700 px-2 py-0.5 rounded-full font-medium">
System
</span>
)}
{summary.hasAppMetrics && (
<span className="text-xs bg-purple-50 text-purple-700 px-2 py-0.5 rounded-full font-medium">
App
</span>
)}
</div>
</div>
<div className="mb-3">
<p className="text-xs text-gray-500">Metrics Collected</p>
<p className="text-lg font-semibold text-gray-900">
{summary.metricCount}
</p>
</div>
{/* Metric count with relative bar */}
<div className="mb-4">
<div className="flex items-end justify-between mb-1.5">
<span className="text-2xl font-bold text-gray-900">
{summary.metricCount}
</span>
<span className="text-xs text-gray-400 mb-1">
metrics
</span>
</div>
<div className="w-full h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full bg-indigo-400 transition-all duration-500"
style={{ width: `${Math.max(coverage, 3)}%` }}
/>
</div>
</div>
<div>
<p className="text-xs text-gray-500 mb-1.5">Sample Metrics</p>
{/* Metric name tags */}
<div className="flex flex-wrap gap-1.5">
{summary.metricNames.map((name: string) => {
return (
<span
key={name}
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700"
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-50 text-gray-600 border border-gray-100"
>
{name}
</span>
);
})}
{summary.metricCount > summary.metricNames.length && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-50 text-gray-400">
+{summary.metricCount - summary.metricNames.length} more
</span>
)}
</div>
</div>
<div className="mt-4 pt-3 border-t border-gray-100">
<AppLink
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_VIEW_METRICS] as Route,
{
modelId: new ObjectID(
(summary.service._id as string) ||
summary.service.id?.toString() ||
"",
),
},
)}
>
View service metrics
</AppLink>
</div>
</div>
</AppLink>
);
})}
</div>

View File

@@ -17,12 +17,9 @@ import { JSONObject } from "Common/Types/JSON";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
import ListResult from "Common/Types/BaseDatabase/ListResult";
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import Profile from "Common/Models/AnalyticsModels/Profile";
import AnalyticsModelAPI, {
ListResult as AnalyticsListResult,
} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
import AnalyticsModelAPI from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import OneUptimeDate from "Common/Types/Date";
import ObjectID from "Common/Types/ObjectID";
@@ -38,6 +35,7 @@ interface ServiceProfileSummary {
profileCount: number;
latestProfileTime: Date | null;
profileTypes: Array<string>;
totalSamples: number;
}
interface FunctionHotspot {
@@ -49,11 +47,23 @@ interface FunctionHotspot {
frameType: string;
}
interface ProfileTypeStats {
type: string;
count: number;
displayName: string;
badgeColor: string;
}
const ProfilesDashboard: FunctionComponent = (): ReactElement => {
const [serviceSummaries, setServiceSummaries] = useState<
Array<ServiceProfileSummary>
>([]);
const [hotspots, setHotspots] = useState<Array<FunctionHotspot>>([]);
const [profileTypeStats, setProfileTypeStats] = useState<
Array<ProfileTypeStats>
>([]);
const [totalProfileCount, setTotalProfileCount] = useState<number>(0);
const [totalSampleCount, setTotalSampleCount] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
@@ -65,28 +75,23 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => {
const now: Date = OneUptimeDate.getCurrentDate();
const oneHourAgo: Date = OneUptimeDate.addRemoveHours(now, -1);
// Load services
const servicesResult: ListResult<Service> = await ModelAPI.getList({
modelType: Service,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
select: {
serviceColor: true,
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
});
const services: Array<Service> = servicesResult.data || [];
// Load recent profiles (last 1 hour) to build per-service summaries
const profilesResult: AnalyticsListResult<Profile> =
await AnalyticsModelAPI.getList({
const [servicesResult, profilesResult] = await Promise.all([
ModelAPI.getList({
modelType: Service,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
select: {
serviceColor: true,
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
}),
AnalyticsModelAPI.getList({
modelType: Profile,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
@@ -103,12 +108,18 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => {
sort: {
startTime: SortOrder.Descending,
},
});
}),
]);
const services: Array<Service> = servicesResult.data || [];
const profiles: Array<Profile> = profilesResult.data || [];
setTotalProfileCount(profiles.length);
// Build per-service summaries
const summaryMap: Map<string, ServiceProfileSummary> = new Map();
const typeCountMap: Map<string, number> = new Map();
let totalSamples: number = 0;
for (const service of services) {
const serviceId: string = service.id?.toString() || "";
@@ -117,6 +128,7 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => {
profileCount: 0,
latestProfileTime: null,
profileTypes: [],
totalSamples: 0,
});
}
@@ -131,6 +143,10 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => {
summary.profileCount += 1;
const samples: number = (profile.sampleCount as number) || 0;
summary.totalSamples += samples;
totalSamples += samples;
const profileTime: Date | undefined = profile.startTime
? new Date(profile.startTime)
: undefined;
@@ -144,29 +160,50 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => {
}
const profileType: string = profile.profileType || "";
if (profileType && !summary.profileTypes.includes(profileType)) {
summary.profileTypes.push(profileType);
}
// Track global type stats
typeCountMap.set(
profileType,
(typeCountMap.get(profileType) || 0) + 1,
);
}
setTotalSampleCount(totalSamples);
// Build profile type stats
const typeStats: Array<ProfileTypeStats> = Array.from(
typeCountMap.entries(),
)
.map(([type, count]: [string, number]) => {
return {
type,
count,
displayName: ProfileUtil.getProfileTypeDisplayName(type),
badgeColor: ProfileUtil.getProfileTypeBadgeColor(type),
};
})
.sort(
(a: ProfileTypeStats, b: ProfileTypeStats) => b.count - a.count,
);
setProfileTypeStats(typeStats);
// Only show services that have profiles
const summariesWithData: Array<ServiceProfileSummary> = Array.from(
summaryMap.values(),
).filter((s: ServiceProfileSummary) => {
return s.profileCount > 0;
});
).filter((s: ServiceProfileSummary) => s.profileCount > 0);
// Sort by profile count descending
summariesWithData.sort(
(a: ServiceProfileSummary, b: ServiceProfileSummary) => {
return b.profileCount - a.profileCount;
},
(a: ServiceProfileSummary, b: ServiceProfileSummary) =>
b.profileCount - a.profileCount,
);
setServiceSummaries(summariesWithData);
// Load top hotspots (function list) across all services
// Load top hotspots
try {
const hotspotsResponse: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.post({
@@ -193,7 +230,6 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => {
] || []) as unknown as Array<FunctionHotspot>;
setHotspots(functions);
} catch {
// Hotspots are optional - don't fail the whole page
setHotspots([]);
}
} catch (err) {
@@ -224,44 +260,184 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => {
if (serviceSummaries.length === 0) {
return (
<div className="rounded-lg border border-gray-200 bg-white p-12 text-center">
<div className="text-gray-400 text-5xl mb-4">
<div className="rounded-xl border border-gray-200 bg-white p-16 text-center">
<div className="mx-auto w-16 h-16 rounded-full bg-indigo-50 flex items-center justify-center mb-5">
<svg
className="mx-auto h-12 w-12"
className="h-8 w-8 text-indigo-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5m.75-9l3-3 2.148 2.148A12.061 12.061 0 0116.5 7.605"
/>
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">
No performance data yet
<h3 className="text-lg font-semibold text-gray-900 mb-2">
No profiling data yet
</h3>
<p className="text-sm text-gray-500 max-w-md mx-auto">
Once your services start sending profiling data, you{"'"}ll see a
summary of which services are being profiled, their performance
hotspots, and more.
<p className="text-sm text-gray-500 max-w-sm mx-auto leading-relaxed">
Once your services start sending profiling data, you{"'"}ll see
performance hotspots, resource usage patterns, and optimization
opportunities.
</p>
</div>
);
}
const maxProfiles: number = Math.max(
...serviceSummaries.map((s: ServiceProfileSummary) => s.profileCount),
);
return (
<Fragment>
{/* Hero Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Profiles</p>
<div className="h-9 w-9 rounded-lg bg-indigo-50 flex items-center justify-center">
<svg
className="h-4.5 w-4.5 text-indigo-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{totalProfileCount.toLocaleString()}
</p>
<p className="text-xs text-gray-400 mt-1">last hour</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Services</p>
<div className="h-9 w-9 rounded-lg bg-green-50 flex items-center justify-center">
<svg
className="h-4.5 w-4.5 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 7.5l-2.25-1.313M21 7.5v2.25m0-2.25l-2.25 1.313M3 7.5l2.25-1.313M3 7.5l2.25 1.313M3 7.5v2.25m9 3l2.25-1.313M12 12.75l-2.25-1.313M12 12.75V15m0 6.75l2.25-1.313M12 21.75V19.5m0 2.25l-2.25-1.313m0-16.875L12 2.25l2.25 1.313M21 14.25v2.25l-2.25 1.313m-13.5 0L3 16.5v-2.25"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{serviceSummaries.length}
</p>
<p className="text-xs text-gray-400 mt-1">being profiled</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Samples</p>
<div className="h-9 w-9 rounded-lg bg-blue-50 flex items-center justify-center">
<svg
className="h-4.5 w-4.5 text-blue-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.5 14.25v2.25m3-4.5v4.5m3-6.75v6.75m3-9v9M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{totalSampleCount >= 1_000_000
? `${(totalSampleCount / 1_000_000).toFixed(1)}M`
: totalSampleCount >= 1_000
? `${(totalSampleCount / 1_000).toFixed(1)}K`
: totalSampleCount.toLocaleString()}
</p>
<p className="text-xs text-gray-400 mt-1">total samples</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-500">Hotspots</p>
<div
className={`h-9 w-9 rounded-lg flex items-center justify-center ${hotspots.length > 0 ? "bg-orange-50" : "bg-gray-50"}`}
>
<svg
className={`h-4.5 w-4.5 ${hotspots.length > 0 ? "text-orange-600" : "text-gray-400"}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 6.51 6.51 0 009 4.572c.163.07.322.148.476.232M12 18.75a6.743 6.743 0 002.14-1.234M12 18.75a6.72 6.72 0 01-2.14-1.234M12 18.75V21m-4.773-4.227l-1.591 1.591M5.636 5.636L4.045 4.045m0 15.91l1.591-1.591M18.364 5.636l1.591-1.591M21 12h-2.25M4.5 12H2.25"
/>
</svg>
</div>
</div>
<p className="text-3xl font-bold text-gray-900 mt-2">
{hotspots.length}
</p>
<p className="text-xs text-gray-400 mt-1">functions identified</p>
</div>
</div>
{/* Profile Type Distribution */}
{profileTypeStats.length > 0 && (
<div className="rounded-xl border border-gray-200 bg-white p-5 mb-6">
<h3 className="text-sm font-semibold text-gray-900 mb-3">
Profile Types Collected
</h3>
<div className="flex flex-wrap gap-3">
{profileTypeStats.map((stat: ProfileTypeStats) => {
const pct: number =
totalProfileCount > 0
? Math.round((stat.count / totalProfileCount) * 100)
: 0;
return (
<div
key={stat.type}
className={`flex items-center gap-2.5 px-3.5 py-2 rounded-lg ${stat.badgeColor}`}
>
<span className="text-sm font-semibold">{stat.count}</span>
<span className="text-sm">{stat.displayName}</span>
<span className="text-xs opacity-60">{pct}%</span>
</div>
);
})}
</div>
</div>
)}
{/* Service Cards */}
<div className="mb-8">
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">
<h3 className="text-base font-semibold text-gray-900">
Services Being Profiled
</h3>
<p className="text-sm text-gray-500 mt-1">
<p className="text-sm text-gray-500 mt-0.5">
Performance data collected in the last hour
</p>
</div>
@@ -276,42 +452,58 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => {
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{serviceSummaries.map((summary: ServiceProfileSummary) => {
const coverage: number =
maxProfiles > 0
? Math.round((summary.profileCount / maxProfiles) * 100)
: 0;
return (
<div
<AppLink
key={summary.service.id?.toString()}
className="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow"
className="block"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_VIEW_PROFILES] as Route,
{
modelId: new ObjectID(summary.service._id as string),
},
)}
>
<div className="flex items-start justify-between mb-3">
<ServiceElement service={summary.service} />
<span className="text-xs bg-green-100 text-green-800 px-2 py-0.5 rounded-full font-medium">
Active
</span>
</div>
<div className="grid grid-cols-2 gap-3 mb-3">
<div>
<p className="text-xs text-gray-500">Profiles</p>
<p className="text-lg font-semibold text-gray-900">
{summary.profileCount}
</p>
<div className="rounded-xl border border-gray-200 bg-white p-5 hover:border-indigo-200 hover:shadow-md transition-all duration-200">
<div className="flex items-start justify-between mb-4">
<ServiceElement service={summary.service} />
<span className="text-xs bg-green-50 text-green-700 px-2 py-0.5 rounded-full font-medium">
Active
</span>
</div>
<div>
<p className="text-xs text-gray-500">Last Captured</p>
<p className="text-sm text-gray-700">
{summary.latestProfileTime
? OneUptimeDate.getDateAsLocalFormattedString(
summary.latestProfileTime,
true,
)
: "-"}
</p>
</div>
</div>
<div>
<p className="text-xs text-gray-500 mb-1.5">
Profile Types Collected
</p>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<p className="text-xs text-gray-500 mb-0.5">Profiles</p>
<p className="text-xl font-bold text-gray-900">
{summary.profileCount}
</p>
</div>
<div>
<p className="text-xs text-gray-500 mb-0.5">Samples</p>
<p className="text-xl font-bold text-gray-900">
{summary.totalSamples >= 1_000
? `${(summary.totalSamples / 1_000).toFixed(1)}K`
: summary.totalSamples.toLocaleString()}
</p>
</div>
</div>
{/* Profile volume bar */}
<div className="mb-3">
<div className="w-full h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full bg-indigo-400 transition-all duration-500"
style={{ width: `${Math.max(coverage, 3)}%` }}
/>
</div>
</div>
{/* Profile type badges */}
<div className="flex flex-wrap gap-1.5">
{summary.profileTypes.map((profileType: string) => {
const badgeColor: string =
@@ -319,29 +511,22 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => {
return (
<span
key={profileType}
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${badgeColor}`}
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${badgeColor}`}
>
{ProfileUtil.getProfileTypeDisplayName(profileType)}
</span>
);
})}
</div>
</div>
<div className="mt-4 pt-3 border-t border-gray-100">
<AppLink
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_VIEW_PROFILES] as Route,
{
modelId: new ObjectID(summary.service._id as string),
},
)}
>
View service profiles
</AppLink>
{summary.latestProfileTime && (
<p className="text-xs text-gray-400 mt-3">
Last captured{" "}
{OneUptimeDate.fromNow(summary.latestProfileTime)}
</p>
)}
</div>
</div>
</AppLink>
);
})}
</div>
@@ -350,70 +535,80 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => {
{/* Top Hotspots */}
{hotspots.length > 0 && (
<div>
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-900">
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 rounded-full bg-orange-500" />
<h3 className="text-base font-semibold text-gray-900">
Top Performance Hotspots
</h3>
<p className="text-sm text-gray-500 mt-1">
Functions using the most resources across all services in the last
hour
</p>
</div>
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
<table className="w-full text-sm text-left">
<thead className="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
<tr>
<th className="px-5 py-3 font-medium">#</th>
<th className="px-5 py-3 font-medium">Function</th>
<th className="px-5 py-3 font-medium">Source File</th>
<th className="px-5 py-3 text-right font-medium">Own Time</th>
<th className="px-5 py-3 text-right font-medium">
Total Time
</th>
<th className="px-5 py-3 text-right font-medium">
Occurrences
</th>
</tr>
</thead>
<tbody>
{hotspots.map((fn: FunctionHotspot, index: number) => {
const maxSelf: number = hotspots[0]?.selfValue || 1;
const barWidth: number = (fn.selfValue / maxSelf) * 100;
<p className="text-sm text-gray-500 mb-4">
Functions consuming the most resources across all services
</p>
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-50">
{hotspots.map((fn: FunctionHotspot, index: number) => {
const maxSelf: number = hotspots[0]?.selfValue || 1;
const barWidth: number = (fn.selfValue / maxSelf) * 100;
return (
<tr
key={`${fn.functionName}-${fn.fileName}-${index}`}
className="border-t border-gray-100 hover:bg-gray-50"
>
<td className="px-5 py-3 text-gray-400 font-mono text-xs">
{index + 1}
</td>
<td className="px-5 py-3">
<div className="font-mono text-xs text-gray-900 truncate max-w-xs">
{fn.functionName}
return (
<div
key={`${fn.functionName}-${fn.fileName}-${index}`}
className="px-5 py-3.5 hover:bg-gray-50 transition-colors"
>
<div className="flex items-start justify-between mb-1.5">
<div className="min-w-0 flex-1 mr-4">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400 font-mono w-5 flex-shrink-0">
#{index + 1}
</span>
<p className="font-mono text-sm text-gray-900 truncate">
{fn.functionName}
</p>
{fn.frameType && (
<span className="flex-shrink-0 text-xs px-1.5 py-0.5 rounded font-medium bg-gray-100 text-gray-600">
{fn.frameType}
</span>
)}
</div>
{fn.fileName && (
<p className="text-xs text-gray-400 mt-0.5 ml-7 truncate">
{fn.fileName}
</p>
)}
</div>
<div className="flex items-center gap-5 flex-shrink-0">
<div className="text-right">
<p className="text-sm font-bold font-mono text-gray-900">
{fn.selfValue.toLocaleString()}
</p>
<p className="text-xs text-gray-400">own time</p>
</div>
<div className="text-right">
<p className="text-sm font-mono text-gray-700">
{fn.totalValue.toLocaleString()}
</p>
<p className="text-xs text-gray-400">total</p>
</div>
<div className="text-right">
<p className="text-sm font-mono text-gray-700">
{fn.sampleCount.toLocaleString()}
</p>
<p className="text-xs text-gray-400">samples</p>
</div>
</div>
</div>
<div className="ml-7">
<div className="w-full h-1 bg-gray-100 rounded-full overflow-hidden">
<div
className="mt-1 h-1 rounded-full bg-orange-400"
className="h-full rounded-full bg-orange-400"
style={{ width: `${barWidth}%` }}
/>
</td>
<td className="px-5 py-3 text-gray-500 text-xs truncate max-w-xs">
{fn.fileName || "-"}
</td>
<td className="px-5 py-3 text-right font-mono text-xs text-gray-900">
{fn.selfValue.toLocaleString()}
</td>
<td className="px-5 py-3 text-right font-mono text-xs text-gray-700">
{fn.totalValue.toLocaleString()}
</td>
<td className="px-5 py-3 text-right font-mono text-xs text-gray-700">
{fn.sampleCount.toLocaleString()}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
)}

View File

@@ -12,12 +12,9 @@ import API from "Common/Utils/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
import ListResult from "Common/Types/BaseDatabase/ListResult";
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import Span, { SpanStatus } from "Common/Models/AnalyticsModels/Span";
import AnalyticsModelAPI, {
ListResult as AnalyticsListResult,
} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
import AnalyticsModelAPI from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import OneUptimeDate from "Common/Types/Date";
import ObjectID from "Common/Types/ObjectID";
@@ -33,6 +30,9 @@ interface ServiceTraceSummary {
totalTraces: number;
errorTraces: number;
latestTraceTime: Date | null;
p50Nanos: number;
p95Nanos: number;
durations: Array<number>;
}
interface RecentTrace {
@@ -44,6 +44,33 @@ interface RecentTrace {
durationNano: number;
}
const formatDuration: (nanos: number) => string = (nanos: number): string => {
if (nanos >= 1_000_000_000) {
return `${(nanos / 1_000_000_000).toFixed(2)}s`;
}
if (nanos >= 1_000_000) {
return `${(nanos / 1_000_000).toFixed(1)}ms`;
}
if (nanos >= 1_000) {
return `${(nanos / 1_000).toFixed(0)}us`;
}
return `${nanos}ns`;
};
const getPercentile: (arr: Array<number>, p: number) => number = (
arr: Array<number>,
p: number,
): number => {
if (arr.length === 0) {
return 0;
}
const sorted: Array<number> = [...arr].sort(
(a: number, b: number) => a - b,
);
const idx: number = Math.ceil((p / 100) * sorted.length) - 1;
return sorted[Math.max(0, idx)] || 0;
};
const TracesDashboard: FunctionComponent = (): ReactElement => {
const [serviceSummaries, setServiceSummaries] = useState<
Array<ServiceTraceSummary>
@@ -55,22 +82,14 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
[],
);
const [services, setServices] = useState<Array<Service>>([]);
const [totalRequests, setTotalRequests] = useState<number>(0);
const [totalErrors, setTotalErrors] = useState<number>(0);
const [globalP50, setGlobalP50] = useState<number>(0);
const [globalP95, setGlobalP95] = useState<number>(0);
const [globalP99, setGlobalP99] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const formatDuration: (nanos: number) => string = (nanos: number): string => {
if (nanos >= 1_000_000_000) {
return `${(nanos / 1_000_000_000).toFixed(2)}s`;
}
if (nanos >= 1_000_000) {
return `${(nanos / 1_000_000).toFixed(1)}ms`;
}
if (nanos >= 1_000) {
return `${(nanos / 1_000).toFixed(0)}us`;
}
return `${nanos}ns`;
};
const loadDashboard: () => Promise<void> = async (): Promise<void> => {
try {
setIsLoading(true);
@@ -79,29 +98,23 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
const now: Date = OneUptimeDate.getCurrentDate();
const oneHourAgo: Date = OneUptimeDate.addRemoveHours(now, -1);
// Load services
const servicesResult: ListResult<Service> = await ModelAPI.getList({
modelType: Service,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
select: {
serviceColor: true,
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
});
const loadedServices: Array<Service> = servicesResult.data || [];
setServices(loadedServices);
// Load recent spans (last 1 hour) to build per-service summaries
const spansResult: AnalyticsListResult<Span> =
await AnalyticsModelAPI.getList({
const [servicesResult, spansResult] = await Promise.all([
ModelAPI.getList({
modelType: Service,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
select: {
serviceColor: true,
name: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
sort: {
name: SortOrder.Ascending,
},
}),
AnalyticsModelAPI.getList({
modelType: Span,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
@@ -122,11 +135,15 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
sort: {
startTime: SortOrder.Descending,
},
});
}),
]);
const loadedServices: Array<Service> = servicesResult.data || [];
setServices(loadedServices);
const allSpans: Array<Span> = spansResult.data || [];
// Build per-service summaries from all spans
// Build per-service summaries
const summaryMap: Map<string, ServiceTraceSummary> = new Map();
for (const service of loadedServices) {
@@ -136,44 +153,52 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
totalTraces: 0,
errorTraces: 0,
latestTraceTime: null,
p50Nanos: 0,
p95Nanos: 0,
durations: [],
});
}
// Track unique traces per service for counting
const serviceTraceIds: Map<string, Set<string>> = new Map();
const serviceErrorTraceIds: Map<string, Set<string>> = new Map();
const errorTraces: Array<RecentTrace> = [];
const allTraces: Array<RecentTrace> = [];
const seenTraceIds: Set<string> = new Set();
const seenErrorTraceIds: Set<string> = new Set();
const allDurations: Array<number> = [];
for (const span of allSpans) {
const serviceId: string = span.serviceId?.toString() || "";
const traceId: string = span.traceId?.toString() || "";
const duration: number = (span.durationUnixNano as number) || 0;
const summary: ServiceTraceSummary | undefined =
summaryMap.get(serviceId);
if (duration > 0) {
allDurations.push(duration);
}
if (summary) {
// Count unique traces per service
if (!serviceTraceIds.has(serviceId)) {
serviceTraceIds.set(serviceId, new Set());
}
if (!serviceErrorTraceIds.has(serviceId)) {
serviceErrorTraceIds.set(serviceId, new Set());
}
const traceSet: Set<string> = serviceTraceIds.get(serviceId)!;
if (!traceSet.has(traceId)) {
traceSet.add(traceId);
summary.totalTraces += 1;
}
if (span.statusCode === SpanStatus.Error) {
const errorSet: Set<string> = serviceErrorTraceIds.get(serviceId)!;
if (duration > 0) {
summary.durations.push(duration);
}
if (span.statusCode === SpanStatus.Error) {
const errorSet: Set<string> =
serviceErrorTraceIds.get(serviceId)!;
if (!errorSet.has(traceId)) {
errorSet.add(traceId);
summary.errorTraces += 1;
@@ -183,7 +208,6 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
const spanTime: Date | undefined = span.startTime
? new Date(span.startTime)
: undefined;
if (
spanTime &&
(!summary.latestTraceTime || spanTime > summary.latestTraceTime)
@@ -192,67 +216,84 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
}
}
/*
* For the recent traces lists, pick the first span per trace
* (which is the most recent since we sort desc)
*/
if (!seenTraceIds.has(traceId) && traceId) {
seenTraceIds.add(traceId);
const traceRecord: RecentTrace = {
traceId: traceId,
allTraces.push({
traceId,
name: span.name?.toString() || "Unknown",
serviceId: serviceId,
serviceId,
startTime: span.startTime ? new Date(span.startTime) : new Date(),
statusCode: span.statusCode || SpanStatus.Unset,
durationNano: (span.durationUnixNano as number) || 0,
};
allTraces.push(traceRecord);
durationNano: duration,
});
}
// Collect error spans, deduped by trace
if (
span.statusCode === SpanStatus.Error &&
traceId &&
!seenErrorTraceIds.has(traceId)
) {
seenErrorTraceIds.add(traceId);
errorTraces.push({
traceId: traceId,
traceId,
name: span.name?.toString() || "Unknown",
serviceId: serviceId,
serviceId,
startTime: span.startTime ? new Date(span.startTime) : new Date(),
statusCode: span.statusCode,
durationNano: (span.durationUnixNano as number) || 0,
durationNano: duration,
});
}
}
// Only show services that have traces
// Compute global percentiles
setGlobalP50(getPercentile(allDurations, 50));
setGlobalP95(getPercentile(allDurations, 95));
setGlobalP99(getPercentile(allDurations, 99));
// Compute per-service percentiles and filter
const summariesWithData: Array<ServiceTraceSummary> = Array.from(
summaryMap.values(),
).filter((s: ServiceTraceSummary) => {
return s.totalTraces > 0;
});
)
.filter((s: ServiceTraceSummary) => s.totalTraces > 0)
.map((s: ServiceTraceSummary) => {
return {
...s,
p50Nanos: getPercentile(s.durations, 50),
p95Nanos: getPercentile(s.durations, 95),
};
});
// Sort by total traces descending
// Sort: highest error rate first, then by total traces
summariesWithData.sort(
(a: ServiceTraceSummary, b: ServiceTraceSummary) => {
const aErrorRate: number =
a.totalTraces > 0 ? a.errorTraces / a.totalTraces : 0;
const bErrorRate: number =
b.totalTraces > 0 ? b.errorTraces / b.totalTraces : 0;
if (bErrorRate !== aErrorRate) {
return bErrorRate - aErrorRate;
}
return b.totalTraces - a.totalTraces;
},
);
setServiceSummaries(summariesWithData);
setRecentErrorTraces(errorTraces.slice(0, 10));
let totalReqs: number = 0;
let totalErrs: number = 0;
for (const s of summariesWithData) {
totalReqs += s.totalTraces;
totalErrs += s.errorTraces;
}
setTotalRequests(totalReqs);
setTotalErrors(totalErrs);
setServiceSummaries(summariesWithData);
setRecentErrorTraces(errorTraces.slice(0, 8));
// Get slowest traces
const slowTraces: Array<RecentTrace> = [...allTraces]
.sort((a: RecentTrace, b: RecentTrace) => {
return b.durationNano - a.durationNano;
})
.slice(0, 10);
.sort(
(a: RecentTrace, b: RecentTrace) => b.durationNano - a.durationNano,
)
.slice(0, 8);
setRecentSlowTraces(slowTraces);
} catch (err) {
setError(API.getFriendlyErrorMessage(err as Error));
@@ -268,9 +309,9 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
const getServiceName: (serviceId: string) => string = (
serviceId: string,
): string => {
const service: Service | undefined = services.find((s: Service) => {
return s.id?.toString() === serviceId;
});
const service: Service | undefined = services.find(
(s: Service) => s.id?.toString() === serviceId,
);
return service?.name?.toString() || "Unknown";
};
@@ -291,73 +332,104 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
if (serviceSummaries.length === 0) {
return (
<div className="rounded-lg border border-gray-200 bg-white p-12 text-center">
<div className="mb-4">
<div className="rounded-xl border border-gray-200 bg-white p-16 text-center">
<div className="mx-auto w-16 h-16 rounded-full bg-indigo-50 flex items-center justify-center mb-5">
<svg
className="mx-auto h-16 w-16 text-indigo-200"
className="h-8 w-8 text-indigo-400"
fill="none"
viewBox="0 0 48 48"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
{/* Three horizontal bars representing a waterfall/trace timeline */}
<rect
x="4"
y="10"
width="28"
height="5"
rx="2.5"
strokeWidth={1.5}
fill="currentColor"
opacity={0.5}
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
/>
<rect
x="12"
y="20"
width="20"
height="5"
rx="2.5"
strokeWidth={1.5}
fill="currentColor"
opacity={0.7}
/>
<rect
x="20"
y="30"
width="24"
height="5"
rx="2.5"
strokeWidth={1.5}
fill="currentColor"
opacity={0.9}
/>
{/* Connecting lines */}
<path d="M18 15 L16 20" strokeWidth={1.5} opacity={0.4} />
<path d="M22 25 L24 30" strokeWidth={1.5} opacity={0.4} />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
No trace data yet
</h3>
<p className="text-sm text-gray-500 max-w-md mx-auto">
<p className="text-sm text-gray-500 max-w-sm mx-auto leading-relaxed">
Once your services start sending distributed tracing data, you{"'"}ll
see a summary of requests flowing through your system, error rates,
and slow operations.
see request rates, error rates, latency percentiles, and more.
</p>
</div>
);
}
const overallErrorRate: number =
totalRequests > 0 ? (totalErrors / totalRequests) * 100 : 0;
return (
<Fragment>
{/* Service Cards */}
<div className="mb-8">
{/* Hero Stats */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div className="rounded-xl border border-gray-200 bg-white p-5">
<p className="text-sm font-medium text-gray-500">Requests</p>
<p className="text-3xl font-bold text-gray-900 mt-1">
{totalRequests.toLocaleString()}
</p>
<p className="text-xs text-gray-400 mt-1">last hour</p>
</div>
<div
className={`rounded-xl border p-5 ${overallErrorRate > 5 ? "border-red-200 bg-red-50" : overallErrorRate > 1 ? "border-amber-200 bg-amber-50" : "border-gray-200 bg-white"}`}
>
<p className="text-sm font-medium text-gray-500">Error Rate</p>
<p
className={`text-3xl font-bold mt-1 ${overallErrorRate > 5 ? "text-red-600" : overallErrorRate > 1 ? "text-amber-600" : "text-green-600"}`}
>
{overallErrorRate.toFixed(1)}%
</p>
<p className="text-xs text-gray-400 mt-1">
{totalErrors.toLocaleString()} errors
</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<p className="text-sm font-medium text-gray-500">P50 Latency</p>
<p className="text-3xl font-bold text-gray-900 mt-1">
{formatDuration(globalP50)}
</p>
<p className="text-xs text-gray-400 mt-1">median</p>
</div>
<div
className={`rounded-xl border p-5 ${globalP95 > 1_000_000_000 ? "border-amber-200 bg-amber-50" : "border-gray-200 bg-white"}`}
>
<p className="text-sm font-medium text-gray-500">P95 Latency</p>
<p
className={`text-3xl font-bold mt-1 ${globalP95 > 1_000_000_000 ? "text-amber-600" : "text-gray-900"}`}
>
{formatDuration(globalP95)}
</p>
<p className="text-xs text-gray-400 mt-1">95th percentile</p>
</div>
<div
className={`rounded-xl border p-5 ${globalP99 > 2_000_000_000 ? "border-red-200 bg-red-50" : "border-gray-200 bg-white"}`}
>
<p className="text-sm font-medium text-gray-500">P99 Latency</p>
<p
className={`text-3xl font-bold mt-1 ${globalP99 > 2_000_000_000 ? "text-red-600" : "text-gray-900"}`}
>
{formatDuration(globalP99)}
</p>
<p className="text-xs text-gray-400 mt-1">99th percentile</p>
</div>
</div>
{/* Service Health Table */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">
Services Overview
<h3 className="text-base font-semibold text-gray-900">
Service Health
</h3>
<p className="text-sm text-gray-500 mt-1">
Request activity across your services in the last hour
<p className="text-sm text-gray-500 mt-0.5">
Sorted by error rate services needing attention first
</p>
</div>
<AppLink
@@ -369,125 +441,180 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
View all spans
</AppLink>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{serviceSummaries.map((summary: ServiceTraceSummary) => {
const errorRate: number =
summary.totalTraces > 0
? (summary.errorTraces / summary.totalTraces) * 100
: 0;
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<table className="w-full">
<thead>
<tr className="border-b border-gray-100">
<th className="text-left text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
Service
</th>
<th className="text-right text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
Requests
</th>
<th className="text-right text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
Error Rate
</th>
<th className="text-right text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
P50
</th>
<th className="text-right text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
P95
</th>
<th className="text-center text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
Status
</th>
<th className="text-right text-xs font-medium text-gray-500 uppercase tracking-wider px-5 py-3">
&nbsp;
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{serviceSummaries.map((summary: ServiceTraceSummary) => {
const errorRate: number =
summary.totalTraces > 0
? (summary.errorTraces / summary.totalTraces) * 100
: 0;
return (
<div
key={summary.service.id?.toString()}
className="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between mb-3">
<ServiceElement service={summary.service} />
{errorRate > 5 ? (
<span className="text-xs bg-red-100 text-red-800 px-2 py-0.5 rounded-full font-medium">
{errorRate.toFixed(1)}% errors
</span>
) : (
<span className="text-xs bg-green-100 text-green-800 px-2 py-0.5 rounded-full font-medium">
Healthy
</span>
)}
</div>
let healthColor: string = "bg-green-500";
let healthLabel: string = "Healthy";
let healthBg: string = "bg-green-50 text-green-700";
if (errorRate > 10) {
healthColor = "bg-red-500";
healthLabel = "Critical";
healthBg = "bg-red-50 text-red-700";
} else if (errorRate > 5) {
healthColor = "bg-amber-500";
healthLabel = "Degraded";
healthBg = "bg-amber-50 text-amber-700";
} else if (errorRate > 1) {
healthColor = "bg-yellow-400";
healthLabel = "Warning";
healthBg = "bg-yellow-50 text-yellow-700";
}
<div className="grid grid-cols-3 gap-3 mb-3">
<div>
<p className="text-xs text-gray-500">Requests</p>
<p className="text-lg font-semibold text-gray-900">
{summary.totalTraces.toLocaleString()}
</p>
</div>
<div>
<p className="text-xs text-gray-500">Errors</p>
<p
className={`text-lg font-semibold ${summary.errorTraces > 0 ? "text-red-600" : "text-gray-900"}`}
>
{summary.errorTraces.toLocaleString()}
</p>
</div>
<div>
<p className="text-xs text-gray-500">Last Seen</p>
<p className="text-sm text-gray-700">
{summary.latestTraceTime
? OneUptimeDate.getDateAsLocalFormattedString(
summary.latestTraceTime,
true,
)
: "-"}
</p>
</div>
</div>
{/* Error rate bar */}
<div>
<div className="flex items-center justify-between text-xs text-gray-500 mb-1">
<span>Error Rate</span>
<span>{errorRate.toFixed(1)}%</span>
</div>
<div className="w-full h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${errorRate > 5 ? "bg-red-500" : errorRate > 0 ? "bg-yellow-400" : "bg-green-400"}`}
style={{
width: `${Math.max(errorRate, errorRate > 0 ? 2 : 0)}%`,
}}
/>
</div>
</div>
<div className="mt-4 pt-3 border-t border-gray-100">
<AppLink
className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_VIEW_TRACES] as Route,
{
modelId: new ObjectID(summary.service._id as string),
},
)}
return (
<tr
key={summary.service.id?.toString()}
className="hover:bg-gray-50 transition-colors"
>
View service traces
</AppLink>
</div>
</div>
);
})}
<td className="px-5 py-3.5">
<ServiceElement service={summary.service} />
</td>
<td className="px-5 py-3.5 text-right">
<span className="text-sm font-medium text-gray-900">
{summary.totalTraces.toLocaleString()}
</span>
</td>
<td className="px-5 py-3.5 text-right">
<div className="flex items-center justify-end gap-2">
<div className="w-16 h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${errorRate > 10 ? "bg-red-500" : errorRate > 5 ? "bg-amber-400" : errorRate > 0 ? "bg-yellow-400" : "bg-green-400"}`}
style={{
width: `${Math.max(errorRate, errorRate > 0 ? 3 : 0)}%`,
}}
/>
</div>
<span
className={`text-sm font-medium ${errorRate > 5 ? "text-red-600" : "text-gray-900"}`}
>
{errorRate.toFixed(1)}%
</span>
</div>
</td>
<td className="px-5 py-3.5 text-right">
<span className="text-sm font-mono text-gray-700">
{formatDuration(summary.p50Nanos)}
</span>
</td>
<td className="px-5 py-3.5 text-right">
<span className="text-sm font-mono text-gray-700">
{formatDuration(summary.p95Nanos)}
</span>
</td>
<td className="px-5 py-3.5 text-center">
<span
className={`inline-flex items-center gap-1.5 text-xs font-medium px-2.5 py-1 rounded-full ${healthBg}`}
>
<span
className={`w-1.5 h-1.5 rounded-full ${healthColor}`}
/>
{healthLabel}
</span>
</td>
<td className="px-5 py-3.5 text-right">
<AppLink
className="text-xs text-indigo-600 hover:text-indigo-800 font-medium"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_VIEW_TRACES] as Route,
{
modelId: new ObjectID(
summary.service._id as string,
),
},
)}
>
View
</AppLink>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* Two-column layout for errors and slow traces */}
{/* Two-column: Errors + Slow Requests */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Recent Errors */}
<div>
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-900">
Recent Errors
</h3>
<p className="text-sm text-gray-500 mt-1">
Failed requests in the last hour
</p>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-red-500" />
<h3 className="text-base font-semibold text-gray-900">
Recent Errors
</h3>
{recentErrorTraces.length > 0 && (
<span className="text-xs bg-red-50 text-red-700 px-2 py-0.5 rounded-full font-medium">
{recentErrorTraces.length}
</span>
)}
</div>
</div>
{recentErrorTraces.length === 0 ? (
<div className="rounded-lg border border-gray-200 bg-white p-8 text-center">
<p className="text-sm text-gray-500">
<div className="rounded-xl border border-gray-200 bg-white p-10 text-center">
<div className="mx-auto w-10 h-10 rounded-full bg-green-50 flex items-center justify-center mb-3">
<svg
className="h-5 w-5 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<p className="text-sm font-medium text-gray-700">
No errors in the last hour
</p>
<p className="text-xs text-gray-400 mt-1">Looking good!</p>
</div>
) : (
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-100">
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-50">
{recentErrorTraces.map((trace: RecentTrace, index: number) => {
return (
<AppLink
key={`${trace.traceId}-${index}`}
className="block px-4 py-3 hover:bg-gray-50 transition-colors"
className="block px-4 py-3 hover:bg-red-50/30 transition-colors"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.TRACE_VIEW]!,
{
modelId: trace.traceId,
},
{ modelId: trace.traceId },
)}
>
<div className="flex items-center justify-between">
@@ -510,10 +637,7 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
{formatDuration(trace.durationNano)}
</p>
<p className="text-xs text-gray-400">
{OneUptimeDate.getDateAsLocalFormattedString(
trace.startTime,
true,
)}
{OneUptimeDate.fromNow(trace.startTime)}
</p>
</div>
</div>
@@ -525,25 +649,23 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
)}
</div>
{/* Slowest Traces */}
{/* Slowest Requests */}
<div>
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-900">
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 rounded-full bg-amber-500" />
<h3 className="text-base font-semibold text-gray-900">
Slowest Requests
</h3>
<p className="text-sm text-gray-500 mt-1">
Longest running operations in the last hour
</p>
</div>
{recentSlowTraces.length === 0 ? (
<div className="rounded-lg border border-gray-200 bg-white p-8 text-center">
<div className="rounded-xl border border-gray-200 bg-white p-10 text-center">
<p className="text-sm text-gray-500">
No traces found in the last hour
No traces in the last hour
</p>
</div>
) : (
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-100">
<div className="rounded-xl border border-gray-200 bg-white overflow-hidden">
<div className="divide-y divide-gray-50">
{recentSlowTraces.map((trace: RecentTrace, index: number) => {
const maxDuration: number =
recentSlowTraces[0]?.durationNano || 1;
@@ -553,15 +675,13 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
return (
<AppLink
key={`${trace.traceId}-slow-${index}`}
className="block px-4 py-3 hover:bg-gray-50 transition-colors"
className="block px-4 py-3 hover:bg-amber-50/30 transition-colors"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.TRACE_VIEW]!,
{
modelId: trace.traceId,
},
{ modelId: trace.traceId },
)}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center justify-between mb-1.5">
<div className="flex items-center space-x-3 min-w-0">
<SpanStatusElement
spanStatusCode={trace.statusCode}

View File

@@ -1,5 +1,5 @@
import { BASE_URL } from "../../Config";
import { Page, expect, test } from "@playwright/test";
import { Page, Response, expect, test } from "@playwright/test";
import URL from "Common/Types/API/URL";
test.describe("check admin dashboard", () => {
@@ -9,7 +9,7 @@ test.describe("check admin dashboard", () => {
page: Page;
}) => {
page.setDefaultNavigationTimeout(120000); // 2 minutes
const response = await page.goto(
const response: Response | null = await page.goto(
`${URL.fromString(BASE_URL.toString()).addRoute("/admin/env.js").toString()}`,
);
expect(response?.ok()).toBeTruthy();

View File

@@ -1,5 +1,5 @@
import { BASE_URL } from "../../Config";
import { Page, expect, test } from "@playwright/test";
import { Page, Response, expect, test } from "@playwright/test";
import URL from "Common/Types/API/URL";
test.describe("check public dashboard", () => {
@@ -9,7 +9,7 @@ test.describe("check public dashboard", () => {
page: Page;
}) => {
page.setDefaultNavigationTimeout(120000); // 2 minutes
const response = await page.goto(
const response: Response | null = await page.goto(
`${URL.fromString(BASE_URL.toString())
.addRoute("/public-dashboard")
.toString()}`,