feat: add Exceptions page and integrate with routing and side menu

This commit is contained in:
Nawaz Dhandala
2026-04-02 18:20:36 +01:00
parent e98b424168
commit b0c9de4d82
6 changed files with 126 additions and 20 deletions

View File

@@ -19,7 +19,6 @@ import AnalyticsModelAPI, {
ListResult as AnalyticsListResult,
} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import IsNull from "Common/Types/BaseDatabase/IsNull";
import OneUptimeDate from "Common/Types/Date";
import ObjectID from "Common/Types/ObjectID";
import ServiceElement from "../Service/ServiceElement";
@@ -100,17 +99,18 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
const loadedServices: Array<Service> = servicesResult.data || [];
setServices(loadedServices);
// Load recent root spans (last 1 hour) to build per-service summaries
const rootSpansResult: AnalyticsListResult<Span> =
// Load recent spans (last 1 hour) to build per-service summaries
const spansResult: AnalyticsListResult<Span> =
await AnalyticsModelAPI.getList({
modelType: Span,
query: {
projectId: ProjectUtil.getCurrentProjectId()!,
startTime: new InBetween(oneHourAgo, now),
parentSpanId: new IsNull(),
},
select: {
traceId: true,
spanId: true,
parentSpanId: true,
serviceId: true,
name: true,
startTime: true,
@@ -124,9 +124,9 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
},
});
const rootSpans: Array<Span> = rootSpansResult.data || [];
const allSpans: Array<Span> = spansResult.data || [];
// Build per-service summaries from root spans
// Build per-service summaries from all spans
const summaryMap: Map<string, ServiceTraceSummary> = new Map();
for (const service of loadedServices) {
@@ -139,19 +139,46 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
});
}
// 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();
for (const span of rootSpans) {
for (const span of allSpans) {
const serviceId: string = span.serviceId?.toString() || "";
const traceId: string = span.traceId?.toString() || "";
const summary: ServiceTraceSummary | undefined =
summaryMap.get(serviceId);
if (summary) {
summary.totalTraces += 1;
// 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) {
summary.errorTraces += 1;
const errorSet: Set<string> =
serviceErrorTraceIds.get(serviceId)!;
if (!errorSet.has(traceId)) {
errorSet.add(traceId);
summary.errorTraces += 1;
}
}
const spanTime: Date | undefined = span.startTime
@@ -166,20 +193,44 @@ const TracesDashboard: FunctionComponent = (): ReactElement => {
}
}
const traceRecord: RecentTrace = {
traceId: span.traceId?.toString() || "",
name: span.name?.toString() || "Unknown",
serviceId: serviceId,
startTime: span.startTime ? new Date(span.startTime) : new Date(),
statusCode: span.statusCode || SpanStatus.Unset,
durationNano: (span.durationUnixNano as number) || 0,
};
// 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);
if (span.statusCode === SpanStatus.Error) {
errorTraces.push(traceRecord);
const traceRecord: RecentTrace = {
traceId: traceId,
name: span.name?.toString() || "Unknown",
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);
}
allTraces.push(traceRecord);
// Collect error spans, deduped by trace
if (
span.statusCode === SpanStatus.Error &&
traceId &&
!seenErrorTraceIds.has(traceId)
) {
seenErrorTraceIds.add(traceId);
errorTraces.push({
traceId: traceId,
name: span.name?.toString() || "Unknown",
serviceId: serviceId,
startTime: span.startTime
? new Date(span.startTime)
: new Date(),
statusCode: span.statusCode,
durationNano: (span.durationUnixNano as number) || 0,
});
}
}
// Only show services that have traces

View File

@@ -0,0 +1,24 @@
import ExceptionsTable from "../../../Components/Exceptions/ExceptionsTable";
import PageComponentProps from "../../PageComponentProps";
import ObjectID from "Common/Types/ObjectID";
import Navigation from "Common/UI/Utils/Navigation";
import React, { Fragment, FunctionComponent, ReactElement } from "react";
const ServiceExceptions: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
return (
<Fragment>
<ExceptionsTable
serviceId={modelId}
query={{}}
title="Exceptions"
description="All the exceptions for this service."
/>
</Fragment>
);
};
export default ServiceExceptions;

View File

@@ -144,6 +144,17 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
}}
icon={IconProp.Fire}
/>
<SideMenuItem
link={{
title: "Exceptions",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_VIEW_EXCEPTIONS] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Error}
/>
</SideMenuSection>
<SideMenuSection title="Advanced">

View File

@@ -26,6 +26,8 @@ import ServiceViewMetrics from "../Pages/Service/View/Metrics";
import ServiceViewProfiles from "../Pages/Service/View/Profiles";
import ServiceViewExceptions from "../Pages/Service/View/Exceptions";
import ServiceViewDelete from "../Pages/Service/View/Delete";
import ServiceViewSettings from "../Pages/Service/View/Settings";
@@ -166,6 +168,16 @@ const ServiceRoutes: FunctionComponent<ComponentProps> = (
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.SERVICE_VIEW_EXCEPTIONS)}
element={
<ServiceViewExceptions
{...props}
pageRoute={RouteMap[PageMap.SERVICE_VIEW_EXCEPTIONS] as Route}
/>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.SERVICE_VIEW_OWNERS)}
element={

View File

@@ -220,6 +220,7 @@ enum PageMap {
SERVICE_VIEW_TRACES = "SERVICE_VIEW_TRACES",
SERVICE_VIEW_METRICS = "SERVICE_VIEW_METRICS",
SERVICE_VIEW_PROFILES = "SERVICE_VIEW_PROFILES",
SERVICE_VIEW_EXCEPTIONS = "SERVICE_VIEW_EXCEPTIONS",
SERVICE_VIEW_OWNERS = "SERVICE_VIEW_OWNERS",
SERVICE_VIEW_DEPENDENCIES = "SERVICE_VIEW_DEPENDENCIES",
SERVICE_VIEW_CODE_REPOSITORIES = "SERVICE_VIEW_CODE_REPOSITORIES",

View File

@@ -50,6 +50,7 @@ export const ServiceRoutePath: Dictionary<string> = {
[PageMap.SERVICE_VIEW_TRACES]: `${RouteParams.ModelID}/traces`,
[PageMap.SERVICE_VIEW_METRICS]: `${RouteParams.ModelID}/metrics`,
[PageMap.SERVICE_VIEW_PROFILES]: `${RouteParams.ModelID}/profiles`,
[PageMap.SERVICE_VIEW_EXCEPTIONS]: `${RouteParams.ModelID}/exceptions`,
[PageMap.SERVICE_VIEW_CODE_REPOSITORIES]: `${RouteParams.ModelID}/code-repositories`,
};
@@ -1486,6 +1487,12 @@ const RouteMap: Dictionary<Route> = {
}`,
),
[PageMap.SERVICE_VIEW_EXCEPTIONS]: new Route(
`/dashboard/${RouteParams.ProjectID}/service/${
ServiceRoutePath[PageMap.SERVICE_VIEW_EXCEPTIONS]
}`,
),
[PageMap.SERVICE_VIEW_CODE_REPOSITORIES]: new Route(
`/dashboard/${RouteParams.ProjectID}/service/${
ServiceRoutePath[PageMap.SERVICE_VIEW_CODE_REPOSITORIES]