diff --git a/App/FeatureSet/Dashboard/src/Components/Exceptions/ExceptionsDashboard.tsx b/App/FeatureSet/Dashboard/src/Components/Exceptions/ExceptionsDashboard.tsx index 392af433b4..3529c7d35f 100644 --- a/App/FeatureSet/Dashboard/src/Components/Exceptions/ExceptionsDashboard.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Exceptions/ExceptionsDashboard.tsx @@ -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>( [], ); + const [recentExceptions, setRecentExceptions] = useState< + Array + >([]); const [serviceSummaries, setServiceSummaries] = useState< Array >([]); @@ -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 = servicesResult.data || []; @@ -136,7 +169,6 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => { const serviceExceptionCounts: Array = []; for (const service of loadedServices) { - // Get unresolved exceptions for this service const serviceExceptions: ListResult = 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 ( -
-
+
+
-

+

No exceptions caught yet

-

- Once your services start reporting exceptions, you{"'"}ll see a - summary of bugs, their frequency, and which services are most - affected. +

+ Once your services start reporting exceptions, you{"'"}ll see bug + frequency, affected services, and resolution status here.

); } + 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 ( + {/* Unresolved Alert Banner */} + {unresolvedCount > 0 && ( + +
20 ? "bg-red-50 border border-red-200" : unresolvedCount > 5 ? "bg-amber-50 border border-amber-200" : "bg-blue-50 border border-blue-200"}`} + > +
+
20 ? "bg-red-100" : unresolvedCount > 5 ? "bg-amber-100" : "bg-blue-100"}`} + > + 20 ? "text-red-600" : unresolvedCount > 5 ? "text-amber-600" : "text-blue-600"}`} + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + strokeWidth={2} + > + + +
+
+

20 ? "text-red-800" : unresolvedCount > 5 ? "text-amber-800" : "text-blue-800"}`} + > + {unresolvedCount} unresolved{" "} + {unresolvedCount === 1 ? "bug" : "bugs"} need attention +

+

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"} +

+
+
+ 20 ? "text-red-400" : unresolvedCount > 5 ? "text-amber-400" : "text-blue-400"}`} + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + strokeWidth={2} + > + + +
+
+ )} + {/* Summary Stats */} -
+
-
+
-
-

Unresolved Bugs

-

- {unresolvedCount.toLocaleString()} -

-
-
+

Unresolved

+
-

Needs attention

+

+ {unresolvedCount.toLocaleString()} +

+

needs attention

@@ -284,31 +388,29 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => { RouteMap[PageMap.EXCEPTIONS_RESOLVED] as Route, )} > -
+
-
-

Resolved

-

- {resolvedCount.toLocaleString()} -

-
-
+

Resolved

+
-

Fixed and verified

+

+ {resolvedCount.toLocaleString()} +

+

fixed

@@ -318,49 +420,73 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => { RouteMap[PageMap.EXCEPTIONS_ARCHIVED] as Route, )} > -
+
-
-

Archived

-

- {archivedCount.toLocaleString()} -

-
-
+

Archived

+
-

- Dismissed or won{"'"}t fix +

+ {archivedCount.toLocaleString()}

+

dismissed

+ +
+
+

Resolution Rate

+
+ + + +
+
+

+ {resolutionRate}% +

+
+
+
+
-
- {/* Most Frequent Exceptions */} +
+ {/* Most Frequent Exceptions - takes 2 columns */} {topExceptions.length > 0 && ( -
-
-
-

+
+
+
+
+

Most Frequent Bugs

-

- Unresolved exceptions with the highest occurrence count -

{ View all
-
-
+
+
{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 ( { ] as Route, ) .toString() - .replace(/\/$/, `/${exception.fingerprint}`), + .replace( + /\/?$/, + `/${exception.fingerprint}`, + ), ) : RouteUtil.populateRouteParams( RouteMap[ @@ -402,39 +536,60 @@ const ExceptionsDashboard: FunctionComponent = (): ReactElement => { ) } > -
+
- -
+
+ + {isNewToday && ( + + New + + )} +
+
{exception.service && ( {exception.service.name?.toString()} )} + {exception.exceptionType && ( + + {exception.exceptionType} + + )} {exception.environment && ( {exception.environment} )} + {exception.lastSeenAt && ( + + {OneUptimeDate.fromNow( + new Date(exception.lastSeenAt), + )} + + )}
-

- {(exception.occuranceCount || 0).toLocaleString()} +

+ {( + exception.occuranceCount || 0 + ).toLocaleString()}

-

occurrences

+

hits

-
+
{
)} - {/* Services Affected */} - {serviceSummaries.length > 0 && ( -
-
-

- Affected Services -

-

- Services with unresolved exceptions -

-
-
-
- {serviceSummaries.map((summary: ServiceExceptionSummary) => { - return ( -
-
- -
-
-

- {summary.unresolvedCount} -

-

unresolved

+ {/* Right sidebar: Affected Services + Recently Seen */} +
+ {/* Affected Services */} + {serviceSummaries.length > 0 && ( +
+
+
+

+ Affected Services +

+
+
+
+ {serviceSummaries.map( + (summary: ServiceExceptionSummary) => { + const barWidth: number = + (summary.unresolvedCount / maxServiceBugs) * 100; + + return ( +
+
+ +
+
+ + {summary.unresolvedCount} + + + bugs + +
+
-
-

- {summary.totalOccurrences.toLocaleString()} -

-

total hits

+
+
+
+
+ + {summary.totalOccurrences.toLocaleString()} hits +
-
-
- ); - })} + ); + }, + )} +
-
- )} + )} + + {/* Recently Active */} + {recentExceptions.length > 0 && ( +
+
+
+

+ Recently Active +

+
+
+
+ {recentExceptions + .slice(0, 5) + .map( + (exception: TelemetryException, index: number) => { + return ( + +

+ {exception.message || + exception.exceptionType || + "Unknown"} +

+
+ {exception.service && ( + + {exception.service.name?.toString()} + + )} + {exception.lastSeenAt && ( + + {OneUptimeDate.fromNow( + new Date(exception.lastSeenAt), + )} + + )} +
+
+ ); + }, + )} +
+
+
+ )} +
); diff --git a/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsDashboard.tsx b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsDashboard.tsx index c163da40a0..4278f700aa 100644 --- a/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsDashboard.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsDashboard.tsx @@ -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; + metricUnits: Array; + metricDescriptions: Array; + 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 >([]); const [totalMetricCount, setTotalMetricCount] = useState(0); + const [metricCategories, setMetricCategories] = useState< + Array + >([]); + const [servicesWithNoMetrics, setServicesWithNoMetrics] = useState(0); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); + 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 = async (): Promise => { try { setIsLoading(true); setError(""); - // Load services - const servicesResult: ListResult = 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 = servicesResult.data || []; - - // Load all metric types with their services - const metricsResult: ListResult = 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 = metricsResult.data || []; setTotalMetricCount(metrics.length); + // Build category counts + const categoryMap: Map = new Map(); + for (const metric of metrics) { + const cat: string = categorizeMetric(metric.name || ""); + categoryMap.set(cat, (categoryMap.get(cat) || 0) + 1); + } + + const categoryColors: Record = + { + 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 = 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 = 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 = 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 = 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 ( -
-
+
+
- - - -
-

+

No metrics data yet

-

+

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.

); } + const maxMetrics: number = Math.max( + ...serviceSummaries.map((s: ServiceMetricSummary) => { + return s.metricCount; + }), + ); + return ( - {/* Summary Stats */} -
-
-

Total Metrics

-

+ {/* Hero Stats */} +

+
+
+

Total Metrics

+
+ + + +
+
+

{totalMetricCount}

+

unique metric types

-
-

Services Reporting

-

+ +

+
+

+ Services Reporting +

+
+ + + +
+
+

{serviceSummaries.length}

+

actively sending data

-
-

Avg Metrics per Service

-

+ +

+
+

+ Avg per Service +

+
+ + + +
+
+

{serviceSummaries.length > 0 ? Math.round(totalMetricCount / serviceSummaries.length) : 0}

+

metrics per service

+
+ +
+
+

+ No Metrics +

+
0 ? "bg-amber-50" : "bg-gray-50"}`} + > + 0 ? "text-amber-600" : "text-gray-400"}`} + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + strokeWidth={2} + > + + +
+
+

0 ? "text-amber-600" : "text-gray-900"}`} + > + {servicesWithNoMetrics} +

+

+ {servicesWithNoMetrics > 0 + ? "services not instrumented" + : "all services covered"} +

+ {/* Metric Categories */} + {metricCategories.length > 0 && ( +
+

+ Metric Categories +

+
+ {metricCategories.map((cat: MetricCategory) => { + const pct: number = + totalMetricCount > 0 + ? Math.round((cat.count / totalMetricCount) * 100) + : 0; + return ( +
+ + {cat.count} + + {cat.name} + + {pct}% + +
+ ); + })} +
+ {/* Category distribution bar */} +
+ {metricCategories.map((cat: MetricCategory) => { + const pct: number = + totalMetricCount > 0 + ? (cat.count / totalMetricCount) * 100 + : 0; + const barColorMap: Record = { + System: "bg-blue-400", + Request: "bg-purple-400", + Database: "bg-amber-400", + Messaging: "bg-green-400", + Custom: "bg-gray-300", + }; + return ( +
+ ); + })} +
+
+ )} + {/* Service Cards */} -
+
-

+

Services Reporting Metrics

-

- Each service and the metrics it collects +

+ Coverage and instrumentation per service

{
{serviceSummaries.map((summary: ServiceMetricSummary) => { + const coverage: number = + maxMetrics > 0 + ? Math.round((summary.metricCount / maxMetrics) * 100) + : 0; + return ( -
-
- - - Active - -
+
+
+ +
+ {summary.hasSystemMetrics && ( + + System + + )} + {summary.hasAppMetrics && ( + + App + + )} +
+
-
-

Metrics Collected

-

- {summary.metricCount} -

-
+ {/* Metric count with relative bar */} +
+
+ + {summary.metricCount} + + + metrics + +
+
+
+
+
-
-

Sample Metrics

+ {/* Metric name tags */}
{summary.metricNames.map((name: string) => { return ( {name} ); })} {summary.metricCount > summary.metricNames.length && ( - + +{summary.metricCount - summary.metricNames.length} more )}
- -
- - View service metrics - -
-
+ ); })}
diff --git a/App/FeatureSet/Dashboard/src/Components/Profiles/ProfilesDashboard.tsx b/App/FeatureSet/Dashboard/src/Components/Profiles/ProfilesDashboard.tsx index 6115ac5d3e..1b4e12919c 100644 --- a/App/FeatureSet/Dashboard/src/Components/Profiles/ProfilesDashboard.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Profiles/ProfilesDashboard.tsx @@ -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; + 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 >([]); const [hotspots, setHotspots] = useState>([]); + const [profileTypeStats, setProfileTypeStats] = useState< + Array + >([]); + const [totalProfileCount, setTotalProfileCount] = useState(0); + const [totalSampleCount, setTotalSampleCount] = useState(0); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); @@ -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 = 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 = servicesResult.data || []; - - // Load recent profiles (last 1 hour) to build per-service summaries - const profilesResult: AnalyticsListResult = - 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 = servicesResult.data || []; const profiles: Array = profilesResult.data || []; + setTotalProfileCount(profiles.length); + // Build per-service summaries const summaryMap: Map = new Map(); + const typeCountMap: Map = 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 = 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 = 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 | HTTPErrorResponse = await API.post({ @@ -193,7 +230,6 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => { ] || []) as unknown as Array; 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 ( -
-
+
+
-

- No performance data yet +

+ No profiling data yet

-

- Once your services start sending profiling data, you{"'"}ll see a - summary of which services are being profiled, their performance - hotspots, and more. +

+ Once your services start sending profiling data, you{"'"}ll see + performance hotspots, resource usage patterns, and optimization + opportunities.

); } + const maxProfiles: number = Math.max( + ...serviceSummaries.map((s: ServiceProfileSummary) => s.profileCount), + ); + return ( + {/* Hero Stats */} +
+
+
+

Profiles

+
+ + + +
+
+

+ {totalProfileCount.toLocaleString()} +

+

last hour

+
+ +
+
+

Services

+
+ + + +
+
+

+ {serviceSummaries.length} +

+

being profiled

+
+ +
+
+

Samples

+
+ + + +
+
+

+ {totalSampleCount >= 1_000_000 + ? `${(totalSampleCount / 1_000_000).toFixed(1)}M` + : totalSampleCount >= 1_000 + ? `${(totalSampleCount / 1_000).toFixed(1)}K` + : totalSampleCount.toLocaleString()} +

+

total samples

+
+ +
+
+

Hotspots

+
0 ? "bg-orange-50" : "bg-gray-50"}`} + > + 0 ? "text-orange-600" : "text-gray-400"}`} + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + strokeWidth={2} + > + + +
+
+

+ {hotspots.length} +

+

functions identified

+
+
+ + {/* Profile Type Distribution */} + {profileTypeStats.length > 0 && ( +
+

+ Profile Types Collected +

+
+ {profileTypeStats.map((stat: ProfileTypeStats) => { + const pct: number = + totalProfileCount > 0 + ? Math.round((stat.count / totalProfileCount) * 100) + : 0; + return ( +
+ {stat.count} + {stat.displayName} + {pct}% +
+ ); + })} +
+
+ )} + {/* Service Cards */} -
+
-

+

Services Being Profiled

-

+

Performance data collected in the last hour

@@ -276,42 +452,58 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => {
{serviceSummaries.map((summary: ServiceProfileSummary) => { + const coverage: number = + maxProfiles > 0 + ? Math.round((summary.profileCount / maxProfiles) * 100) + : 0; + return ( -
-
- - - Active - -
- -
-
-

Profiles

-

- {summary.profileCount} -

+
+
+ + + Active +
-
-

Last Captured

-

- {summary.latestProfileTime - ? OneUptimeDate.getDateAsLocalFormattedString( - summary.latestProfileTime, - true, - ) - : "-"} -

-
-
-
-

- Profile Types Collected -

+
+
+

Profiles

+

+ {summary.profileCount} +

+
+
+

Samples

+

+ {summary.totalSamples >= 1_000 + ? `${(summary.totalSamples / 1_000).toFixed(1)}K` + : summary.totalSamples.toLocaleString()} +

+
+
+ + {/* Profile volume bar */} +
+
+
+
+
+ + {/* Profile type badges */}
{summary.profileTypes.map((profileType: string) => { const badgeColor: string = @@ -319,29 +511,22 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => { return ( {ProfileUtil.getProfileTypeDisplayName(profileType)} ); })}
-
-
- - View service profiles - + {summary.latestProfileTime && ( +

+ Last captured{" "} + {OneUptimeDate.fromNow(summary.latestProfileTime)} +

+ )}
-
+ ); })}
@@ -350,70 +535,80 @@ const ProfilesDashboard: FunctionComponent = (): ReactElement => { {/* Top Hotspots */} {hotspots.length > 0 && (
-
-

+
+
+

Top Performance Hotspots

-

- Functions using the most resources across all services in the last - hour -

-
- - - - - - - - - - - - - {hotspots.map((fn: FunctionHotspot, index: number) => { - const maxSelf: number = hotspots[0]?.selfValue || 1; - const barWidth: number = (fn.selfValue / maxSelf) * 100; +

+ Functions consuming the most resources across all services +

+
+
+ {hotspots.map((fn: FunctionHotspot, index: number) => { + const maxSelf: number = hotspots[0]?.selfValue || 1; + const barWidth: number = (fn.selfValue / maxSelf) * 100; - return ( -
- - - - - - - - ); - })} - -
#FunctionSource FileOwn Time - Total Time - - Occurrences -
- {index + 1} - -
- {fn.functionName} + return ( +
+
+
+
+ + #{index + 1} + +

+ {fn.functionName} +

+ {fn.frameType && ( + + {fn.frameType} + + )}
+ {fn.fileName && ( +

+ {fn.fileName} +

+ )} +
+
+
+

+ {fn.selfValue.toLocaleString()} +

+

own time

+
+
+

+ {fn.totalValue.toLocaleString()} +

+

total

+
+
+

+ {fn.sampleCount.toLocaleString()} +

+

samples

+
+
+
+
+
-
- {fn.fileName || "-"} - - {fn.selfValue.toLocaleString()} - - {fn.totalValue.toLocaleString()} - - {fn.sampleCount.toLocaleString()} -
+
+
+

+ ); + })} +
)} diff --git a/App/FeatureSet/Dashboard/src/Components/Traces/TracesDashboard.tsx b/App/FeatureSet/Dashboard/src/Components/Traces/TracesDashboard.tsx index 30d9fbbab9..c1fbd1d7a5 100644 --- a/App/FeatureSet/Dashboard/src/Components/Traces/TracesDashboard.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Traces/TracesDashboard.tsx @@ -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; } 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, p: number) => number = ( + arr: Array, + p: number, +): number => { + if (arr.length === 0) { + return 0; + } + const sorted: Array = [...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 @@ -55,22 +82,14 @@ const TracesDashboard: FunctionComponent = (): ReactElement => { [], ); const [services, setServices] = useState>([]); + const [totalRequests, setTotalRequests] = useState(0); + const [totalErrors, setTotalErrors] = useState(0); + const [globalP50, setGlobalP50] = useState(0); + const [globalP95, setGlobalP95] = useState(0); + const [globalP99, setGlobalP99] = useState(0); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); - 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 = async (): Promise => { 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 = 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 = servicesResult.data || []; - setServices(loadedServices); - - // Load recent spans (last 1 hour) to build per-service summaries - const spansResult: AnalyticsListResult = - 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 = servicesResult.data || []; + setServices(loadedServices); const allSpans: Array = spansResult.data || []; - // Build per-service summaries from all spans + // Build per-service summaries const summaryMap: Map = 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> = new Map(); const serviceErrorTraceIds: Map> = new Map(); - const errorTraces: Array = []; const allTraces: Array = []; const seenTraceIds: Set = new Set(); const seenErrorTraceIds: Set = new Set(); + const allDurations: Array = []; 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 = serviceTraceIds.get(serviceId)!; - if (!traceSet.has(traceId)) { traceSet.add(traceId); summary.totalTraces += 1; } - if (span.statusCode === SpanStatus.Error) { - const errorSet: Set = serviceErrorTraceIds.get(serviceId)!; + if (duration > 0) { + summary.durations.push(duration); + } + if (span.statusCode === SpanStatus.Error) { + const errorSet: Set = + 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 = 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 = [...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 ( -
-
+
+
- {/* Three horizontal bars representing a waterfall/trace timeline */} - - - - {/* Connecting lines */} - -
-

+

No trace data yet

-

+

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.

); } + const overallErrorRate: number = + totalRequests > 0 ? (totalErrors / totalRequests) * 100 : 0; + return ( - {/* Service Cards */} -
+ {/* Hero Stats */} +
+
+

Requests

+

+ {totalRequests.toLocaleString()} +

+

last hour

+
+ +
5 ? "border-red-200 bg-red-50" : overallErrorRate > 1 ? "border-amber-200 bg-amber-50" : "border-gray-200 bg-white"}`} + > +

Error Rate

+

5 ? "text-red-600" : overallErrorRate > 1 ? "text-amber-600" : "text-green-600"}`} + > + {overallErrorRate.toFixed(1)}% +

+

+ {totalErrors.toLocaleString()} errors +

+
+ +
+

P50 Latency

+

+ {formatDuration(globalP50)} +

+

median

+
+ +
1_000_000_000 ? "border-amber-200 bg-amber-50" : "border-gray-200 bg-white"}`} + > +

P95 Latency

+

1_000_000_000 ? "text-amber-600" : "text-gray-900"}`} + > + {formatDuration(globalP95)} +

+

95th percentile

+
+ +
2_000_000_000 ? "border-red-200 bg-red-50" : "border-gray-200 bg-white"}`} + > +

P99 Latency

+

2_000_000_000 ? "text-red-600" : "text-gray-900"}`} + > + {formatDuration(globalP99)} +

+

99th percentile

+
+
+ + {/* Service Health Table */} +
-

- Services Overview +

+ Service Health

-

- Request activity across your services in the last hour +

+ Sorted by error rate — services needing attention first

{ View all spans
-
- {serviceSummaries.map((summary: ServiceTraceSummary) => { - const errorRate: number = - summary.totalTraces > 0 - ? (summary.errorTraces / summary.totalTraces) * 100 - : 0; +
+ + + + + + + + + + + + + + {serviceSummaries.map((summary: ServiceTraceSummary) => { + const errorRate: number = + summary.totalTraces > 0 + ? (summary.errorTraces / summary.totalTraces) * 100 + : 0; - return ( -
-
- - {errorRate > 5 ? ( - - {errorRate.toFixed(1)}% errors - - ) : ( - - Healthy - - )} -
+ 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"; + } -
-
-

Requests

-

- {summary.totalTraces.toLocaleString()} -

-
-
-

Errors

-

0 ? "text-red-600" : "text-gray-900"}`} - > - {summary.errorTraces.toLocaleString()} -

-
-
-

Last Seen

-

- {summary.latestTraceTime - ? OneUptimeDate.getDateAsLocalFormattedString( - summary.latestTraceTime, - true, - ) - : "-"} -

-
-
- - {/* Error rate bar */} -
-
- Error Rate - {errorRate.toFixed(1)}% -
-
-
5 ? "bg-red-500" : errorRate > 0 ? "bg-yellow-400" : "bg-green-400"}`} - style={{ - width: `${Math.max(errorRate, errorRate > 0 ? 2 : 0)}%`, - }} - /> -
-
- -
- - View service traces - -
-
- ); - })} +
+ + + + + + + + ); + })} + +
+ Service + + Requests + + Error Rate + + P50 + + P95 + + Status + +   +
+ + + + {summary.totalTraces.toLocaleString()} + + +
+
+
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)}%`, + }} + /> +
+ 5 ? "text-red-600" : "text-gray-900"}`} + > + {errorRate.toFixed(1)}% + +
+
+ + {formatDuration(summary.p50Nanos)} + + + + {formatDuration(summary.p95Nanos)} + + + + + {healthLabel} + + + + View + +
- {/* Two-column layout for errors and slow traces */} + {/* Two-column: Errors + Slow Requests */}
{/* Recent Errors */}
-
-

- Recent Errors -

-

- Failed requests in the last hour -

+
+
+
+

+ Recent Errors +

+ {recentErrorTraces.length > 0 && ( + + {recentErrorTraces.length} + + )} +
{recentErrorTraces.length === 0 ? ( -
-

+

+
+ + + +
+

No errors in the last hour

+

Looking good!

) : ( -
-
+
+
{recentErrorTraces.map((trace: RecentTrace, index: number) => { return (
@@ -510,10 +637,7 @@ const TracesDashboard: FunctionComponent = (): ReactElement => { {formatDuration(trace.durationNano)}

- {OneUptimeDate.getDateAsLocalFormattedString( - trace.startTime, - true, - )} + {OneUptimeDate.fromNow(trace.startTime)}

@@ -525,25 +649,23 @@ const TracesDashboard: FunctionComponent = (): ReactElement => { )}
- {/* Slowest Traces */} + {/* Slowest Requests */}
-
-

+
+
+

Slowest Requests

-

- Longest running operations in the last hour -

{recentSlowTraces.length === 0 ? ( -
+

- No traces found in the last hour + No traces in the last hour

) : ( -
-
+
+
{recentSlowTraces.map((trace: RecentTrace, index: number) => { const maxDuration: number = recentSlowTraces[0]?.durationNano || 1; @@ -553,15 +675,13 @@ const TracesDashboard: FunctionComponent = (): ReactElement => { return ( -
+
{ @@ -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(); diff --git a/E2E/Tests/PublicDashboard/StatusCheck.spec.ts b/E2E/Tests/PublicDashboard/StatusCheck.spec.ts index 1d7f6e0944..4809be9afb 100644 --- a/E2E/Tests/PublicDashboard/StatusCheck.spec.ts +++ b/E2E/Tests/PublicDashboard/StatusCheck.spec.ts @@ -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()}`,