From 7f1243c3c0759a3c2c34e6f143a43ba9d5b71093 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Sat, 15 Aug 2026 01:04:11 +0200 Subject: [PATCH] feat(web): give API services an endpoint-shaped view of themselves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The service detail page treated every service the same: Overview, Operations, Dependencies. For an HTTP service that buries the unit people actually reason about — the endpoint — inside a list of raw span names, with no way to open one. Detect HTTP APIs and offer them an Endpoints tab plus a per-endpoint page. Detection rides the existing service-detail overview bundle rather than taking its own round-trip, because it gates a trigger in the page header and so must resolve on first paint of *every* tab, not just Overview. It is deliberately permissive (any server traffic with routes, no ratio gate) — the tab is additive, so a false positive costs a thin extra tab while a false negative hides the feature. A probe failure degrades to "not an API" instead of taking the page's charts down with it. The endpoint list rides the existing rollups: service_operations_* already stores the normalized "GET /api/users" name, so it is an endpoint rollup in disguise. Raw edges filter accurately on SpanKind = 'Server'; the rollup interiors match the display-name shape, since those tables dropped SpanKind. The consequence is recorded in the file: a non-HTTP span literally named "GET /foo" counts inside the rollup window. A dedicated service_endpoints_hourly MV removes both that heuristic and the string split, and ch/queries/ service-endpoints.ts is the single swap point when it lands. Also fixes a latent bug this surfaced. canUseAnnualServiceOverview gated the service-overview rollup route on the singular `spanName` but not the plural `spanNames` — the spelling every modern caller uses. Those queries were routed to service_overview_minutely/_hourly, which aggregate SpanName away, so the filter was silently dropped and the caller got service-wide numbers under one endpoint's name with no error anywhere. canUseServiceOverviewMv checks both spellings; this predicate already checked excludedSpanNames, so the omission was an oversight rather than intent. It had no test coverage at all. --- .../src/routes/internal/query-engine.http.ts | 178 +- apps/web/src/api/warehouse/custom-charts.ts | 26 +- apps/web/src/api/warehouse/endpoint-status.ts | 63 + .../src/api/warehouse/service-endpoints.ts | 79 + .../services/service-endpoints-tab.tsx | 511 ++++ .../services/service-endpoints.test.ts | 93 + .../components/services/service-endpoints.ts | 95 + .../services/service-top-operations-panel.tsx | 209 +- .../services/atoms/warehouse-query-atoms.ts | 19 + apps/web/src/routeTree.gen.ts | 2610 +++++++++-------- apps/web/src/routes/services/$serviceName.tsx | 127 +- .../services/$serviceName_.endpoints.tsx | 542 ++++ packages/domain/src/http/query-engine.ts | 98 + .../src/__sql_baseline__/catalog.sql | 172 ++ .../query-engine/src/ch/builder-fixtures.ts | 44 + packages/query-engine/src/ch/index.ts | 19 + .../ch/queries/overview-rollup-route.test.ts | 107 + .../src/ch/queries/service-endpoints.test.ts | 160 + .../src/ch/queries/service-endpoints.ts | 395 +++ .../query-engine/src/ch/queries/traces.ts | 5 + packages/query-engine/src/registry/queries.ts | 91 + packages/query-engine/src/sql-catalog.test.ts | 3 + 22 files changed, 4287 insertions(+), 1359 deletions(-) create mode 100644 apps/web/src/api/warehouse/endpoint-status.ts create mode 100644 apps/web/src/api/warehouse/service-endpoints.ts create mode 100644 apps/web/src/components/services/service-endpoints-tab.tsx create mode 100644 apps/web/src/components/services/service-endpoints.test.ts create mode 100644 apps/web/src/components/services/service-endpoints.ts create mode 100644 apps/web/src/routes/services/$serviceName_.endpoints.tsx create mode 100644 packages/query-engine/src/ch/queries/overview-rollup-route.test.ts create mode 100644 packages/query-engine/src/ch/queries/service-endpoints.test.ts create mode 100644 packages/query-engine/src/ch/queries/service-endpoints.ts diff --git a/apps/api/src/routes/internal/query-engine.http.ts b/apps/api/src/routes/internal/query-engine.http.ts index dc63bac24..6086b4dc2 100644 --- a/apps/api/src/routes/internal/query-engine.http.ts +++ b/apps/api/src/routes/internal/query-engine.http.ts @@ -34,6 +34,8 @@ import { ServiceWorkloadsResponse, ServiceUsageResponse, ServiceOperationsResponse, + ServiceEndpointsResponse, + EndpointStatusBreakdownResponse, ListLogsResponse, GetLogResponse, ListMetricsResponse, @@ -202,6 +204,57 @@ const withServiceOperationsFallback = makeRollupFallback( "service_operations rollup is absent on this cluster; reading raw traces. Apply ClickHouse schema to restore the fast path.", ) +/** + * Detection verdict + the counts behind it. `isHttpApiService` lives in the + * query package next to the query that feeds it, so the API, the web app, and + * any future MCP/CLI consumer classify identically. + */ +const toApiProfile = (row: { + readonly httpServerSpans: unknown + readonly entrySpans: unknown + readonly distinctEndpoints: unknown +}) => { + const profile = { + httpServerSpans: Number(row.httpServerSpans ?? 0), + entrySpans: Number(row.entrySpans ?? 0), + distinctEndpoints: Number(row.distinctEndpoints ?? 0), + } + return { ...profile, isHttpApi: CH.isHttpApiService(profile) } +} + +/** + * The per-operation sparkline is minute-grain because the rollup it splices is: + * every interval must be a whole-minute multiple, or the raw edges and the + * rollup interiors stop tiling and buckets double-count. Nearest-minute + * rounding of `window/50` keeps roughly fifty points. + */ +const sparklineBucketSeconds = (payload: { + readonly startTime: string + readonly endTime: string + readonly bucketSeconds?: number | undefined +}): number => { + const windowSeconds = Math.max( + 0, + (Date.parse(`${payload.endTime.replace(" ", "T")}Z`) - + Date.parse(`${payload.startTime.replace(" ", "T")}Z`)) / + 1000, + ) + return Math.max(1, Math.round((payload.bucketSeconds ?? windowSeconds / 50) / 60)) * 60 +} + +const groupSparklines = ( + rows: ReadonlyArray<{ readonly spanName: unknown; readonly bucket: unknown; readonly count: unknown }>, +) => { + const sparklines = new Map>() + for (const row of rows) { + const key = String(row.spanName) + const points = sparklines.get(key) ?? [] + points.push({ bucket: String(row.bucket), count: Number(row.count ?? 0) }) + sparklines.set(key, points) + } + return sparklines +} + const decodeTraceId = Schema.decodeSync(TraceId) const decodeSpanId = Schema.decodeSync(SpanId) const decodeServiceName = Schema.decodeUnknownSync(ServiceName) @@ -901,7 +954,7 @@ export const HttpQueryEngineLive = HttpApiBuilder.group(MapleInternalApi, "query // execute-path cache; releases is uncached (mirrors the standalone // handler); environments is edge-cached on a service-scoped key. yield* warehouse.warmRoute(tenant) - const [timeseries, releaseRows, environmentRows] = yield* Effect.all( + const [timeseries, releaseRows, environmentRows, apiProfileRow] = yield* Effect.all( [ queryEngine.execute(tenant, payload.timeseries), runQuery(Queries.serviceReleases, tenant, payload), @@ -910,8 +963,24 @@ export const HttpQueryEngineLive = HttpApiBuilder.group(MapleInternalApi, "query startTime: payload.startTime, endTime: payload.endTime, }), + // Rides along so the Endpoints tab trigger is known on first + // paint of EVERY tab, not just Overview (the header's env + // switcher subscribes to this same response). + // + // Deliberately NOT environment-scoped: whether a service is an + // HTTP API is a property of the service, not of the env you + // happen to be looking at, and an unscoped answer keeps the tab + // from flickering away when you select a low-traffic env. + // + // Detection is a nicety; the chart is the page. A probe failure + // degrades to "not an API" rather than failing the bundle. + runQueryFirst(Queries.serviceApiProfile, tenant, { + serviceName: payload.serviceName, + startTime: payload.startTime, + endTime: payload.endTime, + }).pipe(Effect.orElseSucceed(() => null)), ], - { concurrency: 3 }, + { concurrency: 4 }, ) return new ServiceDetailOverviewResponse({ timeseries, @@ -924,6 +993,7 @@ export const HttpQueryEngineLive = HttpApiBuilder.group(MapleInternalApi, "query environments: environmentRows .map((row) => String(row.environment ?? "")) .filter((env) => env !== ""), + ...(apiProfileRow ? { apiProfile: toApiProfile(apiProfileRow) } : {}), }) }), ) @@ -1095,17 +1165,11 @@ export const HttpQueryEngineLive = HttpApiBuilder.group(MapleInternalApi, "query } const spanNames = summaryRows.map((row) => String(row.spanName)) - // The rollup is minute-grain, so every sparkline interval must be - // a whole-minute multiple. Nearest-minute rounding keeps ~50 points. - const windowSeconds = Math.max( - 0, - (Date.parse(`${payload.endTime.replace(" ", "T")}Z`) - - Date.parse(`${payload.startTime.replace(" ", "T")}Z`)) / - 1000, - ) - const requestedBucketSeconds = payload.bucketSeconds ?? windowSeconds / 50 - const bucketSeconds = Math.max(1, Math.round(requestedBucketSeconds / 60)) * 60 - const timeseriesInput = { ...payload, spanNames, bucketSeconds } + const timeseriesInput = { + ...payload, + spanNames, + bucketSeconds: sparklineBucketSeconds(payload), + } const timeseriesRows = yield* mapExecError( withServiceOperationsFallback( (t, pl) => runQuery(Queries.serviceOperationsTimeseries, t, pl), @@ -1116,13 +1180,7 @@ export const HttpQueryEngineLive = HttpApiBuilder.group(MapleInternalApi, "query "serviceOperationsTimeseries query failed", ) - const sparklines = new Map>() - for (const row of timeseriesRows) { - const key = String(row.spanName) - const points = sparklines.get(key) ?? [] - points.push({ bucket: String(row.bucket), count: toNumber(row.count) }) - sparklines.set(key, points) - } + const sparklines = groupSparklines(timeseriesRows) return summaryRows.map((row) => ({ spanName: String(row.spanName), @@ -1142,6 +1200,86 @@ export const HttpQueryEngineLive = HttpApiBuilder.group(MapleInternalApi, "query return new ServiceOperationsResponse({ data }) }), ) + .handle("serviceEndpoints", ({ payload }) => + Effect.gen(function* () { + const tenant = yield* CurrentTenant.Context + const toNumber = (value: unknown) => Number(value ?? 0) + const data = yield* queryEngine.cachedDirect( + tenant, + "serviceEndpoints", + payload, + Effect.gen(function* () { + // Same rollup-or-raw shape as `serviceOperations`: both reads + // try `service_operations_minutely`/`_hourly` and degrade per-org + // on UNKNOWN_TABLE, and the timeseries read repeats the probe + // rather than inheriting the summary's verdict. + const summaryRows = yield* mapExecError( + withServiceOperationsFallback( + (t, pl) => runQuery(Queries.serviceEndpointsSummary, t, pl), + (t, pl) => runQuery(Queries.serviceEndpointsSummaryRaw, t, pl), + tenant, + payload, + ), + "serviceEndpoints query failed", + ) + if (summaryRows.length === 0) { + return [] + } + + // Sparklines reuse the operations timeseries verbatim — it keys + // on the display span name, which is exactly what the endpoint + // summary returns alongside the split method/route. + const spanNames = summaryRows.map((row) => String(row.spanName)) + const timeseriesInput = { + ...payload, + spanNames, + bucketSeconds: sparklineBucketSeconds(payload), + } + const timeseriesRows = yield* mapExecError( + withServiceOperationsFallback( + (t, pl) => runQuery(Queries.serviceOperationsTimeseries, t, pl), + (t, pl) => runQuery(Queries.serviceOperationsTimeseriesRaw, t, pl), + tenant, + timeseriesInput, + ), + "serviceEndpointsTimeseries query failed", + ) + const sparklines = groupSparklines(timeseriesRows) + + return summaryRows.map((row) => ({ + spanName: String(row.spanName), + method: String(row.method ?? ""), + route: String(row.route ?? ""), + spanCount: toNumber(row.spanCount), + estimatedSpanCount: toNumber(row.estimatedSpanCount), + errorCount: toNumber(row.errorCount), + estimatedErrorCount: toNumber(row.estimatedErrorCount), + errorRate: toNumber(row.errorRate), + avgDurationMs: toNumber(row.avgDurationMs), + p50DurationMs: toNumber(row.p50DurationMs), + p95DurationMs: toNumber(row.p95DurationMs), + p99DurationMs: toNumber(row.p99DurationMs), + sparkline: sparklines.get(String(row.spanName)) ?? [], + })) + }), + 30, + ) + return new ServiceEndpointsResponse({ data }) + }), + ) + .handle("endpointStatusBreakdown", ({ payload }) => + Effect.gen(function* () { + const tenant = yield* CurrentTenant.Context + const rows = yield* runQuery(Queries.endpointStatusBreakdown, tenant, payload) + return new EndpointStatusBreakdownResponse({ + data: rows.map((row) => ({ + statusClass: String(row.statusClass), + spanCount: Number(row.spanCount ?? 0), + estimatedSpanCount: Number(row.estimatedSpanCount ?? 0), + })), + }) + }), + ) .handle("listLogs", ({ payload }) => Effect.gen(function* () { const tenant = yield* CurrentTenant.Context diff --git a/apps/web/src/api/warehouse/custom-charts.ts b/apps/web/src/api/warehouse/custom-charts.ts index 18b1902ed..2fb0a1314 100644 --- a/apps/web/src/api/warehouse/custom-charts.ts +++ b/apps/web/src/api/warehouse/custom-charts.ts @@ -579,9 +579,13 @@ const GetCustomChartServiceDetailInputSchema = Schema.Struct({ // service list's display value (incl. the synthetic `"unknown"`); the // `"unknown" -> ""` remap to the raw warehouse value happens in `toEnvFilter`. environments: Schema.optional(Schema.mutable(Schema.Array(DeploymentEnvironment))), + // Narrows every chart to one operation — the endpoint detail route passes the + // display span name ("GET /api/users"), which `tracesBaseWhereConditions` + // matches against both the raw and rewritten spelling. + spanNames: Schema.optional(Schema.mutable(Schema.Array(SpanName))), }) -type GetCustomChartServiceDetailInput = (typeof GetCustomChartServiceDetailInputSchema)["Encoded"] +export type GetCustomChartServiceDetailInput = (typeof GetCustomChartServiceDetailInputSchema)["Encoded"] export function getCustomChartServiceDetail({ data }: { data: GetCustomChartServiceDetailInput }) { return getCustomChartServiceDetailEffect({ data }) @@ -595,6 +599,7 @@ function makeAllMetricsTimeseriesRequest(opts: { rootSpansOnly?: boolean environments?: ReadonlyArray commitShas?: ReadonlyArray + spanNames?: ReadonlyArray groupBy?: string[] }) { return new QueryEngineExecuteRequest({ @@ -611,6 +616,7 @@ function makeAllMetricsTimeseriesRequest(opts: { rootSpansOnly: opts.rootSpansOnly ?? true, environments: opts.environments, commitShas: opts.commitShas, + spanNames: opts.spanNames, }, bucketSeconds: opts.bucketSeconds, }, @@ -809,6 +815,7 @@ const getCustomChartServiceDetailEffect = Effect.fn("QueryEngine.getCustomChartS serviceName: input.serviceName, rootSpansOnly: true, environments: toEnvFilter(input.environments), + spanNames: input.spanNames, } // Throughput renders immediately from the sampling-aware `estimatedSpanCount` @@ -835,10 +842,26 @@ const getCustomChartServiceDetailEffect = Effect.fn("QueryEngine.getCustomChartS * read the SAME atom key, so this fires once for the whole tab instead of three * independent browser→Worker round-trips. */ +export interface ServiceApiProfile { + isHttpApi: boolean + httpServerSpans: number + entrySpans: number + distinctEndpoints: number +} + +/** An API deployed behind this build omits `apiProfile`; treat that as "not an API". */ +const NOT_AN_API: ServiceApiProfile = { + isHttpApi: false, + httpServerSpans: 0, + entrySpans: 0, + distinctEndpoints: 0, +} + export interface ServiceDetailOverviewResult { data: ServiceDetailTimeSeriesPoint[] releases: ReadonlyArray<{ bucket: string; commitSha: CommitSha; count: number; errorCount: number }> environments: string[] + apiProfile: ServiceApiProfile } export function getServiceDetailOverview({ data }: { data: GetCustomChartServiceDetailInput }) { @@ -891,6 +914,7 @@ const getServiceDetailOverviewEffect = Effect.fn("QueryEngine.getServiceDetailOv errorCount: Number(r.errorCount ?? 0), })), environments: [...result.environments], + apiProfile: result.apiProfile ? { ...result.apiProfile } : NOT_AN_API, } satisfies ServiceDetailOverviewResult }) diff --git a/apps/web/src/api/warehouse/endpoint-status.ts b/apps/web/src/api/warehouse/endpoint-status.ts new file mode 100644 index 000000000..a950825aa --- /dev/null +++ b/apps/web/src/api/warehouse/endpoint-status.ts @@ -0,0 +1,63 @@ +import { Effect, Schema } from "effect" +import { DeploymentEnvironment, EndpointStatusBreakdownRequest, ServiceName } from "@maple/domain/http" +import { MapleInternalAtomClient } from "@/lib/services/common/internal-atom-client" +import { WarehouseDateTimeString, decodeInput, runWarehouseQuery } from "@/api/warehouse/effect-utils" + +/** The classes the warehouse emits, in the order they should render. */ +export const STATUS_CLASS_ORDER = ["2xx", "3xx", "4xx", "5xx", "1xx", "unknown"] as const + +export interface EndpointStatusSlice { + statusClass: string + spanCount: number + estimatedSpanCount: number +} + +const GetEndpointStatusBreakdownInput = Schema.Struct({ + serviceName: ServiceName, + /** Display span name ("GET /api/users"). */ + spanName: Schema.String, + startTime: WarehouseDateTimeString, + endTime: WarehouseDateTimeString, + environments: Schema.optional(Schema.Array(DeploymentEnvironment)), +}) + +export type GetEndpointStatusBreakdownInput = (typeof GetEndpointStatusBreakdownInput)["Encoded"] + +export const getEndpointStatusBreakdown = Effect.fn("QueryEngine.getEndpointStatusBreakdown")(function* ({ + data, +}: { + data: GetEndpointStatusBreakdownInput +}) { + const input = yield* decodeInput(GetEndpointStatusBreakdownInput, data, "getEndpointStatusBreakdown") + + const result = yield* runWarehouseQuery("endpointStatusBreakdown", () => + Effect.gen(function* () { + const client = yield* MapleInternalAtomClient + return yield* client.queryEngine.endpointStatusBreakdown({ + payload: new EndpointStatusBreakdownRequest({ + serviceName: input.serviceName, + spanName: input.spanName, + startTime: input.startTime, + endTime: input.endTime, + environments: input.environments, + }), + }) + }), + ) + + // Ordered here rather than in SQL so the chart's stack order is a UI decision + // (2xx first, degrading to 5xx) instead of a lexical accident. + const rank = (statusClass: string) => { + const index = STATUS_CLASS_ORDER.indexOf(statusClass as (typeof STATUS_CLASS_ORDER)[number]) + return index === -1 ? STATUS_CLASS_ORDER.length : index + } + const slices: EndpointStatusSlice[] = result.data + .map((row) => ({ + statusClass: row.statusClass, + spanCount: row.spanCount, + estimatedSpanCount: row.estimatedSpanCount, + })) + .toSorted((a, b) => rank(a.statusClass) - rank(b.statusClass)) + + return { slices } +}) diff --git a/apps/web/src/api/warehouse/service-endpoints.ts b/apps/web/src/api/warehouse/service-endpoints.ts new file mode 100644 index 000000000..8a9caeed1 --- /dev/null +++ b/apps/web/src/api/warehouse/service-endpoints.ts @@ -0,0 +1,79 @@ +import { Effect, Schema } from "effect" +import { DeploymentEnvironment, ServiceName, ServiceEndpointsRequest } from "@maple/domain/http" +import { MapleInternalAtomClient } from "@/lib/services/common/internal-atom-client" +import { WarehouseDateTimeString, decodeInput, runWarehouseQuery } from "@/api/warehouse/effect-utils" +import type { ServiceOperationSparklinePoint } from "@/api/warehouse/service-operations" + +export interface ServiceEndpoint { + /** + * Display span name ("GET /api/users"). This — not `route` — is the value the + * /traces `spanNames` filter matches, so keep it whole when linking out. + */ + spanName: string + method: string + route: string + spanCount: number + estimatedSpanCount: number + errorCount: number + estimatedErrorCount: number + /** 0–1 ratio, sampling-weighted. ×100 only at display. */ + errorRate: number + avgDurationMs: number + p50DurationMs: number + p95DurationMs: number + p99DurationMs: number + sparkline: ServiceOperationSparklinePoint[] +} + +const GetServiceEndpointsInput = Schema.Struct({ + serviceName: ServiceName, + startTime: WarehouseDateTimeString, + endTime: WarehouseDateTimeString, + environments: Schema.optional(Schema.Array(DeploymentEnvironment)), + bucketSeconds: Schema.optional(Schema.Number), + limit: Schema.optional(Schema.Number), +}) + +export type GetServiceEndpointsInput = (typeof GetServiceEndpointsInput)["Encoded"] + +export const getServiceEndpoints = Effect.fn("QueryEngine.getServiceEndpoints")(function* ({ + data, +}: { + data: GetServiceEndpointsInput +}) { + const input = yield* decodeInput(GetServiceEndpointsInput, data, "getServiceEndpoints") + + const result = yield* runWarehouseQuery("serviceEndpoints", () => + Effect.gen(function* () { + const client = yield* MapleInternalAtomClient + return yield* client.queryEngine.serviceEndpoints({ + payload: new ServiceEndpointsRequest({ + serviceName: input.serviceName, + startTime: input.startTime, + endTime: input.endTime, + environments: input.environments, + bucketSeconds: input.bucketSeconds, + limit: input.limit, + }), + }) + }), + ) + + const endpoints: ServiceEndpoint[] = result.data.map((row) => ({ + spanName: row.spanName, + method: row.method, + route: row.route, + spanCount: row.spanCount, + estimatedSpanCount: row.estimatedSpanCount, + errorCount: row.errorCount, + estimatedErrorCount: row.estimatedErrorCount, + errorRate: row.errorRate, + avgDurationMs: row.avgDurationMs, + p50DurationMs: row.p50DurationMs, + p95DurationMs: row.p95DurationMs, + p99DurationMs: row.p99DurationMs, + sparkline: row.sparkline.map((point) => ({ bucket: point.bucket, count: point.count })), + })) + + return { endpoints } +}) diff --git a/apps/web/src/components/services/service-endpoints-tab.tsx b/apps/web/src/components/services/service-endpoints-tab.tsx new file mode 100644 index 000000000..0b7f05623 --- /dev/null +++ b/apps/web/src/components/services/service-endpoints-tab.tsx @@ -0,0 +1,511 @@ +import { useMemo, useState } from "react" +import { useNavigate } from "@tanstack/react-router" +import { cn } from "@maple/ui/lib/utils" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@maple/ui/components/ui/table" +import { Skeleton } from "@maple/ui/components/ui/skeleton" +import { Sparkline } from "@maple/ui/components/ui/gradient-chart" +import { LatencyValue } from "@maple/ui/components/latency-value" +import { ChevronDownIcon, ChevronUpIcon, ChevronExpandYIcon } from "@/components/icons" +import { Result } from "@/lib/effect-atom" +import { useRetainedRefreshableResultValue } from "@/hooks/use-retained-refreshable-result-value" +import { getServiceEndpointsResultAtom } from "@/lib/services/atoms/warehouse-query-atoms" +import { QueryErrorState } from "@/components/common/query-error-state" +import type { ServiceEndpoint } from "@/api/warehouse/service-endpoints" +import { + endpointDetailSearch, + methodTone, + serviceEndpointsQueryInput, + splitRouteForDisplay, +} from "./service-endpoints" +import { callsPerSecond, windowSeconds } from "./service-operations" + +interface ServiceEndpointsTabProps { + serviceName: string + effectiveStartTime: string + effectiveEndTime: string + environments?: string[] + /** Raw search params, forwarded to the detail route so relative presets stay live. */ + startTime?: string + endTime?: string + timePreset?: string +} + +type SortKey = "calls" | "errorRate" | "p50" | "p95" | "p99" +type SortDir = "asc" | "desc" + +function formatRate(value: number): string { + if (value >= 1000) return `${(value / 1000).toFixed(1)}k` + if (value >= 1) return value.toFixed(1) + return value.toFixed(2) +} + +function formatErrorRate(rate: number): string { + if (rate >= 0.01) return `${(rate * 100).toFixed(1)}%` + if (rate > 0) return "<1%" + return "0%" +} + +function errorTone(rate: number): "error" | "warn" | "default" { + if (rate > 0.05) return "error" + if (rate > 0.01) return "warn" + return "default" +} + +const sortValue = (endpoint: ServiceEndpoint, key: SortKey): number => { + switch (key) { + case "calls": + return endpoint.estimatedSpanCount + case "errorRate": + return endpoint.errorRate + case "p50": + return endpoint.p50DurationMs + case "p95": + return endpoint.p95DurationMs + case "p99": + return endpoint.p99DurationMs + } +} + +export function ServiceEndpointsTab({ + serviceName, + effectiveStartTime, + effectiveEndTime, + environments, + startTime, + endTime, + timePreset, +}: ServiceEndpointsTabProps) { + const navigate = useNavigate() + const [sortKey, setSortKey] = useState("calls") + const [sortDir, setSortDir] = useState("desc") + + const result = useRetainedRefreshableResultValue( + getServiceEndpointsResultAtom({ + data: serviceEndpointsQueryInput({ + serviceName, + effectiveStartTime, + effectiveEndTime, + environments, + }), + }), + ) + + const seconds = windowSeconds(effectiveStartTime, effectiveEndTime) + + const endpoints = useMemo( + () => + Result.builder(result) + .onSuccess((r) => [...r.endpoints]) + .orElse(() => []), + [result], + ) + + const sorted = useMemo(() => { + return endpoints.toSorted((a, b) => { + const diff = sortValue(b, sortKey) - sortValue(a, sortKey) + return sortDir === "desc" ? diff : -diff + }) + }, [endpoints, sortKey, sortDir]) + + // Column-relative maxima drive the inline throughput/latency bars, mirroring + // the Operations and Dependencies tables so all three read as one system. + const maxima = useMemo( + () => + endpoints.reduce( + (acc, endpoint) => ({ + calls: Math.max(acc.calls, endpoint.estimatedSpanCount), + p95: Math.max(acc.p95, endpoint.p95DurationMs), + }), + { calls: 0, p95: 0 }, + ), + [endpoints], + ) + + const toggleSort = (key: SortKey) => { + if (key === sortKey) { + setSortDir(sortDir === "desc" ? "asc" : "desc") + } else { + setSortKey(key) + setSortDir("desc") + } + } + + const handleRowClick = (endpoint: ServiceEndpoint) => { + navigate({ + to: "/services/$serviceName/endpoints", + params: { serviceName }, + search: endpointDetailSearch({ + method: endpoint.method, + route: endpoint.route, + environments, + startTime, + endTime, + timePreset, + }), + }) + } + + if (!Result.isSuccess(result)) { + return Result.builder(result) + .onError((error) => ) + .orElse(() => ) + } + + const isWaiting = Result.isSuccess(result) && result.waiting + + return ( +
+ {/* Desktop: dense sortable table with inline distribution bars. */} +
+ + + + + Endpoint + + toggleSort("calls")} + /> + toggleSort("errorRate")} + /> + toggleSort("p50")} + /> + toggleSort("p95")} + /> + toggleSort("p99")} + className="hidden xl:table-cell" + /> + + Activity + + + + + {sorted.length === 0 ? ( + + + No endpoints recorded in this window. + + + ) : ( + sorted.map((endpoint) => { + const tone = errorTone(endpoint.errorRate) + return ( + handleRowClick(endpoint)} + className="cursor-pointer group/row border-b last:border-b-0 hover:bg-muted/40" + > + {/* w-full + max-w-0: this cell absorbs every pixel the + content-sized numeric columns don't need. */} + +
+ + +
+
+ + + {endpoint.estimatedSpanCount > endpoint.spanCount ? "~" : ""} + {formatRate( + callsPerSecond(endpoint.estimatedSpanCount, seconds), + )} + + + 0 ? endpoint.errorRate : 0} + // Fixed severity scale (5% = full bar), matching the + // Operations tab — a 0.2% sliver stays a sliver. + max={0.05} + tone="errors" + > + + {formatErrorRate(endpoint.errorRate)} + + + + + + + + + {/* p99 is the first thing to go on a narrow viewport — + p95 already carries the tail signal. */} + + + + + ({ + value: point.count, + }))} + className="ml-auto h-6 w-[88px]" + /> + +
+ ) + }) + )} +
+
+
+ + {/* Mobile: tap-to-detail list with a compact sort control. */} +
+
+ Sort + {( + [ + ["calls", "Req"], + ["errorRate", "Errors"], + ["p95", "p95"], + ] as const + ).map(([key, label]) => { + const active = sortKey === key + const Icon = active + ? sortDir === "desc" + ? ChevronDownIcon + : ChevronUpIcon + : ChevronExpandYIcon + return ( + + ) + })} +
+
+ {sorted.length === 0 ? ( +
+ No endpoints recorded in this window. +
+ ) : ( + sorted.map((endpoint) => { + const tone = errorTone(endpoint.errorRate) + return ( + + ) + }) + )} +
+
+
+ ) +} + +/** + * A route that gives up its middle, not its end. `head` shrinks and truncates; + * `tail` (the last path segment) is fixed, so `/subscriptions/v2/{id}/cancel` + * degrades to `/subscriptions/v2…/cancel` rather than `/subscriptions…`. + */ +export function RouteLabel({ route, className }: { route: string; className?: string }) { + const { head, tail } = splitRouteForDisplay(route) + return ( + + {head ? {head} : null} + {/* Capped so a single enormous segment can't push the head to zero. */} + {tail} + + ) +} + +export function MethodBadge({ method, className }: { method: string; className?: string }) { + return ( + + {method || "—"} + + ) +} + +function EndpointsLoadingState() { + return ( +
+ {Array.from({ length: 10 }).map((_, i) => ( +
+ + + + + + +
+ ))} +
+ ) +} + +interface BarCellProps { + value: number + max: number + tone: "calls" | "errors" | "latency" + children: React.ReactNode +} + +/** Numeric cell with a column-tinted distribution bar — same treatment as the + * Operations and Dependencies tabs so the tables read identically. */ +function BarCell({ value, max, tone, children }: BarCellProps) { + const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0 + const hasBar = pct > 0 + return ( + + {hasBar ? ( +
+ ) : null} + {children} + + ) +} + +interface SortableHeadProps { + label: string + align?: "left" | "right" + active: boolean + dir: SortDir + onClick: () => void + /** Extra classes, e.g. the responsive hide that pairs with its cell. */ + className?: string +} + +function SortableHead({ label, align = "left", active, dir, onClick, className }: SortableHeadProps) { + const Icon = active ? (dir === "desc" ? ChevronDownIcon : ChevronUpIcon) : ChevronExpandYIcon + return ( + + + {label} + + + + ) +} diff --git a/apps/web/src/components/services/service-endpoints.test.ts b/apps/web/src/components/services/service-endpoints.test.ts new file mode 100644 index 000000000..8c7fac6bc --- /dev/null +++ b/apps/web/src/components/services/service-endpoints.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest" +import { OPERATIONS_LIMIT } from "./service-operations" +import { + endpointDetailSearch, + endpointSpanName, + methodTone, + serviceEndpointsQueryInput, + splitRouteForDisplay, +} from "./service-endpoints" + +describe("serviceEndpointsQueryInput", () => { + const args = { + serviceName: "api", + effectiveStartTime: "2026-08-14 00:00:00", + effectiveEndTime: "2026-08-14 12:00:00", + } + + it("matches the operations query's bucket sizing and limit", () => { + // The two tables sit side by side; divergent sizing would read as a data bug. + const input = serviceEndpointsQueryInput(args) + expect(input.limit).toBe(OPERATIONS_LIMIT) + // 12h / 50 = 864s, rounded to the nearest whole minute (the rollup's grain). + expect(input.bucketSeconds).toBe(840) + expect(input.bucketSeconds! % 60).toBe(0) + }) + + it("omits an empty environment filter so the key stays stable", () => { + expect(serviceEndpointsQueryInput({ ...args, environments: [] }).environments).toBeUndefined() + expect(serviceEndpointsQueryInput({ ...args, environments: ["prod"] }).environments).toEqual(["prod"]) + }) +}) + +describe("endpointSpanName", () => { + it("recomposes the display name the warehouse keys on", () => { + expect(endpointSpanName("GET", "/v1/users")).toBe("GET /v1/users") + }) + + it("round-trips a route containing path parameters and slashes", () => { + const route = "/v1/orgs/{orgId}/users/{userId}" + expect(endpointSpanName("DELETE", route)).toBe(`DELETE ${route}`) + }) +}) + +describe("splitRouteForDisplay", () => { + it("keeps the last segment as the fixed tail", () => { + // The shared prefix is what a table of sibling routes can afford to lose. + expect(splitRouteForDisplay("/subscriptions/v2/{id}/cancel")).toEqual({ + head: "/subscriptions/v2/{id}", + tail: "/cancel", + }) + }) + + it("treats a single-segment route as all tail, so nothing truncates", () => { + expect(splitRouteForDisplay("/lander")).toEqual({ head: "", tail: "/lander" }) + }) + + it("handles a route with no leading slash", () => { + expect(splitRouteForDisplay("health")).toEqual({ head: "", tail: "health" }) + }) + + it("handles a trailing slash without emitting an empty tail-only split", () => { + expect(splitRouteForDisplay("/api/v1/")).toEqual({ head: "/api/v1", tail: "/" }) + }) + + it("round-trips: head + tail is always the original route", () => { + for (const route of ["/a/b/c", "/lander", "/", "", "/api/match", "no-slash"]) { + const { head, tail } = splitRouteForDisplay(route) + expect(head + tail).toBe(route) + } + }) +}) + +describe("methodTone", () => { + it("groups verbs by effect, not by verb", () => { + expect(methodTone("GET")).toBe(methodTone("HEAD")) + expect(methodTone("POST")).toBe(methodTone("PATCH")) + expect(methodTone("DELETE")).not.toBe(methodTone("POST")) + expect(methodTone("get")).toBe(methodTone("GET")) + }) + + it("falls back for an unrecognized or empty method", () => { + expect(methodTone("")).toContain("muted") + expect(methodTone("TRACE")).toContain("muted") + }) +}) + +describe("endpointDetailSearch", () => { + it("drops an empty environment list rather than serializing it", () => { + expect( + endpointDetailSearch({ method: "GET", route: "/x", environments: [] }).environments, + ).toBeUndefined() + }) +}) diff --git a/apps/web/src/components/services/service-endpoints.ts b/apps/web/src/components/services/service-endpoints.ts new file mode 100644 index 000000000..1d369e23b --- /dev/null +++ b/apps/web/src/components/services/service-endpoints.ts @@ -0,0 +1,95 @@ +import type { GetServiceEndpointsInput } from "@/api/warehouse/service-endpoints" +import { OPERATIONS_LIMIT, operationsBucketSeconds } from "./service-operations" + +/** + * The shared atom-family input for the endpoints query. The Overview panel and + * the Endpoints tab both build their key through this, so opening the tab after + * seeing the panel is a cache hit — the same trick `serviceOperationsQueryInput` + * plays for Operations. + * + * Bucket sizing and limit are deliberately borrowed from the operations helpers + * rather than re-derived: the two tables sit side by side and a divergence would + * read as a data bug. + */ +export function serviceEndpointsQueryInput(args: { + serviceName: string + effectiveStartTime: string + effectiveEndTime: string + environments?: readonly string[] +}): GetServiceEndpointsInput { + return { + serviceName: args.serviceName, + startTime: args.effectiveStartTime, + endTime: args.effectiveEndTime, + environments: args.environments?.length ? args.environments : undefined, + bucketSeconds: operationsBucketSeconds(args.effectiveStartTime, args.effectiveEndTime), + limit: OPERATIONS_LIMIT, + } +} + +/** + * Tailwind classes per HTTP verb, keyed on read/write/destroy rather than on a + * per-verb palette: the useful signal when scanning a route table is "does this + * change anything", not "which of the seven verbs is it". + */ +export function methodTone(method: string): string { + switch (method.toUpperCase()) { + case "GET": + case "HEAD": + case "OPTIONS": + return "text-severity-info/90 bg-severity-info/10" + case "POST": + case "PUT": + case "PATCH": + return "text-severity-warn/90 bg-severity-warn/10" + case "DELETE": + return "text-severity-error/90 bg-severity-error/10" + default: + return "text-muted-foreground bg-muted" + } +} + +/** + * Split a route so the distinguishing end of it survives truncation. + * + * Endpoints on one service overwhelmingly share a prefix — a table of + * `/subscriptions/v2/{id}/cancel`, `/subscriptions/v2/{id}/refund`, + * `/subscriptions/v2/search` end-truncated to `/subscriptions…` is fifteen + * identical rows. Rendering `head` as the shrinking part and `tail` as a + * fixed one elides the middle instead, so the last segment is always readable. + */ +export function splitRouteForDisplay(route: string): { head: string; tail: string } { + const lastSlash = route.lastIndexOf("/") + // No separator, or the only slash is the leading one: there is no shared + // prefix to give up, so the whole route is the tail. + if (lastSlash <= 0) return { head: "", tail: route } + return { head: route.slice(0, lastSlash), tail: route.slice(lastSlash) } +} + +/** Search params for the endpoint detail route. */ +export function endpointDetailSearch(args: { + method: string + route: string + environments?: readonly string[] + startTime?: string + endTime?: string + timePreset?: string +}) { + return { + method: args.method, + route: args.route, + environments: args.environments?.length ? [...args.environments] : undefined, + startTime: args.startTime, + endTime: args.endTime, + timePreset: args.timePreset, + } +} + +/** + * The display span name the warehouse keys on, recomposed from the split halves + * the detail route carries in its URL. Every query filter on that route goes + * through this — `route` alone matches nothing. + */ +export function endpointSpanName(method: string, route: string): string { + return `${method} ${route}` +} diff --git a/apps/web/src/components/services/service-top-operations-panel.tsx b/apps/web/src/components/services/service-top-operations-panel.tsx index 8803089be..993618014 100644 --- a/apps/web/src/components/services/service-top-operations-panel.tsx +++ b/apps/web/src/components/services/service-top-operations-panel.tsx @@ -1,22 +1,28 @@ -import { useMemo } from "react" +import { useMemo, type ReactNode } from "react" +import { useNavigate } from "@tanstack/react-router" import { cn } from "@maple/ui/lib/utils" import { Sparkline } from "@maple/ui/components/ui/gradient-chart" import { Result } from "@/lib/effect-atom" import { useRetainedRefreshableResultValue } from "@/hooks/use-retained-refreshable-result-value" -import { getServiceOperationsResultAtom } from "@/lib/services/atoms/warehouse-query-atoms" +import { + getServiceEndpointsResultAtom, + getServiceOperationsResultAtom, +} from "@/lib/services/atoms/warehouse-query-atoms" import { LatencyValue } from "@maple/ui/components/latency-value" import type { ServiceOperation } from "@/api/warehouse/service-operations" import { SectionCard } from "./section-card" import { callsPerSecond, serviceOperationsQueryInput, windowSeconds } from "./service-operations" +import { endpointDetailSearch, serviceEndpointsQueryInput } from "./service-endpoints" +import { MethodBadge, RouteLabel } from "./service-endpoints-tab" const PANEL_LIMIT = 5 -interface ServiceTopOperationsPanelProps { +interface ServiceTopPanelProps { serviceName: string effectiveStartTime: string effectiveEndTime: string environments?: string[] - /** Switches the page to the Operations tab (URL-driven). */ + /** Switches the page to the corresponding tab (URL-driven). */ onViewAll: () => void } @@ -32,6 +38,23 @@ function formatErrorRate(rate: number): string { return "0%" } +/** + * One row of the digest, reduced to what the panel actually renders. Both the + * operation and endpoint shapes narrow to this, so the two variants below share + * a single presentation instead of a second copy of the row markup. + */ +interface TopSpanRow { + key: string + label: ReactNode + title: string + estimatedSpanCount: number + spanCount: number + errorRate: number + p95DurationMs: number + sparkline: ReadonlyArray<{ count: number }> + onSelect: () => void +} + /** * "Top operations" digest on the Overview tab: the service's busiest span names * with rate/error/p95 at a glance. Reads the same atom key the Operations tab @@ -44,7 +67,7 @@ export function ServiceTopOperationsPanel({ effectiveEndTime, environments, onViewAll, -}: ServiceTopOperationsPanelProps) { +}: ServiceTopPanelProps) { const result = useRetainedRefreshableResultValue( getServiceOperationsResultAtom({ data: serviceOperationsQueryInput({ @@ -56,24 +79,156 @@ export function ServiceTopOperationsPanel({ }), ) - const operations = useMemo( + const rows = useMemo( + () => + Result.builder(result) + .onSuccess((r) => + r.operations.slice(0, PANEL_LIMIT).map( + (op: ServiceOperation): TopSpanRow => ({ + key: op.spanName, + label: ( + + {op.spanName} + + ), + title: `${op.spanName} — see Operations tab`, + estimatedSpanCount: op.estimatedSpanCount, + spanCount: op.spanCount, + errorRate: op.errorRate, + p95DurationMs: op.p95DurationMs, + sparkline: op.sparkline, + onSelect: onViewAll, + }), + ), + ) + .orElse(() => []), + [result, onViewAll], + ) + + return ( + + ) +} + +interface ServiceTopEndpointsPanelProps extends ServiceTopPanelProps { + /** Raw search params, forwarded to the detail route so relative presets stay live. */ + startTime?: string + endTime?: string + timePreset?: string +} + +/** + * The Endpoints variant, shown on Overview in place of Top operations when the + * service is detected as an HTTP API. Reads the same atom key the Endpoints tab + * uses, and rows drill straight into the endpoint detail route rather than only + * switching tabs — an endpoint is a place you can go, an operation isn't. + */ +export function ServiceTopEndpointsPanel({ + serviceName, + effectiveStartTime, + effectiveEndTime, + environments, + startTime, + endTime, + timePreset, + onViewAll, +}: ServiceTopEndpointsPanelProps) { + const navigate = useNavigate() + const result = useRetainedRefreshableResultValue( + getServiceEndpointsResultAtom({ + data: serviceEndpointsQueryInput({ + serviceName, + effectiveStartTime, + effectiveEndTime, + environments, + }), + }), + ) + + const rows = useMemo( () => Result.builder(result) - .onSuccess((r) => r.operations.slice(0, PANEL_LIMIT)) + .onSuccess((r) => + r.endpoints.slice(0, PANEL_LIMIT).map( + (endpoint): TopSpanRow => ({ + key: endpoint.spanName, + label: ( + + + + + ), + title: `${endpoint.spanName} — open endpoint`, + estimatedSpanCount: endpoint.estimatedSpanCount, + spanCount: endpoint.spanCount, + errorRate: endpoint.errorRate, + p95DurationMs: endpoint.p95DurationMs, + sparkline: endpoint.sparkline, + onSelect: () => + navigate({ + to: "/services/$serviceName/endpoints", + params: { serviceName }, + search: endpointDetailSearch({ + method: endpoint.method, + route: endpoint.route, + environments, + startTime, + endTime, + timePreset, + }), + }), + }), + ), + ) .orElse(() => []), - [result], + [result, navigate, serviceName, environments, startTime, endTime, timePreset], ) - if (operations.length === 0) return null + return ( + + ) +} + +interface TopSpansPanelProps { + title: string + rows: ReadonlyArray + waiting: boolean + effectiveStartTime: string + effectiveEndTime: string + onViewAll: () => void +} + +function TopSpansPanel({ + title, + rows, + waiting, + effectiveStartTime, + effectiveEndTime, + onViewAll, +}: TopSpansPanelProps) { + if (rows.length === 0) return null const seconds = windowSeconds(effectiveStartTime, effectiveEndTime) - const isWaiting = Result.isSuccess(result) && result.waiting - const maxCalls = operations.reduce((acc, op) => Math.max(acc, op.estimatedSpanCount), 0) + const maxCalls = rows.reduce((acc, row) => Math.max(acc, row.estimatedSpanCount), 0) return (
    - {operations.map((op) => { - const barPct = maxCalls > 0 ? Math.min((op.estimatedSpanCount / maxCalls) * 100, 100) : 0 + {rows.map((row) => { + const barPct = maxCalls > 0 ? Math.min((row.estimatedSpanCount / maxCalls) * 100, 100) : 0 return ( -
  • +
  • diff --git a/apps/web/src/lib/services/atoms/warehouse-query-atoms.ts b/apps/web/src/lib/services/atoms/warehouse-query-atoms.ts index a811130f4..9a1bf962d 100644 --- a/apps/web/src/lib/services/atoms/warehouse-query-atoms.ts +++ b/apps/web/src/lib/services/atoms/warehouse-query-atoms.ts @@ -10,6 +10,7 @@ import { getCustomChartTimeSeries, getOverviewThroughputRefinement, getOverviewTimeSeries, + getCustomChartServiceDetail, getServiceDetailOverview, getServiceDetailThroughputRefinement, } from "@/api/warehouse/custom-charts" @@ -54,6 +55,8 @@ import { workloadInfraTimeseries, } from "@/api/warehouse/infra" import { getServiceUsage } from "@/api/warehouse/service-usage" +import { getEndpointStatusBreakdown } from "@/api/warehouse/endpoint-status" +import { getServiceEndpoints } from "@/api/warehouse/service-endpoints" import { getServiceOperations } from "@/api/warehouse/service-operations" import { getServiceDependenciesBundle, @@ -194,6 +197,22 @@ export const getServiceOperationsResultAtom = makeQueryAtomFamily(getServiceOper staleTime: 30_000, }) +// Same staleTime and same input builder as the operations atom, so the Overview +// panel and the Endpoints tab share one round-trip (see serviceEndpointsQueryInput). +export const getServiceEndpointsResultAtom = makeQueryAtomFamily(getServiceEndpoints, { + staleTime: 30_000, +}) + +// Endpoint detail: the four service charts narrowed to one span name, and the +// status-class split beside them. +export const getEndpointDetailChartsResultAtom = makeQueryAtomFamily(getCustomChartServiceDetail, { + staleTime: 30_000, +}) + +export const getEndpointStatusBreakdownResultAtom = makeQueryAtomFamily(getEndpointStatusBreakdown, { + staleTime: 30_000, +}) + export const getServicesFacetsResultAtom = makeQueryAtomFamily(getServicesFacets, { // 5 min idle TTL — environments / commit SHAs / service names move slowly, // and the dashboard route now reuses this atom for demo-detection (was a diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 112b1737f..8e2a9f1b4 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -8,1420 +8,1454 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { Route as rootRouteImport } from "./routes/__root" -import { Route as IndexRouteImport } from "./routes/index" -import { Route as AccountRouteImport } from "./routes/account" -import { Route as ChatRouteImport } from "./routes/chat" -import { Route as CliLoginRouteImport } from "./routes/cli-login" -import { Route as ConnectorsRouteImport } from "./routes/connectors" -import { Route as DeveloperRouteImport } from "./routes/developer" -import { Route as FlowLabRouteImport } from "./routes/flow-lab" -import { Route as InfraBenchRouteImport } from "./routes/infra-bench" -import { Route as IntegrationsRouteImport } from "./routes/integrations" -import { Route as LogsBenchRouteImport } from "./routes/logs-bench" -import { Route as McpRouteImport } from "./routes/mcp" -import { Route as McpAuthorizeRouteImport } from "./routes/mcp-authorize" -import { Route as NodeLabRouteImport } from "./routes/node-lab" -import { Route as OrgRequiredRouteImport } from "./routes/org-required" -import { Route as OverviewBenchRouteImport } from "./routes/overview-bench" -import { Route as QueryBuilderLabRouteImport } from "./routes/query-builder-lab" -import { Route as QuickStartRouteImport } from "./routes/quick-start" -import { Route as SelectPlanRouteImport } from "./routes/select-plan" -import { Route as ServiceDetailBenchRouteImport } from "./routes/service-detail-bench" -import { Route as ServiceMapRouteImport } from "./routes/service-map" -import { Route as ServiceMapBenchRouteImport } from "./routes/service-map-bench" -import { Route as SettingsRouteImport } from "./routes/settings" -import { Route as SignInRouteImport } from "./routes/sign-in" -import { Route as SignUpRouteImport } from "./routes/sign-up" -import { Route as TimelineLabRouteImport } from "./routes/timeline-lab" -import { Route as WidgetLabRouteImport } from "./routes/widget-lab" -import { Route as AlertsIndexRouteImport } from "./routes/alerts/index" -import { Route as AlertsRuleIdRouteImport } from "./routes/alerts/$ruleId" -import { Route as AlertsCreateRouteImport } from "./routes/alerts/create" -import { Route as AnalyticsIndexRouteImport } from "./routes/analytics/index" -import { Route as AnomaliesIndexRouteImport } from "./routes/anomalies/index" -import { Route as AnomaliesIncidentIdRouteImport } from "./routes/anomalies/$incidentId" -import { Route as DashboardsIndexRouteImport } from "./routes/dashboards/index" -import { Route as DashboardsDashboardIdRouteImport } from "./routes/dashboards/$dashboardId" -import { Route as DashboardsTemplatesRouteImport } from "./routes/dashboards/templates" -import { Route as ErrorsIndexRouteImport } from "./routes/errors/index" -import { Route as ErrorsErrorTypeRouteImport } from "./routes/errors/$errorType" -import { Route as InfraIndexRouteImport } from "./routes/infra/index" -import { Route as InfraHostNameRouteImport } from "./routes/infra/$hostName" -import { Route as InvestigationsIndexRouteImport } from "./routes/investigations/index" -import { Route as InvestigationsIdRouteImport } from "./routes/investigations/$id" -import { Route as LogsIndexRouteImport } from "./routes/logs/index" -import { Route as LogsLogIdRouteImport } from "./routes/logs/$logId" -import { Route as MetricsIndexRouteImport } from "./routes/metrics/index" -import { Route as MetricsMetricNameRouteImport } from "./routes/metrics/$metricName" -import { Route as RecommendationsRecommendationKeyRouteImport } from "./routes/recommendations/$recommendationKey" -import { Route as ReplaysIndexRouteImport } from "./routes/replays/index" -import { Route as ReplaysSessionIdRouteImport } from "./routes/replays/$sessionId" -import { Route as ServicesIndexRouteImport } from "./routes/services/index" -import { Route as ServicesServiceNameRouteImport } from "./routes/services/$serviceName" -import { Route as TracesIndexRouteImport } from "./routes/traces/index" -import { Route as TracesTraceIdRouteImport } from "./routes/traces/$traceId" -import { Route as AlertsIncidentsIncidentIdRouteImport } from "./routes/alerts/incidents/$incidentId" -import { Route as ErrorsIssuesIndexRouteImport } from "./routes/errors/issues/index" -import { Route as ErrorsIssuesIssueIdRouteImport } from "./routes/errors/issues/$issueId" -import { Route as InfraCloudflareIndexRouteImport } from "./routes/infra/cloudflare/index" -import { Route as InfraCloudflareZoneNameRouteImport } from "./routes/infra/cloudflare/$zoneName" -import { Route as InfraPlanetscaleIndexRouteImport } from "./routes/infra/planetscale/index" -import { Route as InfraPlanetscaleDbNameRouteImport } from "./routes/infra/planetscale/$dbName" -import { Route as DashboardsDashboardIdWidgetsWidgetIdRouteImport } from "./routes/dashboards/$dashboardId_.widgets.$widgetId" -import { Route as InfraKubernetesNodesIndexRouteImport } from "./routes/infra/kubernetes/nodes/index" -import { Route as InfraKubernetesNodesNodeNameRouteImport } from "./routes/infra/kubernetes/nodes/$nodeName" -import { Route as InfraKubernetesPodsIndexRouteImport } from "./routes/infra/kubernetes/pods/index" -import { Route as InfraKubernetesPodsPodNameRouteImport } from "./routes/infra/kubernetes/pods/$podName" -import { Route as InfraKubernetesWorkloadsIndexRouteImport } from "./routes/infra/kubernetes/workloads/index" -import { Route as InfraKubernetesWorkloadsKindWorkloadNameRouteImport } from "./routes/infra/kubernetes/workloads/$kind/$workloadName" +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as AccountRouteImport } from './routes/account' +import { Route as ChatRouteImport } from './routes/chat' +import { Route as CliLoginRouteImport } from './routes/cli-login' +import { Route as ConnectorsRouteImport } from './routes/connectors' +import { Route as DeveloperRouteImport } from './routes/developer' +import { Route as FlowLabRouteImport } from './routes/flow-lab' +import { Route as InfraBenchRouteImport } from './routes/infra-bench' +import { Route as IntegrationsRouteImport } from './routes/integrations' +import { Route as LogsBenchRouteImport } from './routes/logs-bench' +import { Route as McpRouteImport } from './routes/mcp' +import { Route as McpAuthorizeRouteImport } from './routes/mcp-authorize' +import { Route as NodeLabRouteImport } from './routes/node-lab' +import { Route as OrgRequiredRouteImport } from './routes/org-required' +import { Route as OverviewBenchRouteImport } from './routes/overview-bench' +import { Route as QueryBuilderLabRouteImport } from './routes/query-builder-lab' +import { Route as QuickStartRouteImport } from './routes/quick-start' +import { Route as SelectPlanRouteImport } from './routes/select-plan' +import { Route as ServiceDetailBenchRouteImport } from './routes/service-detail-bench' +import { Route as ServiceMapRouteImport } from './routes/service-map' +import { Route as ServiceMapBenchRouteImport } from './routes/service-map-bench' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as SignInRouteImport } from './routes/sign-in' +import { Route as SignUpRouteImport } from './routes/sign-up' +import { Route as TimelineLabRouteImport } from './routes/timeline-lab' +import { Route as WidgetLabRouteImport } from './routes/widget-lab' +import { Route as AlertsIndexRouteImport } from './routes/alerts/index' +import { Route as AlertsRuleIdRouteImport } from './routes/alerts/$ruleId' +import { Route as AlertsCreateRouteImport } from './routes/alerts/create' +import { Route as AnalyticsIndexRouteImport } from './routes/analytics/index' +import { Route as AnomaliesIndexRouteImport } from './routes/anomalies/index' +import { Route as AnomaliesIncidentIdRouteImport } from './routes/anomalies/$incidentId' +import { Route as DashboardsIndexRouteImport } from './routes/dashboards/index' +import { Route as DashboardsDashboardIdRouteImport } from './routes/dashboards/$dashboardId' +import { Route as DashboardsTemplatesRouteImport } from './routes/dashboards/templates' +import { Route as ErrorsIndexRouteImport } from './routes/errors/index' +import { Route as ErrorsErrorTypeRouteImport } from './routes/errors/$errorType' +import { Route as InfraIndexRouteImport } from './routes/infra/index' +import { Route as InfraHostNameRouteImport } from './routes/infra/$hostName' +import { Route as InvestigationsIndexRouteImport } from './routes/investigations/index' +import { Route as InvestigationsIdRouteImport } from './routes/investigations/$id' +import { Route as LogsIndexRouteImport } from './routes/logs/index' +import { Route as LogsLogIdRouteImport } from './routes/logs/$logId' +import { Route as MetricsIndexRouteImport } from './routes/metrics/index' +import { Route as MetricsMetricNameRouteImport } from './routes/metrics/$metricName' +import { Route as RecommendationsRecommendationKeyRouteImport } from './routes/recommendations/$recommendationKey' +import { Route as ReplaysIndexRouteImport } from './routes/replays/index' +import { Route as ReplaysSessionIdRouteImport } from './routes/replays/$sessionId' +import { Route as ServicesIndexRouteImport } from './routes/services/index' +import { Route as ServicesServiceNameRouteImport } from './routes/services/$serviceName' +import { Route as TracesIndexRouteImport } from './routes/traces/index' +import { Route as TracesTraceIdRouteImport } from './routes/traces/$traceId' +import { Route as AlertsIncidentsIncidentIdRouteImport } from './routes/alerts/incidents/$incidentId' +import { Route as ErrorsIssuesIndexRouteImport } from './routes/errors/issues/index' +import { Route as ErrorsIssuesIssueIdRouteImport } from './routes/errors/issues/$issueId' +import { Route as InfraCloudflareIndexRouteImport } from './routes/infra/cloudflare/index' +import { Route as InfraCloudflareZoneNameRouteImport } from './routes/infra/cloudflare/$zoneName' +import { Route as InfraPlanetscaleIndexRouteImport } from './routes/infra/planetscale/index' +import { Route as InfraPlanetscaleDbNameRouteImport } from './routes/infra/planetscale/$dbName' +import { Route as ServicesServiceNameEndpointsRouteImport } from './routes/services/$serviceName_.endpoints' +import { Route as DashboardsDashboardIdWidgetsWidgetIdRouteImport } from './routes/dashboards/$dashboardId_.widgets.$widgetId' +import { Route as InfraKubernetesNodesIndexRouteImport } from './routes/infra/kubernetes/nodes/index' +import { Route as InfraKubernetesNodesNodeNameRouteImport } from './routes/infra/kubernetes/nodes/$nodeName' +import { Route as InfraKubernetesPodsIndexRouteImport } from './routes/infra/kubernetes/pods/index' +import { Route as InfraKubernetesPodsPodNameRouteImport } from './routes/infra/kubernetes/pods/$podName' +import { Route as InfraKubernetesWorkloadsIndexRouteImport } from './routes/infra/kubernetes/workloads/index' +import { Route as InfraKubernetesWorkloadsKindWorkloadNameRouteImport } from './routes/infra/kubernetes/workloads/$kind/$workloadName' const IndexRoute = IndexRouteImport.update({ - id: "/", - path: "/", - getParentRoute: () => rootRouteImport, + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, } as any) const AccountRoute = AccountRouteImport.update({ - id: "/account", - path: "/account", - getParentRoute: () => rootRouteImport, + id: '/account', + path: '/account', + getParentRoute: () => rootRouteImport, } as any) const ChatRoute = ChatRouteImport.update({ - id: "/chat", - path: "/chat", - getParentRoute: () => rootRouteImport, + id: '/chat', + path: '/chat', + getParentRoute: () => rootRouteImport, } as any) const CliLoginRoute = CliLoginRouteImport.update({ - id: "/cli-login", - path: "/cli-login", - getParentRoute: () => rootRouteImport, + id: '/cli-login', + path: '/cli-login', + getParentRoute: () => rootRouteImport, } as any) const ConnectorsRoute = ConnectorsRouteImport.update({ - id: "/connectors", - path: "/connectors", - getParentRoute: () => rootRouteImport, + id: '/connectors', + path: '/connectors', + getParentRoute: () => rootRouteImport, } as any) const DeveloperRoute = DeveloperRouteImport.update({ - id: "/developer", - path: "/developer", - getParentRoute: () => rootRouteImport, + id: '/developer', + path: '/developer', + getParentRoute: () => rootRouteImport, } as any) const FlowLabRoute = FlowLabRouteImport.update({ - id: "/flow-lab", - path: "/flow-lab", - getParentRoute: () => rootRouteImport, + id: '/flow-lab', + path: '/flow-lab', + getParentRoute: () => rootRouteImport, } as any) const InfraBenchRoute = InfraBenchRouteImport.update({ - id: "/infra-bench", - path: "/infra-bench", - getParentRoute: () => rootRouteImport, + id: '/infra-bench', + path: '/infra-bench', + getParentRoute: () => rootRouteImport, } as any) const IntegrationsRoute = IntegrationsRouteImport.update({ - id: "/integrations", - path: "/integrations", - getParentRoute: () => rootRouteImport, + id: '/integrations', + path: '/integrations', + getParentRoute: () => rootRouteImport, } as any) const LogsBenchRoute = LogsBenchRouteImport.update({ - id: "/logs-bench", - path: "/logs-bench", - getParentRoute: () => rootRouteImport, + id: '/logs-bench', + path: '/logs-bench', + getParentRoute: () => rootRouteImport, } as any) const McpRoute = McpRouteImport.update({ - id: "/mcp", - path: "/mcp", - getParentRoute: () => rootRouteImport, + id: '/mcp', + path: '/mcp', + getParentRoute: () => rootRouteImport, } as any) const McpAuthorizeRoute = McpAuthorizeRouteImport.update({ - id: "/mcp-authorize", - path: "/mcp-authorize", - getParentRoute: () => rootRouteImport, + id: '/mcp-authorize', + path: '/mcp-authorize', + getParentRoute: () => rootRouteImport, } as any) const NodeLabRoute = NodeLabRouteImport.update({ - id: "/node-lab", - path: "/node-lab", - getParentRoute: () => rootRouteImport, + id: '/node-lab', + path: '/node-lab', + getParentRoute: () => rootRouteImport, } as any) const OrgRequiredRoute = OrgRequiredRouteImport.update({ - id: "/org-required", - path: "/org-required", - getParentRoute: () => rootRouteImport, + id: '/org-required', + path: '/org-required', + getParentRoute: () => rootRouteImport, } as any) const OverviewBenchRoute = OverviewBenchRouteImport.update({ - id: "/overview-bench", - path: "/overview-bench", - getParentRoute: () => rootRouteImport, + id: '/overview-bench', + path: '/overview-bench', + getParentRoute: () => rootRouteImport, } as any) const QueryBuilderLabRoute = QueryBuilderLabRouteImport.update({ - id: "/query-builder-lab", - path: "/query-builder-lab", - getParentRoute: () => rootRouteImport, + id: '/query-builder-lab', + path: '/query-builder-lab', + getParentRoute: () => rootRouteImport, } as any) const QuickStartRoute = QuickStartRouteImport.update({ - id: "/quick-start", - path: "/quick-start", - getParentRoute: () => rootRouteImport, + id: '/quick-start', + path: '/quick-start', + getParentRoute: () => rootRouteImport, } as any) const SelectPlanRoute = SelectPlanRouteImport.update({ - id: "/select-plan", - path: "/select-plan", - getParentRoute: () => rootRouteImport, + id: '/select-plan', + path: '/select-plan', + getParentRoute: () => rootRouteImport, } as any) const ServiceDetailBenchRoute = ServiceDetailBenchRouteImport.update({ - id: "/service-detail-bench", - path: "/service-detail-bench", - getParentRoute: () => rootRouteImport, + id: '/service-detail-bench', + path: '/service-detail-bench', + getParentRoute: () => rootRouteImport, } as any) const ServiceMapRoute = ServiceMapRouteImport.update({ - id: "/service-map", - path: "/service-map", - getParentRoute: () => rootRouteImport, + id: '/service-map', + path: '/service-map', + getParentRoute: () => rootRouteImport, } as any) const ServiceMapBenchRoute = ServiceMapBenchRouteImport.update({ - id: "/service-map-bench", - path: "/service-map-bench", - getParentRoute: () => rootRouteImport, + id: '/service-map-bench', + path: '/service-map-bench', + getParentRoute: () => rootRouteImport, } as any) const SettingsRoute = SettingsRouteImport.update({ - id: "/settings", - path: "/settings", - getParentRoute: () => rootRouteImport, + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, } as any) const SignInRoute = SignInRouteImport.update({ - id: "/sign-in", - path: "/sign-in", - getParentRoute: () => rootRouteImport, + id: '/sign-in', + path: '/sign-in', + getParentRoute: () => rootRouteImport, } as any) const SignUpRoute = SignUpRouteImport.update({ - id: "/sign-up", - path: "/sign-up", - getParentRoute: () => rootRouteImport, + id: '/sign-up', + path: '/sign-up', + getParentRoute: () => rootRouteImport, } as any) const TimelineLabRoute = TimelineLabRouteImport.update({ - id: "/timeline-lab", - path: "/timeline-lab", - getParentRoute: () => rootRouteImport, + id: '/timeline-lab', + path: '/timeline-lab', + getParentRoute: () => rootRouteImport, } as any) const WidgetLabRoute = WidgetLabRouteImport.update({ - id: "/widget-lab", - path: "/widget-lab", - getParentRoute: () => rootRouteImport, + id: '/widget-lab', + path: '/widget-lab', + getParentRoute: () => rootRouteImport, } as any) const AlertsIndexRoute = AlertsIndexRouteImport.update({ - id: "/alerts/", - path: "/alerts/", - getParentRoute: () => rootRouteImport, + id: '/alerts/', + path: '/alerts/', + getParentRoute: () => rootRouteImport, } as any) const AlertsRuleIdRoute = AlertsRuleIdRouteImport.update({ - id: "/alerts/$ruleId", - path: "/alerts/$ruleId", - getParentRoute: () => rootRouteImport, + id: '/alerts/$ruleId', + path: '/alerts/$ruleId', + getParentRoute: () => rootRouteImport, } as any) const AlertsCreateRoute = AlertsCreateRouteImport.update({ - id: "/alerts/create", - path: "/alerts/create", - getParentRoute: () => rootRouteImport, + id: '/alerts/create', + path: '/alerts/create', + getParentRoute: () => rootRouteImport, } as any) const AnalyticsIndexRoute = AnalyticsIndexRouteImport.update({ - id: "/analytics/", - path: "/analytics/", - getParentRoute: () => rootRouteImport, + id: '/analytics/', + path: '/analytics/', + getParentRoute: () => rootRouteImport, } as any) const AnomaliesIndexRoute = AnomaliesIndexRouteImport.update({ - id: "/anomalies/", - path: "/anomalies/", - getParentRoute: () => rootRouteImport, + id: '/anomalies/', + path: '/anomalies/', + getParentRoute: () => rootRouteImport, } as any) const AnomaliesIncidentIdRoute = AnomaliesIncidentIdRouteImport.update({ - id: "/anomalies/$incidentId", - path: "/anomalies/$incidentId", - getParentRoute: () => rootRouteImport, + id: '/anomalies/$incidentId', + path: '/anomalies/$incidentId', + getParentRoute: () => rootRouteImport, } as any) const DashboardsIndexRoute = DashboardsIndexRouteImport.update({ - id: "/dashboards/", - path: "/dashboards/", - getParentRoute: () => rootRouteImport, + id: '/dashboards/', + path: '/dashboards/', + getParentRoute: () => rootRouteImport, } as any) const DashboardsDashboardIdRoute = DashboardsDashboardIdRouteImport.update({ - id: "/dashboards/$dashboardId", - path: "/dashboards/$dashboardId", - getParentRoute: () => rootRouteImport, + id: '/dashboards/$dashboardId', + path: '/dashboards/$dashboardId', + getParentRoute: () => rootRouteImport, } as any) const DashboardsTemplatesRoute = DashboardsTemplatesRouteImport.update({ - id: "/dashboards/templates", - path: "/dashboards/templates", - getParentRoute: () => rootRouteImport, + id: '/dashboards/templates', + path: '/dashboards/templates', + getParentRoute: () => rootRouteImport, } as any) const ErrorsIndexRoute = ErrorsIndexRouteImport.update({ - id: "/errors/", - path: "/errors/", - getParentRoute: () => rootRouteImport, + id: '/errors/', + path: '/errors/', + getParentRoute: () => rootRouteImport, } as any) const ErrorsErrorTypeRoute = ErrorsErrorTypeRouteImport.update({ - id: "/errors/$errorType", - path: "/errors/$errorType", - getParentRoute: () => rootRouteImport, + id: '/errors/$errorType', + path: '/errors/$errorType', + getParentRoute: () => rootRouteImport, } as any) const InfraIndexRoute = InfraIndexRouteImport.update({ - id: "/infra/", - path: "/infra/", - getParentRoute: () => rootRouteImport, + id: '/infra/', + path: '/infra/', + getParentRoute: () => rootRouteImport, } as any) const InfraHostNameRoute = InfraHostNameRouteImport.update({ - id: "/infra/$hostName", - path: "/infra/$hostName", - getParentRoute: () => rootRouteImport, + id: '/infra/$hostName', + path: '/infra/$hostName', + getParentRoute: () => rootRouteImport, } as any) const InvestigationsIndexRoute = InvestigationsIndexRouteImport.update({ - id: "/investigations/", - path: "/investigations/", - getParentRoute: () => rootRouteImport, + id: '/investigations/', + path: '/investigations/', + getParentRoute: () => rootRouteImport, } as any) const InvestigationsIdRoute = InvestigationsIdRouteImport.update({ - id: "/investigations/$id", - path: "/investigations/$id", - getParentRoute: () => rootRouteImport, + id: '/investigations/$id', + path: '/investigations/$id', + getParentRoute: () => rootRouteImport, } as any) const LogsIndexRoute = LogsIndexRouteImport.update({ - id: "/logs/", - path: "/logs/", - getParentRoute: () => rootRouteImport, + id: '/logs/', + path: '/logs/', + getParentRoute: () => rootRouteImport, } as any) const LogsLogIdRoute = LogsLogIdRouteImport.update({ - id: "/logs/$logId", - path: "/logs/$logId", - getParentRoute: () => rootRouteImport, + id: '/logs/$logId', + path: '/logs/$logId', + getParentRoute: () => rootRouteImport, } as any) const MetricsIndexRoute = MetricsIndexRouteImport.update({ - id: "/metrics/", - path: "/metrics/", - getParentRoute: () => rootRouteImport, + id: '/metrics/', + path: '/metrics/', + getParentRoute: () => rootRouteImport, } as any) const MetricsMetricNameRoute = MetricsMetricNameRouteImport.update({ - id: "/metrics/$metricName", - path: "/metrics/$metricName", - getParentRoute: () => rootRouteImport, -} as any) -const RecommendationsRecommendationKeyRoute = RecommendationsRecommendationKeyRouteImport.update({ - id: "/recommendations/$recommendationKey", - path: "/recommendations/$recommendationKey", - getParentRoute: () => rootRouteImport, -} as any) + id: '/metrics/$metricName', + path: '/metrics/$metricName', + getParentRoute: () => rootRouteImport, +} as any) +const RecommendationsRecommendationKeyRoute = + RecommendationsRecommendationKeyRouteImport.update({ + id: '/recommendations/$recommendationKey', + path: '/recommendations/$recommendationKey', + getParentRoute: () => rootRouteImport, + } as any) const ReplaysIndexRoute = ReplaysIndexRouteImport.update({ - id: "/replays/", - path: "/replays/", - getParentRoute: () => rootRouteImport, + id: '/replays/', + path: '/replays/', + getParentRoute: () => rootRouteImport, } as any) const ReplaysSessionIdRoute = ReplaysSessionIdRouteImport.update({ - id: "/replays/$sessionId", - path: "/replays/$sessionId", - getParentRoute: () => rootRouteImport, + id: '/replays/$sessionId', + path: '/replays/$sessionId', + getParentRoute: () => rootRouteImport, } as any) const ServicesIndexRoute = ServicesIndexRouteImport.update({ - id: "/services/", - path: "/services/", - getParentRoute: () => rootRouteImport, + id: '/services/', + path: '/services/', + getParentRoute: () => rootRouteImport, } as any) const ServicesServiceNameRoute = ServicesServiceNameRouteImport.update({ - id: "/services/$serviceName", - path: "/services/$serviceName", - getParentRoute: () => rootRouteImport, + id: '/services/$serviceName', + path: '/services/$serviceName', + getParentRoute: () => rootRouteImport, } as any) const TracesIndexRoute = TracesIndexRouteImport.update({ - id: "/traces/", - path: "/traces/", - getParentRoute: () => rootRouteImport, + id: '/traces/', + path: '/traces/', + getParentRoute: () => rootRouteImport, } as any) const TracesTraceIdRoute = TracesTraceIdRouteImport.update({ - id: "/traces/$traceId", - path: "/traces/$traceId", - getParentRoute: () => rootRouteImport, -} as any) -const AlertsIncidentsIncidentIdRoute = AlertsIncidentsIncidentIdRouteImport.update({ - id: "/alerts/incidents/$incidentId", - path: "/alerts/incidents/$incidentId", - getParentRoute: () => rootRouteImport, -} as any) + id: '/traces/$traceId', + path: '/traces/$traceId', + getParentRoute: () => rootRouteImport, +} as any) +const AlertsIncidentsIncidentIdRoute = + AlertsIncidentsIncidentIdRouteImport.update({ + id: '/alerts/incidents/$incidentId', + path: '/alerts/incidents/$incidentId', + getParentRoute: () => rootRouteImport, + } as any) const ErrorsIssuesIndexRoute = ErrorsIssuesIndexRouteImport.update({ - id: "/errors/issues/", - path: "/errors/issues/", - getParentRoute: () => rootRouteImport, + id: '/errors/issues/', + path: '/errors/issues/', + getParentRoute: () => rootRouteImport, } as any) const ErrorsIssuesIssueIdRoute = ErrorsIssuesIssueIdRouteImport.update({ - id: "/errors/issues/$issueId", - path: "/errors/issues/$issueId", - getParentRoute: () => rootRouteImport, + id: '/errors/issues/$issueId', + path: '/errors/issues/$issueId', + getParentRoute: () => rootRouteImport, } as any) const InfraCloudflareIndexRoute = InfraCloudflareIndexRouteImport.update({ - id: "/infra/cloudflare/", - path: "/infra/cloudflare/", - getParentRoute: () => rootRouteImport, + id: '/infra/cloudflare/', + path: '/infra/cloudflare/', + getParentRoute: () => rootRouteImport, } as any) const InfraCloudflareZoneNameRoute = InfraCloudflareZoneNameRouteImport.update({ - id: "/infra/cloudflare/$zoneName", - path: "/infra/cloudflare/$zoneName", - getParentRoute: () => rootRouteImport, + id: '/infra/cloudflare/$zoneName', + path: '/infra/cloudflare/$zoneName', + getParentRoute: () => rootRouteImport, } as any) const InfraPlanetscaleIndexRoute = InfraPlanetscaleIndexRouteImport.update({ - id: "/infra/planetscale/", - path: "/infra/planetscale/", - getParentRoute: () => rootRouteImport, + id: '/infra/planetscale/', + path: '/infra/planetscale/', + getParentRoute: () => rootRouteImport, } as any) const InfraPlanetscaleDbNameRoute = InfraPlanetscaleDbNameRouteImport.update({ - id: "/infra/planetscale/$dbName", - path: "/infra/planetscale/$dbName", - getParentRoute: () => rootRouteImport, -} as any) -const DashboardsDashboardIdWidgetsWidgetIdRoute = DashboardsDashboardIdWidgetsWidgetIdRouteImport.update({ - id: "/dashboards/$dashboardId_/widgets/$widgetId", - path: "/dashboards/$dashboardId/widgets/$widgetId", - getParentRoute: () => rootRouteImport, -} as any) -const InfraKubernetesNodesIndexRoute = InfraKubernetesNodesIndexRouteImport.update({ - id: "/infra/kubernetes/nodes/", - path: "/infra/kubernetes/nodes/", - getParentRoute: () => rootRouteImport, -} as any) -const InfraKubernetesNodesNodeNameRoute = InfraKubernetesNodesNodeNameRouteImport.update({ - id: "/infra/kubernetes/nodes/$nodeName", - path: "/infra/kubernetes/nodes/$nodeName", - getParentRoute: () => rootRouteImport, -} as any) -const InfraKubernetesPodsIndexRoute = InfraKubernetesPodsIndexRouteImport.update({ - id: "/infra/kubernetes/pods/", - path: "/infra/kubernetes/pods/", - getParentRoute: () => rootRouteImport, -} as any) -const InfraKubernetesPodsPodNameRoute = InfraKubernetesPodsPodNameRouteImport.update({ - id: "/infra/kubernetes/pods/$podName", - path: "/infra/kubernetes/pods/$podName", - getParentRoute: () => rootRouteImport, -} as any) -const InfraKubernetesWorkloadsIndexRoute = InfraKubernetesWorkloadsIndexRouteImport.update({ - id: "/infra/kubernetes/workloads/", - path: "/infra/kubernetes/workloads/", - getParentRoute: () => rootRouteImport, -} as any) + id: '/infra/planetscale/$dbName', + path: '/infra/planetscale/$dbName', + getParentRoute: () => rootRouteImport, +} as any) +const ServicesServiceNameEndpointsRoute = + ServicesServiceNameEndpointsRouteImport.update({ + id: '/services/$serviceName_/endpoints', + path: '/services/$serviceName/endpoints', + getParentRoute: () => rootRouteImport, + } as any) +const DashboardsDashboardIdWidgetsWidgetIdRoute = + DashboardsDashboardIdWidgetsWidgetIdRouteImport.update({ + id: '/dashboards/$dashboardId_/widgets/$widgetId', + path: '/dashboards/$dashboardId/widgets/$widgetId', + getParentRoute: () => rootRouteImport, + } as any) +const InfraKubernetesNodesIndexRoute = + InfraKubernetesNodesIndexRouteImport.update({ + id: '/infra/kubernetes/nodes/', + path: '/infra/kubernetes/nodes/', + getParentRoute: () => rootRouteImport, + } as any) +const InfraKubernetesNodesNodeNameRoute = + InfraKubernetesNodesNodeNameRouteImport.update({ + id: '/infra/kubernetes/nodes/$nodeName', + path: '/infra/kubernetes/nodes/$nodeName', + getParentRoute: () => rootRouteImport, + } as any) +const InfraKubernetesPodsIndexRoute = + InfraKubernetesPodsIndexRouteImport.update({ + id: '/infra/kubernetes/pods/', + path: '/infra/kubernetes/pods/', + getParentRoute: () => rootRouteImport, + } as any) +const InfraKubernetesPodsPodNameRoute = + InfraKubernetesPodsPodNameRouteImport.update({ + id: '/infra/kubernetes/pods/$podName', + path: '/infra/kubernetes/pods/$podName', + getParentRoute: () => rootRouteImport, + } as any) +const InfraKubernetesWorkloadsIndexRoute = + InfraKubernetesWorkloadsIndexRouteImport.update({ + id: '/infra/kubernetes/workloads/', + path: '/infra/kubernetes/workloads/', + getParentRoute: () => rootRouteImport, + } as any) const InfraKubernetesWorkloadsKindWorkloadNameRoute = - InfraKubernetesWorkloadsKindWorkloadNameRouteImport.update({ - id: "/infra/kubernetes/workloads/$kind/$workloadName", - path: "/infra/kubernetes/workloads/$kind/$workloadName", - getParentRoute: () => rootRouteImport, - } as any) + InfraKubernetesWorkloadsKindWorkloadNameRouteImport.update({ + id: '/infra/kubernetes/workloads/$kind/$workloadName', + path: '/infra/kubernetes/workloads/$kind/$workloadName', + getParentRoute: () => rootRouteImport, + } as any) export interface FileRoutesByFullPath { - "/": typeof IndexRoute - "/account": typeof AccountRoute - "/chat": typeof ChatRoute - "/cli-login": typeof CliLoginRoute - "/connectors": typeof ConnectorsRoute - "/developer": typeof DeveloperRoute - "/flow-lab": typeof FlowLabRoute - "/infra-bench": typeof InfraBenchRoute - "/integrations": typeof IntegrationsRoute - "/logs-bench": typeof LogsBenchRoute - "/mcp": typeof McpRoute - "/mcp-authorize": typeof McpAuthorizeRoute - "/node-lab": typeof NodeLabRoute - "/org-required": typeof OrgRequiredRoute - "/overview-bench": typeof OverviewBenchRoute - "/query-builder-lab": typeof QueryBuilderLabRoute - "/quick-start": typeof QuickStartRoute - "/select-plan": typeof SelectPlanRoute - "/service-detail-bench": typeof ServiceDetailBenchRoute - "/service-map": typeof ServiceMapRoute - "/service-map-bench": typeof ServiceMapBenchRoute - "/settings": typeof SettingsRoute - "/sign-in": typeof SignInRoute - "/sign-up": typeof SignUpRoute - "/timeline-lab": typeof TimelineLabRoute - "/widget-lab": typeof WidgetLabRoute - "/alerts/$ruleId": typeof AlertsRuleIdRoute - "/alerts/create": typeof AlertsCreateRoute - "/anomalies/$incidentId": typeof AnomaliesIncidentIdRoute - "/dashboards/$dashboardId": typeof DashboardsDashboardIdRoute - "/dashboards/templates": typeof DashboardsTemplatesRoute - "/errors/$errorType": typeof ErrorsErrorTypeRoute - "/infra/$hostName": typeof InfraHostNameRoute - "/investigations/$id": typeof InvestigationsIdRoute - "/logs/$logId": typeof LogsLogIdRoute - "/metrics/$metricName": typeof MetricsMetricNameRoute - "/recommendations/$recommendationKey": typeof RecommendationsRecommendationKeyRoute - "/replays/$sessionId": typeof ReplaysSessionIdRoute - "/services/$serviceName": typeof ServicesServiceNameRoute - "/traces/$traceId": typeof TracesTraceIdRoute - "/alerts/": typeof AlertsIndexRoute - "/analytics/": typeof AnalyticsIndexRoute - "/anomalies/": typeof AnomaliesIndexRoute - "/dashboards/": typeof DashboardsIndexRoute - "/errors/": typeof ErrorsIndexRoute - "/infra/": typeof InfraIndexRoute - "/investigations/": typeof InvestigationsIndexRoute - "/logs/": typeof LogsIndexRoute - "/metrics/": typeof MetricsIndexRoute - "/replays/": typeof ReplaysIndexRoute - "/services/": typeof ServicesIndexRoute - "/traces/": typeof TracesIndexRoute - "/alerts/incidents/$incidentId": typeof AlertsIncidentsIncidentIdRoute - "/errors/issues/$issueId": typeof ErrorsIssuesIssueIdRoute - "/infra/cloudflare/$zoneName": typeof InfraCloudflareZoneNameRoute - "/infra/planetscale/$dbName": typeof InfraPlanetscaleDbNameRoute - "/errors/issues/": typeof ErrorsIssuesIndexRoute - "/infra/cloudflare/": typeof InfraCloudflareIndexRoute - "/infra/planetscale/": typeof InfraPlanetscaleIndexRoute - "/dashboards/$dashboardId/widgets/$widgetId": typeof DashboardsDashboardIdWidgetsWidgetIdRoute - "/infra/kubernetes/nodes/$nodeName": typeof InfraKubernetesNodesNodeNameRoute - "/infra/kubernetes/pods/$podName": typeof InfraKubernetesPodsPodNameRoute - "/infra/kubernetes/nodes/": typeof InfraKubernetesNodesIndexRoute - "/infra/kubernetes/pods/": typeof InfraKubernetesPodsIndexRoute - "/infra/kubernetes/workloads/": typeof InfraKubernetesWorkloadsIndexRoute - "/infra/kubernetes/workloads/$kind/$workloadName": typeof InfraKubernetesWorkloadsKindWorkloadNameRoute + '/': typeof IndexRoute + '/account': typeof AccountRoute + '/chat': typeof ChatRoute + '/cli-login': typeof CliLoginRoute + '/connectors': typeof ConnectorsRoute + '/developer': typeof DeveloperRoute + '/flow-lab': typeof FlowLabRoute + '/infra-bench': typeof InfraBenchRoute + '/integrations': typeof IntegrationsRoute + '/logs-bench': typeof LogsBenchRoute + '/mcp': typeof McpRoute + '/mcp-authorize': typeof McpAuthorizeRoute + '/node-lab': typeof NodeLabRoute + '/org-required': typeof OrgRequiredRoute + '/overview-bench': typeof OverviewBenchRoute + '/query-builder-lab': typeof QueryBuilderLabRoute + '/quick-start': typeof QuickStartRoute + '/select-plan': typeof SelectPlanRoute + '/service-detail-bench': typeof ServiceDetailBenchRoute + '/service-map': typeof ServiceMapRoute + '/service-map-bench': typeof ServiceMapBenchRoute + '/settings': typeof SettingsRoute + '/sign-in': typeof SignInRoute + '/sign-up': typeof SignUpRoute + '/timeline-lab': typeof TimelineLabRoute + '/widget-lab': typeof WidgetLabRoute + '/alerts/$ruleId': typeof AlertsRuleIdRoute + '/alerts/create': typeof AlertsCreateRoute + '/anomalies/$incidentId': typeof AnomaliesIncidentIdRoute + '/dashboards/$dashboardId': typeof DashboardsDashboardIdRoute + '/dashboards/templates': typeof DashboardsTemplatesRoute + '/errors/$errorType': typeof ErrorsErrorTypeRoute + '/infra/$hostName': typeof InfraHostNameRoute + '/investigations/$id': typeof InvestigationsIdRoute + '/logs/$logId': typeof LogsLogIdRoute + '/metrics/$metricName': typeof MetricsMetricNameRoute + '/recommendations/$recommendationKey': typeof RecommendationsRecommendationKeyRoute + '/replays/$sessionId': typeof ReplaysSessionIdRoute + '/services/$serviceName': typeof ServicesServiceNameRoute + '/traces/$traceId': typeof TracesTraceIdRoute + '/alerts/': typeof AlertsIndexRoute + '/analytics/': typeof AnalyticsIndexRoute + '/anomalies/': typeof AnomaliesIndexRoute + '/dashboards/': typeof DashboardsIndexRoute + '/errors/': typeof ErrorsIndexRoute + '/infra/': typeof InfraIndexRoute + '/investigations/': typeof InvestigationsIndexRoute + '/logs/': typeof LogsIndexRoute + '/metrics/': typeof MetricsIndexRoute + '/replays/': typeof ReplaysIndexRoute + '/services/': typeof ServicesIndexRoute + '/traces/': typeof TracesIndexRoute + '/alerts/incidents/$incidentId': typeof AlertsIncidentsIncidentIdRoute + '/errors/issues/$issueId': typeof ErrorsIssuesIssueIdRoute + '/infra/cloudflare/$zoneName': typeof InfraCloudflareZoneNameRoute + '/infra/planetscale/$dbName': typeof InfraPlanetscaleDbNameRoute + '/services/$serviceName/endpoints': typeof ServicesServiceNameEndpointsRoute + '/errors/issues/': typeof ErrorsIssuesIndexRoute + '/infra/cloudflare/': typeof InfraCloudflareIndexRoute + '/infra/planetscale/': typeof InfraPlanetscaleIndexRoute + '/dashboards/$dashboardId/widgets/$widgetId': typeof DashboardsDashboardIdWidgetsWidgetIdRoute + '/infra/kubernetes/nodes/$nodeName': typeof InfraKubernetesNodesNodeNameRoute + '/infra/kubernetes/pods/$podName': typeof InfraKubernetesPodsPodNameRoute + '/infra/kubernetes/nodes/': typeof InfraKubernetesNodesIndexRoute + '/infra/kubernetes/pods/': typeof InfraKubernetesPodsIndexRoute + '/infra/kubernetes/workloads/': typeof InfraKubernetesWorkloadsIndexRoute + '/infra/kubernetes/workloads/$kind/$workloadName': typeof InfraKubernetesWorkloadsKindWorkloadNameRoute } export interface FileRoutesByTo { - "/": typeof IndexRoute - "/account": typeof AccountRoute - "/chat": typeof ChatRoute - "/cli-login": typeof CliLoginRoute - "/connectors": typeof ConnectorsRoute - "/developer": typeof DeveloperRoute - "/flow-lab": typeof FlowLabRoute - "/infra-bench": typeof InfraBenchRoute - "/integrations": typeof IntegrationsRoute - "/logs-bench": typeof LogsBenchRoute - "/mcp": typeof McpRoute - "/mcp-authorize": typeof McpAuthorizeRoute - "/node-lab": typeof NodeLabRoute - "/org-required": typeof OrgRequiredRoute - "/overview-bench": typeof OverviewBenchRoute - "/query-builder-lab": typeof QueryBuilderLabRoute - "/quick-start": typeof QuickStartRoute - "/select-plan": typeof SelectPlanRoute - "/service-detail-bench": typeof ServiceDetailBenchRoute - "/service-map": typeof ServiceMapRoute - "/service-map-bench": typeof ServiceMapBenchRoute - "/settings": typeof SettingsRoute - "/sign-in": typeof SignInRoute - "/sign-up": typeof SignUpRoute - "/timeline-lab": typeof TimelineLabRoute - "/widget-lab": typeof WidgetLabRoute - "/alerts/$ruleId": typeof AlertsRuleIdRoute - "/alerts/create": typeof AlertsCreateRoute - "/anomalies/$incidentId": typeof AnomaliesIncidentIdRoute - "/dashboards/$dashboardId": typeof DashboardsDashboardIdRoute - "/dashboards/templates": typeof DashboardsTemplatesRoute - "/errors/$errorType": typeof ErrorsErrorTypeRoute - "/infra/$hostName": typeof InfraHostNameRoute - "/investigations/$id": typeof InvestigationsIdRoute - "/logs/$logId": typeof LogsLogIdRoute - "/metrics/$metricName": typeof MetricsMetricNameRoute - "/recommendations/$recommendationKey": typeof RecommendationsRecommendationKeyRoute - "/replays/$sessionId": typeof ReplaysSessionIdRoute - "/services/$serviceName": typeof ServicesServiceNameRoute - "/traces/$traceId": typeof TracesTraceIdRoute - "/alerts": typeof AlertsIndexRoute - "/analytics": typeof AnalyticsIndexRoute - "/anomalies": typeof AnomaliesIndexRoute - "/dashboards": typeof DashboardsIndexRoute - "/errors": typeof ErrorsIndexRoute - "/infra": typeof InfraIndexRoute - "/investigations": typeof InvestigationsIndexRoute - "/logs": typeof LogsIndexRoute - "/metrics": typeof MetricsIndexRoute - "/replays": typeof ReplaysIndexRoute - "/services": typeof ServicesIndexRoute - "/traces": typeof TracesIndexRoute - "/alerts/incidents/$incidentId": typeof AlertsIncidentsIncidentIdRoute - "/errors/issues/$issueId": typeof ErrorsIssuesIssueIdRoute - "/infra/cloudflare/$zoneName": typeof InfraCloudflareZoneNameRoute - "/infra/planetscale/$dbName": typeof InfraPlanetscaleDbNameRoute - "/errors/issues": typeof ErrorsIssuesIndexRoute - "/infra/cloudflare": typeof InfraCloudflareIndexRoute - "/infra/planetscale": typeof InfraPlanetscaleIndexRoute - "/dashboards/$dashboardId/widgets/$widgetId": typeof DashboardsDashboardIdWidgetsWidgetIdRoute - "/infra/kubernetes/nodes/$nodeName": typeof InfraKubernetesNodesNodeNameRoute - "/infra/kubernetes/pods/$podName": typeof InfraKubernetesPodsPodNameRoute - "/infra/kubernetes/nodes": typeof InfraKubernetesNodesIndexRoute - "/infra/kubernetes/pods": typeof InfraKubernetesPodsIndexRoute - "/infra/kubernetes/workloads": typeof InfraKubernetesWorkloadsIndexRoute - "/infra/kubernetes/workloads/$kind/$workloadName": typeof InfraKubernetesWorkloadsKindWorkloadNameRoute + '/': typeof IndexRoute + '/account': typeof AccountRoute + '/chat': typeof ChatRoute + '/cli-login': typeof CliLoginRoute + '/connectors': typeof ConnectorsRoute + '/developer': typeof DeveloperRoute + '/flow-lab': typeof FlowLabRoute + '/infra-bench': typeof InfraBenchRoute + '/integrations': typeof IntegrationsRoute + '/logs-bench': typeof LogsBenchRoute + '/mcp': typeof McpRoute + '/mcp-authorize': typeof McpAuthorizeRoute + '/node-lab': typeof NodeLabRoute + '/org-required': typeof OrgRequiredRoute + '/overview-bench': typeof OverviewBenchRoute + '/query-builder-lab': typeof QueryBuilderLabRoute + '/quick-start': typeof QuickStartRoute + '/select-plan': typeof SelectPlanRoute + '/service-detail-bench': typeof ServiceDetailBenchRoute + '/service-map': typeof ServiceMapRoute + '/service-map-bench': typeof ServiceMapBenchRoute + '/settings': typeof SettingsRoute + '/sign-in': typeof SignInRoute + '/sign-up': typeof SignUpRoute + '/timeline-lab': typeof TimelineLabRoute + '/widget-lab': typeof WidgetLabRoute + '/alerts/$ruleId': typeof AlertsRuleIdRoute + '/alerts/create': typeof AlertsCreateRoute + '/anomalies/$incidentId': typeof AnomaliesIncidentIdRoute + '/dashboards/$dashboardId': typeof DashboardsDashboardIdRoute + '/dashboards/templates': typeof DashboardsTemplatesRoute + '/errors/$errorType': typeof ErrorsErrorTypeRoute + '/infra/$hostName': typeof InfraHostNameRoute + '/investigations/$id': typeof InvestigationsIdRoute + '/logs/$logId': typeof LogsLogIdRoute + '/metrics/$metricName': typeof MetricsMetricNameRoute + '/recommendations/$recommendationKey': typeof RecommendationsRecommendationKeyRoute + '/replays/$sessionId': typeof ReplaysSessionIdRoute + '/services/$serviceName': typeof ServicesServiceNameRoute + '/traces/$traceId': typeof TracesTraceIdRoute + '/alerts': typeof AlertsIndexRoute + '/analytics': typeof AnalyticsIndexRoute + '/anomalies': typeof AnomaliesIndexRoute + '/dashboards': typeof DashboardsIndexRoute + '/errors': typeof ErrorsIndexRoute + '/infra': typeof InfraIndexRoute + '/investigations': typeof InvestigationsIndexRoute + '/logs': typeof LogsIndexRoute + '/metrics': typeof MetricsIndexRoute + '/replays': typeof ReplaysIndexRoute + '/services': typeof ServicesIndexRoute + '/traces': typeof TracesIndexRoute + '/alerts/incidents/$incidentId': typeof AlertsIncidentsIncidentIdRoute + '/errors/issues/$issueId': typeof ErrorsIssuesIssueIdRoute + '/infra/cloudflare/$zoneName': typeof InfraCloudflareZoneNameRoute + '/infra/planetscale/$dbName': typeof InfraPlanetscaleDbNameRoute + '/services/$serviceName/endpoints': typeof ServicesServiceNameEndpointsRoute + '/errors/issues': typeof ErrorsIssuesIndexRoute + '/infra/cloudflare': typeof InfraCloudflareIndexRoute + '/infra/planetscale': typeof InfraPlanetscaleIndexRoute + '/dashboards/$dashboardId/widgets/$widgetId': typeof DashboardsDashboardIdWidgetsWidgetIdRoute + '/infra/kubernetes/nodes/$nodeName': typeof InfraKubernetesNodesNodeNameRoute + '/infra/kubernetes/pods/$podName': typeof InfraKubernetesPodsPodNameRoute + '/infra/kubernetes/nodes': typeof InfraKubernetesNodesIndexRoute + '/infra/kubernetes/pods': typeof InfraKubernetesPodsIndexRoute + '/infra/kubernetes/workloads': typeof InfraKubernetesWorkloadsIndexRoute + '/infra/kubernetes/workloads/$kind/$workloadName': typeof InfraKubernetesWorkloadsKindWorkloadNameRoute } export interface FileRoutesById { - __root__: typeof rootRouteImport - "/": typeof IndexRoute - "/account": typeof AccountRoute - "/chat": typeof ChatRoute - "/cli-login": typeof CliLoginRoute - "/connectors": typeof ConnectorsRoute - "/developer": typeof DeveloperRoute - "/flow-lab": typeof FlowLabRoute - "/infra-bench": typeof InfraBenchRoute - "/integrations": typeof IntegrationsRoute - "/logs-bench": typeof LogsBenchRoute - "/mcp": typeof McpRoute - "/mcp-authorize": typeof McpAuthorizeRoute - "/node-lab": typeof NodeLabRoute - "/org-required": typeof OrgRequiredRoute - "/overview-bench": typeof OverviewBenchRoute - "/query-builder-lab": typeof QueryBuilderLabRoute - "/quick-start": typeof QuickStartRoute - "/select-plan": typeof SelectPlanRoute - "/service-detail-bench": typeof ServiceDetailBenchRoute - "/service-map": typeof ServiceMapRoute - "/service-map-bench": typeof ServiceMapBenchRoute - "/settings": typeof SettingsRoute - "/sign-in": typeof SignInRoute - "/sign-up": typeof SignUpRoute - "/timeline-lab": typeof TimelineLabRoute - "/widget-lab": typeof WidgetLabRoute - "/alerts/$ruleId": typeof AlertsRuleIdRoute - "/alerts/create": typeof AlertsCreateRoute - "/anomalies/$incidentId": typeof AnomaliesIncidentIdRoute - "/dashboards/$dashboardId": typeof DashboardsDashboardIdRoute - "/dashboards/templates": typeof DashboardsTemplatesRoute - "/errors/$errorType": typeof ErrorsErrorTypeRoute - "/infra/$hostName": typeof InfraHostNameRoute - "/investigations/$id": typeof InvestigationsIdRoute - "/logs/$logId": typeof LogsLogIdRoute - "/metrics/$metricName": typeof MetricsMetricNameRoute - "/recommendations/$recommendationKey": typeof RecommendationsRecommendationKeyRoute - "/replays/$sessionId": typeof ReplaysSessionIdRoute - "/services/$serviceName": typeof ServicesServiceNameRoute - "/traces/$traceId": typeof TracesTraceIdRoute - "/alerts/": typeof AlertsIndexRoute - "/analytics/": typeof AnalyticsIndexRoute - "/anomalies/": typeof AnomaliesIndexRoute - "/dashboards/": typeof DashboardsIndexRoute - "/errors/": typeof ErrorsIndexRoute - "/infra/": typeof InfraIndexRoute - "/investigations/": typeof InvestigationsIndexRoute - "/logs/": typeof LogsIndexRoute - "/metrics/": typeof MetricsIndexRoute - "/replays/": typeof ReplaysIndexRoute - "/services/": typeof ServicesIndexRoute - "/traces/": typeof TracesIndexRoute - "/alerts/incidents/$incidentId": typeof AlertsIncidentsIncidentIdRoute - "/errors/issues/$issueId": typeof ErrorsIssuesIssueIdRoute - "/infra/cloudflare/$zoneName": typeof InfraCloudflareZoneNameRoute - "/infra/planetscale/$dbName": typeof InfraPlanetscaleDbNameRoute - "/errors/issues/": typeof ErrorsIssuesIndexRoute - "/infra/cloudflare/": typeof InfraCloudflareIndexRoute - "/infra/planetscale/": typeof InfraPlanetscaleIndexRoute - "/dashboards/$dashboardId_/widgets/$widgetId": typeof DashboardsDashboardIdWidgetsWidgetIdRoute - "/infra/kubernetes/nodes/$nodeName": typeof InfraKubernetesNodesNodeNameRoute - "/infra/kubernetes/pods/$podName": typeof InfraKubernetesPodsPodNameRoute - "/infra/kubernetes/nodes/": typeof InfraKubernetesNodesIndexRoute - "/infra/kubernetes/pods/": typeof InfraKubernetesPodsIndexRoute - "/infra/kubernetes/workloads/": typeof InfraKubernetesWorkloadsIndexRoute - "/infra/kubernetes/workloads/$kind/$workloadName": typeof InfraKubernetesWorkloadsKindWorkloadNameRoute + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/account': typeof AccountRoute + '/chat': typeof ChatRoute + '/cli-login': typeof CliLoginRoute + '/connectors': typeof ConnectorsRoute + '/developer': typeof DeveloperRoute + '/flow-lab': typeof FlowLabRoute + '/infra-bench': typeof InfraBenchRoute + '/integrations': typeof IntegrationsRoute + '/logs-bench': typeof LogsBenchRoute + '/mcp': typeof McpRoute + '/mcp-authorize': typeof McpAuthorizeRoute + '/node-lab': typeof NodeLabRoute + '/org-required': typeof OrgRequiredRoute + '/overview-bench': typeof OverviewBenchRoute + '/query-builder-lab': typeof QueryBuilderLabRoute + '/quick-start': typeof QuickStartRoute + '/select-plan': typeof SelectPlanRoute + '/service-detail-bench': typeof ServiceDetailBenchRoute + '/service-map': typeof ServiceMapRoute + '/service-map-bench': typeof ServiceMapBenchRoute + '/settings': typeof SettingsRoute + '/sign-in': typeof SignInRoute + '/sign-up': typeof SignUpRoute + '/timeline-lab': typeof TimelineLabRoute + '/widget-lab': typeof WidgetLabRoute + '/alerts/$ruleId': typeof AlertsRuleIdRoute + '/alerts/create': typeof AlertsCreateRoute + '/anomalies/$incidentId': typeof AnomaliesIncidentIdRoute + '/dashboards/$dashboardId': typeof DashboardsDashboardIdRoute + '/dashboards/templates': typeof DashboardsTemplatesRoute + '/errors/$errorType': typeof ErrorsErrorTypeRoute + '/infra/$hostName': typeof InfraHostNameRoute + '/investigations/$id': typeof InvestigationsIdRoute + '/logs/$logId': typeof LogsLogIdRoute + '/metrics/$metricName': typeof MetricsMetricNameRoute + '/recommendations/$recommendationKey': typeof RecommendationsRecommendationKeyRoute + '/replays/$sessionId': typeof ReplaysSessionIdRoute + '/services/$serviceName': typeof ServicesServiceNameRoute + '/traces/$traceId': typeof TracesTraceIdRoute + '/alerts/': typeof AlertsIndexRoute + '/analytics/': typeof AnalyticsIndexRoute + '/anomalies/': typeof AnomaliesIndexRoute + '/dashboards/': typeof DashboardsIndexRoute + '/errors/': typeof ErrorsIndexRoute + '/infra/': typeof InfraIndexRoute + '/investigations/': typeof InvestigationsIndexRoute + '/logs/': typeof LogsIndexRoute + '/metrics/': typeof MetricsIndexRoute + '/replays/': typeof ReplaysIndexRoute + '/services/': typeof ServicesIndexRoute + '/traces/': typeof TracesIndexRoute + '/alerts/incidents/$incidentId': typeof AlertsIncidentsIncidentIdRoute + '/errors/issues/$issueId': typeof ErrorsIssuesIssueIdRoute + '/infra/cloudflare/$zoneName': typeof InfraCloudflareZoneNameRoute + '/infra/planetscale/$dbName': typeof InfraPlanetscaleDbNameRoute + '/services/$serviceName_/endpoints': typeof ServicesServiceNameEndpointsRoute + '/errors/issues/': typeof ErrorsIssuesIndexRoute + '/infra/cloudflare/': typeof InfraCloudflareIndexRoute + '/infra/planetscale/': typeof InfraPlanetscaleIndexRoute + '/dashboards/$dashboardId_/widgets/$widgetId': typeof DashboardsDashboardIdWidgetsWidgetIdRoute + '/infra/kubernetes/nodes/$nodeName': typeof InfraKubernetesNodesNodeNameRoute + '/infra/kubernetes/pods/$podName': typeof InfraKubernetesPodsPodNameRoute + '/infra/kubernetes/nodes/': typeof InfraKubernetesNodesIndexRoute + '/infra/kubernetes/pods/': typeof InfraKubernetesPodsIndexRoute + '/infra/kubernetes/workloads/': typeof InfraKubernetesWorkloadsIndexRoute + '/infra/kubernetes/workloads/$kind/$workloadName': typeof InfraKubernetesWorkloadsKindWorkloadNameRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: - | "/" - | "/account" - | "/chat" - | "/cli-login" - | "/connectors" - | "/developer" - | "/flow-lab" - | "/infra-bench" - | "/integrations" - | "/logs-bench" - | "/mcp" - | "/mcp-authorize" - | "/node-lab" - | "/org-required" - | "/overview-bench" - | "/query-builder-lab" - | "/quick-start" - | "/select-plan" - | "/service-detail-bench" - | "/service-map" - | "/service-map-bench" - | "/settings" - | "/sign-in" - | "/sign-up" - | "/timeline-lab" - | "/widget-lab" - | "/alerts/$ruleId" - | "/alerts/create" - | "/anomalies/$incidentId" - | "/dashboards/$dashboardId" - | "/dashboards/templates" - | "/errors/$errorType" - | "/infra/$hostName" - | "/investigations/$id" - | "/logs/$logId" - | "/metrics/$metricName" - | "/recommendations/$recommendationKey" - | "/replays/$sessionId" - | "/services/$serviceName" - | "/traces/$traceId" - | "/alerts/" - | "/analytics/" - | "/anomalies/" - | "/dashboards/" - | "/errors/" - | "/infra/" - | "/investigations/" - | "/logs/" - | "/metrics/" - | "/replays/" - | "/services/" - | "/traces/" - | "/alerts/incidents/$incidentId" - | "/errors/issues/$issueId" - | "/infra/cloudflare/$zoneName" - | "/infra/planetscale/$dbName" - | "/errors/issues/" - | "/infra/cloudflare/" - | "/infra/planetscale/" - | "/dashboards/$dashboardId/widgets/$widgetId" - | "/infra/kubernetes/nodes/$nodeName" - | "/infra/kubernetes/pods/$podName" - | "/infra/kubernetes/nodes/" - | "/infra/kubernetes/pods/" - | "/infra/kubernetes/workloads/" - | "/infra/kubernetes/workloads/$kind/$workloadName" - fileRoutesByTo: FileRoutesByTo - to: - | "/" - | "/account" - | "/chat" - | "/cli-login" - | "/connectors" - | "/developer" - | "/flow-lab" - | "/infra-bench" - | "/integrations" - | "/logs-bench" - | "/mcp" - | "/mcp-authorize" - | "/node-lab" - | "/org-required" - | "/overview-bench" - | "/query-builder-lab" - | "/quick-start" - | "/select-plan" - | "/service-detail-bench" - | "/service-map" - | "/service-map-bench" - | "/settings" - | "/sign-in" - | "/sign-up" - | "/timeline-lab" - | "/widget-lab" - | "/alerts/$ruleId" - | "/alerts/create" - | "/anomalies/$incidentId" - | "/dashboards/$dashboardId" - | "/dashboards/templates" - | "/errors/$errorType" - | "/infra/$hostName" - | "/investigations/$id" - | "/logs/$logId" - | "/metrics/$metricName" - | "/recommendations/$recommendationKey" - | "/replays/$sessionId" - | "/services/$serviceName" - | "/traces/$traceId" - | "/alerts" - | "/analytics" - | "/anomalies" - | "/dashboards" - | "/errors" - | "/infra" - | "/investigations" - | "/logs" - | "/metrics" - | "/replays" - | "/services" - | "/traces" - | "/alerts/incidents/$incidentId" - | "/errors/issues/$issueId" - | "/infra/cloudflare/$zoneName" - | "/infra/planetscale/$dbName" - | "/errors/issues" - | "/infra/cloudflare" - | "/infra/planetscale" - | "/dashboards/$dashboardId/widgets/$widgetId" - | "/infra/kubernetes/nodes/$nodeName" - | "/infra/kubernetes/pods/$podName" - | "/infra/kubernetes/nodes" - | "/infra/kubernetes/pods" - | "/infra/kubernetes/workloads" - | "/infra/kubernetes/workloads/$kind/$workloadName" - id: - | "__root__" - | "/" - | "/account" - | "/chat" - | "/cli-login" - | "/connectors" - | "/developer" - | "/flow-lab" - | "/infra-bench" - | "/integrations" - | "/logs-bench" - | "/mcp" - | "/mcp-authorize" - | "/node-lab" - | "/org-required" - | "/overview-bench" - | "/query-builder-lab" - | "/quick-start" - | "/select-plan" - | "/service-detail-bench" - | "/service-map" - | "/service-map-bench" - | "/settings" - | "/sign-in" - | "/sign-up" - | "/timeline-lab" - | "/widget-lab" - | "/alerts/$ruleId" - | "/alerts/create" - | "/anomalies/$incidentId" - | "/dashboards/$dashboardId" - | "/dashboards/templates" - | "/errors/$errorType" - | "/infra/$hostName" - | "/investigations/$id" - | "/logs/$logId" - | "/metrics/$metricName" - | "/recommendations/$recommendationKey" - | "/replays/$sessionId" - | "/services/$serviceName" - | "/traces/$traceId" - | "/alerts/" - | "/analytics/" - | "/anomalies/" - | "/dashboards/" - | "/errors/" - | "/infra/" - | "/investigations/" - | "/logs/" - | "/metrics/" - | "/replays/" - | "/services/" - | "/traces/" - | "/alerts/incidents/$incidentId" - | "/errors/issues/$issueId" - | "/infra/cloudflare/$zoneName" - | "/infra/planetscale/$dbName" - | "/errors/issues/" - | "/infra/cloudflare/" - | "/infra/planetscale/" - | "/dashboards/$dashboardId_/widgets/$widgetId" - | "/infra/kubernetes/nodes/$nodeName" - | "/infra/kubernetes/pods/$podName" - | "/infra/kubernetes/nodes/" - | "/infra/kubernetes/pods/" - | "/infra/kubernetes/workloads/" - | "/infra/kubernetes/workloads/$kind/$workloadName" - fileRoutesById: FileRoutesById + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/account' + | '/chat' + | '/cli-login' + | '/connectors' + | '/developer' + | '/flow-lab' + | '/infra-bench' + | '/integrations' + | '/logs-bench' + | '/mcp' + | '/mcp-authorize' + | '/node-lab' + | '/org-required' + | '/overview-bench' + | '/query-builder-lab' + | '/quick-start' + | '/select-plan' + | '/service-detail-bench' + | '/service-map' + | '/service-map-bench' + | '/settings' + | '/sign-in' + | '/sign-up' + | '/timeline-lab' + | '/widget-lab' + | '/alerts/$ruleId' + | '/alerts/create' + | '/anomalies/$incidentId' + | '/dashboards/$dashboardId' + | '/dashboards/templates' + | '/errors/$errorType' + | '/infra/$hostName' + | '/investigations/$id' + | '/logs/$logId' + | '/metrics/$metricName' + | '/recommendations/$recommendationKey' + | '/replays/$sessionId' + | '/services/$serviceName' + | '/traces/$traceId' + | '/alerts/' + | '/analytics/' + | '/anomalies/' + | '/dashboards/' + | '/errors/' + | '/infra/' + | '/investigations/' + | '/logs/' + | '/metrics/' + | '/replays/' + | '/services/' + | '/traces/' + | '/alerts/incidents/$incidentId' + | '/errors/issues/$issueId' + | '/infra/cloudflare/$zoneName' + | '/infra/planetscale/$dbName' + | '/services/$serviceName/endpoints' + | '/errors/issues/' + | '/infra/cloudflare/' + | '/infra/planetscale/' + | '/dashboards/$dashboardId/widgets/$widgetId' + | '/infra/kubernetes/nodes/$nodeName' + | '/infra/kubernetes/pods/$podName' + | '/infra/kubernetes/nodes/' + | '/infra/kubernetes/pods/' + | '/infra/kubernetes/workloads/' + | '/infra/kubernetes/workloads/$kind/$workloadName' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/account' + | '/chat' + | '/cli-login' + | '/connectors' + | '/developer' + | '/flow-lab' + | '/infra-bench' + | '/integrations' + | '/logs-bench' + | '/mcp' + | '/mcp-authorize' + | '/node-lab' + | '/org-required' + | '/overview-bench' + | '/query-builder-lab' + | '/quick-start' + | '/select-plan' + | '/service-detail-bench' + | '/service-map' + | '/service-map-bench' + | '/settings' + | '/sign-in' + | '/sign-up' + | '/timeline-lab' + | '/widget-lab' + | '/alerts/$ruleId' + | '/alerts/create' + | '/anomalies/$incidentId' + | '/dashboards/$dashboardId' + | '/dashboards/templates' + | '/errors/$errorType' + | '/infra/$hostName' + | '/investigations/$id' + | '/logs/$logId' + | '/metrics/$metricName' + | '/recommendations/$recommendationKey' + | '/replays/$sessionId' + | '/services/$serviceName' + | '/traces/$traceId' + | '/alerts' + | '/analytics' + | '/anomalies' + | '/dashboards' + | '/errors' + | '/infra' + | '/investigations' + | '/logs' + | '/metrics' + | '/replays' + | '/services' + | '/traces' + | '/alerts/incidents/$incidentId' + | '/errors/issues/$issueId' + | '/infra/cloudflare/$zoneName' + | '/infra/planetscale/$dbName' + | '/services/$serviceName/endpoints' + | '/errors/issues' + | '/infra/cloudflare' + | '/infra/planetscale' + | '/dashboards/$dashboardId/widgets/$widgetId' + | '/infra/kubernetes/nodes/$nodeName' + | '/infra/kubernetes/pods/$podName' + | '/infra/kubernetes/nodes' + | '/infra/kubernetes/pods' + | '/infra/kubernetes/workloads' + | '/infra/kubernetes/workloads/$kind/$workloadName' + id: + | '__root__' + | '/' + | '/account' + | '/chat' + | '/cli-login' + | '/connectors' + | '/developer' + | '/flow-lab' + | '/infra-bench' + | '/integrations' + | '/logs-bench' + | '/mcp' + | '/mcp-authorize' + | '/node-lab' + | '/org-required' + | '/overview-bench' + | '/query-builder-lab' + | '/quick-start' + | '/select-plan' + | '/service-detail-bench' + | '/service-map' + | '/service-map-bench' + | '/settings' + | '/sign-in' + | '/sign-up' + | '/timeline-lab' + | '/widget-lab' + | '/alerts/$ruleId' + | '/alerts/create' + | '/anomalies/$incidentId' + | '/dashboards/$dashboardId' + | '/dashboards/templates' + | '/errors/$errorType' + | '/infra/$hostName' + | '/investigations/$id' + | '/logs/$logId' + | '/metrics/$metricName' + | '/recommendations/$recommendationKey' + | '/replays/$sessionId' + | '/services/$serviceName' + | '/traces/$traceId' + | '/alerts/' + | '/analytics/' + | '/anomalies/' + | '/dashboards/' + | '/errors/' + | '/infra/' + | '/investigations/' + | '/logs/' + | '/metrics/' + | '/replays/' + | '/services/' + | '/traces/' + | '/alerts/incidents/$incidentId' + | '/errors/issues/$issueId' + | '/infra/cloudflare/$zoneName' + | '/infra/planetscale/$dbName' + | '/services/$serviceName_/endpoints' + | '/errors/issues/' + | '/infra/cloudflare/' + | '/infra/planetscale/' + | '/dashboards/$dashboardId_/widgets/$widgetId' + | '/infra/kubernetes/nodes/$nodeName' + | '/infra/kubernetes/pods/$podName' + | '/infra/kubernetes/nodes/' + | '/infra/kubernetes/pods/' + | '/infra/kubernetes/workloads/' + | '/infra/kubernetes/workloads/$kind/$workloadName' + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - AccountRoute: typeof AccountRoute - ChatRoute: typeof ChatRoute - CliLoginRoute: typeof CliLoginRoute - ConnectorsRoute: typeof ConnectorsRoute - DeveloperRoute: typeof DeveloperRoute - FlowLabRoute: typeof FlowLabRoute - InfraBenchRoute: typeof InfraBenchRoute - IntegrationsRoute: typeof IntegrationsRoute - LogsBenchRoute: typeof LogsBenchRoute - McpRoute: typeof McpRoute - McpAuthorizeRoute: typeof McpAuthorizeRoute - NodeLabRoute: typeof NodeLabRoute - OrgRequiredRoute: typeof OrgRequiredRoute - OverviewBenchRoute: typeof OverviewBenchRoute - QueryBuilderLabRoute: typeof QueryBuilderLabRoute - QuickStartRoute: typeof QuickStartRoute - SelectPlanRoute: typeof SelectPlanRoute - ServiceDetailBenchRoute: typeof ServiceDetailBenchRoute - ServiceMapRoute: typeof ServiceMapRoute - ServiceMapBenchRoute: typeof ServiceMapBenchRoute - SettingsRoute: typeof SettingsRoute - SignInRoute: typeof SignInRoute - SignUpRoute: typeof SignUpRoute - TimelineLabRoute: typeof TimelineLabRoute - WidgetLabRoute: typeof WidgetLabRoute - AlertsRuleIdRoute: typeof AlertsRuleIdRoute - AlertsCreateRoute: typeof AlertsCreateRoute - AnomaliesIncidentIdRoute: typeof AnomaliesIncidentIdRoute - DashboardsDashboardIdRoute: typeof DashboardsDashboardIdRoute - DashboardsTemplatesRoute: typeof DashboardsTemplatesRoute - ErrorsErrorTypeRoute: typeof ErrorsErrorTypeRoute - InfraHostNameRoute: typeof InfraHostNameRoute - InvestigationsIdRoute: typeof InvestigationsIdRoute - LogsLogIdRoute: typeof LogsLogIdRoute - MetricsMetricNameRoute: typeof MetricsMetricNameRoute - RecommendationsRecommendationKeyRoute: typeof RecommendationsRecommendationKeyRoute - ReplaysSessionIdRoute: typeof ReplaysSessionIdRoute - ServicesServiceNameRoute: typeof ServicesServiceNameRoute - TracesTraceIdRoute: typeof TracesTraceIdRoute - AlertsIndexRoute: typeof AlertsIndexRoute - AnalyticsIndexRoute: typeof AnalyticsIndexRoute - AnomaliesIndexRoute: typeof AnomaliesIndexRoute - DashboardsIndexRoute: typeof DashboardsIndexRoute - ErrorsIndexRoute: typeof ErrorsIndexRoute - InfraIndexRoute: typeof InfraIndexRoute - InvestigationsIndexRoute: typeof InvestigationsIndexRoute - LogsIndexRoute: typeof LogsIndexRoute - MetricsIndexRoute: typeof MetricsIndexRoute - ReplaysIndexRoute: typeof ReplaysIndexRoute - ServicesIndexRoute: typeof ServicesIndexRoute - TracesIndexRoute: typeof TracesIndexRoute - AlertsIncidentsIncidentIdRoute: typeof AlertsIncidentsIncidentIdRoute - ErrorsIssuesIssueIdRoute: typeof ErrorsIssuesIssueIdRoute - InfraCloudflareZoneNameRoute: typeof InfraCloudflareZoneNameRoute - InfraPlanetscaleDbNameRoute: typeof InfraPlanetscaleDbNameRoute - ErrorsIssuesIndexRoute: typeof ErrorsIssuesIndexRoute - InfraCloudflareIndexRoute: typeof InfraCloudflareIndexRoute - InfraPlanetscaleIndexRoute: typeof InfraPlanetscaleIndexRoute - DashboardsDashboardIdWidgetsWidgetIdRoute: typeof DashboardsDashboardIdWidgetsWidgetIdRoute - InfraKubernetesNodesNodeNameRoute: typeof InfraKubernetesNodesNodeNameRoute - InfraKubernetesPodsPodNameRoute: typeof InfraKubernetesPodsPodNameRoute - InfraKubernetesNodesIndexRoute: typeof InfraKubernetesNodesIndexRoute - InfraKubernetesPodsIndexRoute: typeof InfraKubernetesPodsIndexRoute - InfraKubernetesWorkloadsIndexRoute: typeof InfraKubernetesWorkloadsIndexRoute - InfraKubernetesWorkloadsKindWorkloadNameRoute: typeof InfraKubernetesWorkloadsKindWorkloadNameRoute + IndexRoute: typeof IndexRoute + AccountRoute: typeof AccountRoute + ChatRoute: typeof ChatRoute + CliLoginRoute: typeof CliLoginRoute + ConnectorsRoute: typeof ConnectorsRoute + DeveloperRoute: typeof DeveloperRoute + FlowLabRoute: typeof FlowLabRoute + InfraBenchRoute: typeof InfraBenchRoute + IntegrationsRoute: typeof IntegrationsRoute + LogsBenchRoute: typeof LogsBenchRoute + McpRoute: typeof McpRoute + McpAuthorizeRoute: typeof McpAuthorizeRoute + NodeLabRoute: typeof NodeLabRoute + OrgRequiredRoute: typeof OrgRequiredRoute + OverviewBenchRoute: typeof OverviewBenchRoute + QueryBuilderLabRoute: typeof QueryBuilderLabRoute + QuickStartRoute: typeof QuickStartRoute + SelectPlanRoute: typeof SelectPlanRoute + ServiceDetailBenchRoute: typeof ServiceDetailBenchRoute + ServiceMapRoute: typeof ServiceMapRoute + ServiceMapBenchRoute: typeof ServiceMapBenchRoute + SettingsRoute: typeof SettingsRoute + SignInRoute: typeof SignInRoute + SignUpRoute: typeof SignUpRoute + TimelineLabRoute: typeof TimelineLabRoute + WidgetLabRoute: typeof WidgetLabRoute + AlertsRuleIdRoute: typeof AlertsRuleIdRoute + AlertsCreateRoute: typeof AlertsCreateRoute + AnomaliesIncidentIdRoute: typeof AnomaliesIncidentIdRoute + DashboardsDashboardIdRoute: typeof DashboardsDashboardIdRoute + DashboardsTemplatesRoute: typeof DashboardsTemplatesRoute + ErrorsErrorTypeRoute: typeof ErrorsErrorTypeRoute + InfraHostNameRoute: typeof InfraHostNameRoute + InvestigationsIdRoute: typeof InvestigationsIdRoute + LogsLogIdRoute: typeof LogsLogIdRoute + MetricsMetricNameRoute: typeof MetricsMetricNameRoute + RecommendationsRecommendationKeyRoute: typeof RecommendationsRecommendationKeyRoute + ReplaysSessionIdRoute: typeof ReplaysSessionIdRoute + ServicesServiceNameRoute: typeof ServicesServiceNameRoute + TracesTraceIdRoute: typeof TracesTraceIdRoute + AlertsIndexRoute: typeof AlertsIndexRoute + AnalyticsIndexRoute: typeof AnalyticsIndexRoute + AnomaliesIndexRoute: typeof AnomaliesIndexRoute + DashboardsIndexRoute: typeof DashboardsIndexRoute + ErrorsIndexRoute: typeof ErrorsIndexRoute + InfraIndexRoute: typeof InfraIndexRoute + InvestigationsIndexRoute: typeof InvestigationsIndexRoute + LogsIndexRoute: typeof LogsIndexRoute + MetricsIndexRoute: typeof MetricsIndexRoute + ReplaysIndexRoute: typeof ReplaysIndexRoute + ServicesIndexRoute: typeof ServicesIndexRoute + TracesIndexRoute: typeof TracesIndexRoute + AlertsIncidentsIncidentIdRoute: typeof AlertsIncidentsIncidentIdRoute + ErrorsIssuesIssueIdRoute: typeof ErrorsIssuesIssueIdRoute + InfraCloudflareZoneNameRoute: typeof InfraCloudflareZoneNameRoute + InfraPlanetscaleDbNameRoute: typeof InfraPlanetscaleDbNameRoute + ServicesServiceNameEndpointsRoute: typeof ServicesServiceNameEndpointsRoute + ErrorsIssuesIndexRoute: typeof ErrorsIssuesIndexRoute + InfraCloudflareIndexRoute: typeof InfraCloudflareIndexRoute + InfraPlanetscaleIndexRoute: typeof InfraPlanetscaleIndexRoute + DashboardsDashboardIdWidgetsWidgetIdRoute: typeof DashboardsDashboardIdWidgetsWidgetIdRoute + InfraKubernetesNodesNodeNameRoute: typeof InfraKubernetesNodesNodeNameRoute + InfraKubernetesPodsPodNameRoute: typeof InfraKubernetesPodsPodNameRoute + InfraKubernetesNodesIndexRoute: typeof InfraKubernetesNodesIndexRoute + InfraKubernetesPodsIndexRoute: typeof InfraKubernetesPodsIndexRoute + InfraKubernetesWorkloadsIndexRoute: typeof InfraKubernetesWorkloadsIndexRoute + InfraKubernetesWorkloadsKindWorkloadNameRoute: typeof InfraKubernetesWorkloadsKindWorkloadNameRoute } -declare module "@tanstack/react-router" { - interface FileRoutesByPath { - "/": { - id: "/" - path: "/" - fullPath: "/" - preLoaderRoute: typeof IndexRouteImport - parentRoute: typeof rootRouteImport - } - "/account": { - id: "/account" - path: "/account" - fullPath: "/account" - preLoaderRoute: typeof AccountRouteImport - parentRoute: typeof rootRouteImport - } - "/chat": { - id: "/chat" - path: "/chat" - fullPath: "/chat" - preLoaderRoute: typeof ChatRouteImport - parentRoute: typeof rootRouteImport - } - "/cli-login": { - id: "/cli-login" - path: "/cli-login" - fullPath: "/cli-login" - preLoaderRoute: typeof CliLoginRouteImport - parentRoute: typeof rootRouteImport - } - "/connectors": { - id: "/connectors" - path: "/connectors" - fullPath: "/connectors" - preLoaderRoute: typeof ConnectorsRouteImport - parentRoute: typeof rootRouteImport - } - "/developer": { - id: "/developer" - path: "/developer" - fullPath: "/developer" - preLoaderRoute: typeof DeveloperRouteImport - parentRoute: typeof rootRouteImport - } - "/flow-lab": { - id: "/flow-lab" - path: "/flow-lab" - fullPath: "/flow-lab" - preLoaderRoute: typeof FlowLabRouteImport - parentRoute: typeof rootRouteImport - } - "/infra-bench": { - id: "/infra-bench" - path: "/infra-bench" - fullPath: "/infra-bench" - preLoaderRoute: typeof InfraBenchRouteImport - parentRoute: typeof rootRouteImport - } - "/integrations": { - id: "/integrations" - path: "/integrations" - fullPath: "/integrations" - preLoaderRoute: typeof IntegrationsRouteImport - parentRoute: typeof rootRouteImport - } - "/logs-bench": { - id: "/logs-bench" - path: "/logs-bench" - fullPath: "/logs-bench" - preLoaderRoute: typeof LogsBenchRouteImport - parentRoute: typeof rootRouteImport - } - "/mcp": { - id: "/mcp" - path: "/mcp" - fullPath: "/mcp" - preLoaderRoute: typeof McpRouteImport - parentRoute: typeof rootRouteImport - } - "/mcp-authorize": { - id: "/mcp-authorize" - path: "/mcp-authorize" - fullPath: "/mcp-authorize" - preLoaderRoute: typeof McpAuthorizeRouteImport - parentRoute: typeof rootRouteImport - } - "/node-lab": { - id: "/node-lab" - path: "/node-lab" - fullPath: "/node-lab" - preLoaderRoute: typeof NodeLabRouteImport - parentRoute: typeof rootRouteImport - } - "/org-required": { - id: "/org-required" - path: "/org-required" - fullPath: "/org-required" - preLoaderRoute: typeof OrgRequiredRouteImport - parentRoute: typeof rootRouteImport - } - "/overview-bench": { - id: "/overview-bench" - path: "/overview-bench" - fullPath: "/overview-bench" - preLoaderRoute: typeof OverviewBenchRouteImport - parentRoute: typeof rootRouteImport - } - "/query-builder-lab": { - id: "/query-builder-lab" - path: "/query-builder-lab" - fullPath: "/query-builder-lab" - preLoaderRoute: typeof QueryBuilderLabRouteImport - parentRoute: typeof rootRouteImport - } - "/quick-start": { - id: "/quick-start" - path: "/quick-start" - fullPath: "/quick-start" - preLoaderRoute: typeof QuickStartRouteImport - parentRoute: typeof rootRouteImport - } - "/select-plan": { - id: "/select-plan" - path: "/select-plan" - fullPath: "/select-plan" - preLoaderRoute: typeof SelectPlanRouteImport - parentRoute: typeof rootRouteImport - } - "/service-detail-bench": { - id: "/service-detail-bench" - path: "/service-detail-bench" - fullPath: "/service-detail-bench" - preLoaderRoute: typeof ServiceDetailBenchRouteImport - parentRoute: typeof rootRouteImport - } - "/service-map": { - id: "/service-map" - path: "/service-map" - fullPath: "/service-map" - preLoaderRoute: typeof ServiceMapRouteImport - parentRoute: typeof rootRouteImport - } - "/service-map-bench": { - id: "/service-map-bench" - path: "/service-map-bench" - fullPath: "/service-map-bench" - preLoaderRoute: typeof ServiceMapBenchRouteImport - parentRoute: typeof rootRouteImport - } - "/settings": { - id: "/settings" - path: "/settings" - fullPath: "/settings" - preLoaderRoute: typeof SettingsRouteImport - parentRoute: typeof rootRouteImport - } - "/sign-in": { - id: "/sign-in" - path: "/sign-in" - fullPath: "/sign-in" - preLoaderRoute: typeof SignInRouteImport - parentRoute: typeof rootRouteImport - } - "/sign-up": { - id: "/sign-up" - path: "/sign-up" - fullPath: "/sign-up" - preLoaderRoute: typeof SignUpRouteImport - parentRoute: typeof rootRouteImport - } - "/timeline-lab": { - id: "/timeline-lab" - path: "/timeline-lab" - fullPath: "/timeline-lab" - preLoaderRoute: typeof TimelineLabRouteImport - parentRoute: typeof rootRouteImport - } - "/widget-lab": { - id: "/widget-lab" - path: "/widget-lab" - fullPath: "/widget-lab" - preLoaderRoute: typeof WidgetLabRouteImport - parentRoute: typeof rootRouteImport - } - "/alerts/": { - id: "/alerts/" - path: "/alerts" - fullPath: "/alerts/" - preLoaderRoute: typeof AlertsIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/alerts/$ruleId": { - id: "/alerts/$ruleId" - path: "/alerts/$ruleId" - fullPath: "/alerts/$ruleId" - preLoaderRoute: typeof AlertsRuleIdRouteImport - parentRoute: typeof rootRouteImport - } - "/alerts/create": { - id: "/alerts/create" - path: "/alerts/create" - fullPath: "/alerts/create" - preLoaderRoute: typeof AlertsCreateRouteImport - parentRoute: typeof rootRouteImport - } - "/analytics/": { - id: "/analytics/" - path: "/analytics" - fullPath: "/analytics/" - preLoaderRoute: typeof AnalyticsIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/anomalies/": { - id: "/anomalies/" - path: "/anomalies" - fullPath: "/anomalies/" - preLoaderRoute: typeof AnomaliesIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/anomalies/$incidentId": { - id: "/anomalies/$incidentId" - path: "/anomalies/$incidentId" - fullPath: "/anomalies/$incidentId" - preLoaderRoute: typeof AnomaliesIncidentIdRouteImport - parentRoute: typeof rootRouteImport - } - "/dashboards/": { - id: "/dashboards/" - path: "/dashboards" - fullPath: "/dashboards/" - preLoaderRoute: typeof DashboardsIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/dashboards/$dashboardId": { - id: "/dashboards/$dashboardId" - path: "/dashboards/$dashboardId" - fullPath: "/dashboards/$dashboardId" - preLoaderRoute: typeof DashboardsDashboardIdRouteImport - parentRoute: typeof rootRouteImport - } - "/dashboards/templates": { - id: "/dashboards/templates" - path: "/dashboards/templates" - fullPath: "/dashboards/templates" - preLoaderRoute: typeof DashboardsTemplatesRouteImport - parentRoute: typeof rootRouteImport - } - "/errors/": { - id: "/errors/" - path: "/errors" - fullPath: "/errors/" - preLoaderRoute: typeof ErrorsIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/errors/$errorType": { - id: "/errors/$errorType" - path: "/errors/$errorType" - fullPath: "/errors/$errorType" - preLoaderRoute: typeof ErrorsErrorTypeRouteImport - parentRoute: typeof rootRouteImport - } - "/infra/": { - id: "/infra/" - path: "/infra" - fullPath: "/infra/" - preLoaderRoute: typeof InfraIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/infra/$hostName": { - id: "/infra/$hostName" - path: "/infra/$hostName" - fullPath: "/infra/$hostName" - preLoaderRoute: typeof InfraHostNameRouteImport - parentRoute: typeof rootRouteImport - } - "/investigations/": { - id: "/investigations/" - path: "/investigations" - fullPath: "/investigations/" - preLoaderRoute: typeof InvestigationsIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/investigations/$id": { - id: "/investigations/$id" - path: "/investigations/$id" - fullPath: "/investigations/$id" - preLoaderRoute: typeof InvestigationsIdRouteImport - parentRoute: typeof rootRouteImport - } - "/logs/": { - id: "/logs/" - path: "/logs" - fullPath: "/logs/" - preLoaderRoute: typeof LogsIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/logs/$logId": { - id: "/logs/$logId" - path: "/logs/$logId" - fullPath: "/logs/$logId" - preLoaderRoute: typeof LogsLogIdRouteImport - parentRoute: typeof rootRouteImport - } - "/metrics/": { - id: "/metrics/" - path: "/metrics" - fullPath: "/metrics/" - preLoaderRoute: typeof MetricsIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/metrics/$metricName": { - id: "/metrics/$metricName" - path: "/metrics/$metricName" - fullPath: "/metrics/$metricName" - preLoaderRoute: typeof MetricsMetricNameRouteImport - parentRoute: typeof rootRouteImport - } - "/recommendations/$recommendationKey": { - id: "/recommendations/$recommendationKey" - path: "/recommendations/$recommendationKey" - fullPath: "/recommendations/$recommendationKey" - preLoaderRoute: typeof RecommendationsRecommendationKeyRouteImport - parentRoute: typeof rootRouteImport - } - "/replays/": { - id: "/replays/" - path: "/replays" - fullPath: "/replays/" - preLoaderRoute: typeof ReplaysIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/replays/$sessionId": { - id: "/replays/$sessionId" - path: "/replays/$sessionId" - fullPath: "/replays/$sessionId" - preLoaderRoute: typeof ReplaysSessionIdRouteImport - parentRoute: typeof rootRouteImport - } - "/services/": { - id: "/services/" - path: "/services" - fullPath: "/services/" - preLoaderRoute: typeof ServicesIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/services/$serviceName": { - id: "/services/$serviceName" - path: "/services/$serviceName" - fullPath: "/services/$serviceName" - preLoaderRoute: typeof ServicesServiceNameRouteImport - parentRoute: typeof rootRouteImport - } - "/traces/": { - id: "/traces/" - path: "/traces" - fullPath: "/traces/" - preLoaderRoute: typeof TracesIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/traces/$traceId": { - id: "/traces/$traceId" - path: "/traces/$traceId" - fullPath: "/traces/$traceId" - preLoaderRoute: typeof TracesTraceIdRouteImport - parentRoute: typeof rootRouteImport - } - "/alerts/incidents/$incidentId": { - id: "/alerts/incidents/$incidentId" - path: "/alerts/incidents/$incidentId" - fullPath: "/alerts/incidents/$incidentId" - preLoaderRoute: typeof AlertsIncidentsIncidentIdRouteImport - parentRoute: typeof rootRouteImport - } - "/errors/issues/": { - id: "/errors/issues/" - path: "/errors/issues" - fullPath: "/errors/issues/" - preLoaderRoute: typeof ErrorsIssuesIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/errors/issues/$issueId": { - id: "/errors/issues/$issueId" - path: "/errors/issues/$issueId" - fullPath: "/errors/issues/$issueId" - preLoaderRoute: typeof ErrorsIssuesIssueIdRouteImport - parentRoute: typeof rootRouteImport - } - "/infra/cloudflare/": { - id: "/infra/cloudflare/" - path: "/infra/cloudflare" - fullPath: "/infra/cloudflare/" - preLoaderRoute: typeof InfraCloudflareIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/infra/cloudflare/$zoneName": { - id: "/infra/cloudflare/$zoneName" - path: "/infra/cloudflare/$zoneName" - fullPath: "/infra/cloudflare/$zoneName" - preLoaderRoute: typeof InfraCloudflareZoneNameRouteImport - parentRoute: typeof rootRouteImport - } - "/infra/planetscale/": { - id: "/infra/planetscale/" - path: "/infra/planetscale" - fullPath: "/infra/planetscale/" - preLoaderRoute: typeof InfraPlanetscaleIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/infra/planetscale/$dbName": { - id: "/infra/planetscale/$dbName" - path: "/infra/planetscale/$dbName" - fullPath: "/infra/planetscale/$dbName" - preLoaderRoute: typeof InfraPlanetscaleDbNameRouteImport - parentRoute: typeof rootRouteImport - } - "/dashboards/$dashboardId_/widgets/$widgetId": { - id: "/dashboards/$dashboardId_/widgets/$widgetId" - path: "/dashboards/$dashboardId/widgets/$widgetId" - fullPath: "/dashboards/$dashboardId/widgets/$widgetId" - preLoaderRoute: typeof DashboardsDashboardIdWidgetsWidgetIdRouteImport - parentRoute: typeof rootRouteImport - } - "/infra/kubernetes/nodes/": { - id: "/infra/kubernetes/nodes/" - path: "/infra/kubernetes/nodes" - fullPath: "/infra/kubernetes/nodes/" - preLoaderRoute: typeof InfraKubernetesNodesIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/infra/kubernetes/nodes/$nodeName": { - id: "/infra/kubernetes/nodes/$nodeName" - path: "/infra/kubernetes/nodes/$nodeName" - fullPath: "/infra/kubernetes/nodes/$nodeName" - preLoaderRoute: typeof InfraKubernetesNodesNodeNameRouteImport - parentRoute: typeof rootRouteImport - } - "/infra/kubernetes/pods/": { - id: "/infra/kubernetes/pods/" - path: "/infra/kubernetes/pods" - fullPath: "/infra/kubernetes/pods/" - preLoaderRoute: typeof InfraKubernetesPodsIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/infra/kubernetes/pods/$podName": { - id: "/infra/kubernetes/pods/$podName" - path: "/infra/kubernetes/pods/$podName" - fullPath: "/infra/kubernetes/pods/$podName" - preLoaderRoute: typeof InfraKubernetesPodsPodNameRouteImport - parentRoute: typeof rootRouteImport - } - "/infra/kubernetes/workloads/": { - id: "/infra/kubernetes/workloads/" - path: "/infra/kubernetes/workloads" - fullPath: "/infra/kubernetes/workloads/" - preLoaderRoute: typeof InfraKubernetesWorkloadsIndexRouteImport - parentRoute: typeof rootRouteImport - } - "/infra/kubernetes/workloads/$kind/$workloadName": { - id: "/infra/kubernetes/workloads/$kind/$workloadName" - path: "/infra/kubernetes/workloads/$kind/$workloadName" - fullPath: "/infra/kubernetes/workloads/$kind/$workloadName" - preLoaderRoute: typeof InfraKubernetesWorkloadsKindWorkloadNameRouteImport - parentRoute: typeof rootRouteImport - } - } +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/account': { + id: '/account' + path: '/account' + fullPath: '/account' + preLoaderRoute: typeof AccountRouteImport + parentRoute: typeof rootRouteImport + } + '/chat': { + id: '/chat' + path: '/chat' + fullPath: '/chat' + preLoaderRoute: typeof ChatRouteImport + parentRoute: typeof rootRouteImport + } + '/cli-login': { + id: '/cli-login' + path: '/cli-login' + fullPath: '/cli-login' + preLoaderRoute: typeof CliLoginRouteImport + parentRoute: typeof rootRouteImport + } + '/connectors': { + id: '/connectors' + path: '/connectors' + fullPath: '/connectors' + preLoaderRoute: typeof ConnectorsRouteImport + parentRoute: typeof rootRouteImport + } + '/developer': { + id: '/developer' + path: '/developer' + fullPath: '/developer' + preLoaderRoute: typeof DeveloperRouteImport + parentRoute: typeof rootRouteImport + } + '/flow-lab': { + id: '/flow-lab' + path: '/flow-lab' + fullPath: '/flow-lab' + preLoaderRoute: typeof FlowLabRouteImport + parentRoute: typeof rootRouteImport + } + '/infra-bench': { + id: '/infra-bench' + path: '/infra-bench' + fullPath: '/infra-bench' + preLoaderRoute: typeof InfraBenchRouteImport + parentRoute: typeof rootRouteImport + } + '/integrations': { + id: '/integrations' + path: '/integrations' + fullPath: '/integrations' + preLoaderRoute: typeof IntegrationsRouteImport + parentRoute: typeof rootRouteImport + } + '/logs-bench': { + id: '/logs-bench' + path: '/logs-bench' + fullPath: '/logs-bench' + preLoaderRoute: typeof LogsBenchRouteImport + parentRoute: typeof rootRouteImport + } + '/mcp': { + id: '/mcp' + path: '/mcp' + fullPath: '/mcp' + preLoaderRoute: typeof McpRouteImport + parentRoute: typeof rootRouteImport + } + '/mcp-authorize': { + id: '/mcp-authorize' + path: '/mcp-authorize' + fullPath: '/mcp-authorize' + preLoaderRoute: typeof McpAuthorizeRouteImport + parentRoute: typeof rootRouteImport + } + '/node-lab': { + id: '/node-lab' + path: '/node-lab' + fullPath: '/node-lab' + preLoaderRoute: typeof NodeLabRouteImport + parentRoute: typeof rootRouteImport + } + '/org-required': { + id: '/org-required' + path: '/org-required' + fullPath: '/org-required' + preLoaderRoute: typeof OrgRequiredRouteImport + parentRoute: typeof rootRouteImport + } + '/overview-bench': { + id: '/overview-bench' + path: '/overview-bench' + fullPath: '/overview-bench' + preLoaderRoute: typeof OverviewBenchRouteImport + parentRoute: typeof rootRouteImport + } + '/query-builder-lab': { + id: '/query-builder-lab' + path: '/query-builder-lab' + fullPath: '/query-builder-lab' + preLoaderRoute: typeof QueryBuilderLabRouteImport + parentRoute: typeof rootRouteImport + } + '/quick-start': { + id: '/quick-start' + path: '/quick-start' + fullPath: '/quick-start' + preLoaderRoute: typeof QuickStartRouteImport + parentRoute: typeof rootRouteImport + } + '/select-plan': { + id: '/select-plan' + path: '/select-plan' + fullPath: '/select-plan' + preLoaderRoute: typeof SelectPlanRouteImport + parentRoute: typeof rootRouteImport + } + '/service-detail-bench': { + id: '/service-detail-bench' + path: '/service-detail-bench' + fullPath: '/service-detail-bench' + preLoaderRoute: typeof ServiceDetailBenchRouteImport + parentRoute: typeof rootRouteImport + } + '/service-map': { + id: '/service-map' + path: '/service-map' + fullPath: '/service-map' + preLoaderRoute: typeof ServiceMapRouteImport + parentRoute: typeof rootRouteImport + } + '/service-map-bench': { + id: '/service-map-bench' + path: '/service-map-bench' + fullPath: '/service-map-bench' + preLoaderRoute: typeof ServiceMapBenchRouteImport + parentRoute: typeof rootRouteImport + } + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/sign-in': { + id: '/sign-in' + path: '/sign-in' + fullPath: '/sign-in' + preLoaderRoute: typeof SignInRouteImport + parentRoute: typeof rootRouteImport + } + '/sign-up': { + id: '/sign-up' + path: '/sign-up' + fullPath: '/sign-up' + preLoaderRoute: typeof SignUpRouteImport + parentRoute: typeof rootRouteImport + } + '/timeline-lab': { + id: '/timeline-lab' + path: '/timeline-lab' + fullPath: '/timeline-lab' + preLoaderRoute: typeof TimelineLabRouteImport + parentRoute: typeof rootRouteImport + } + '/widget-lab': { + id: '/widget-lab' + path: '/widget-lab' + fullPath: '/widget-lab' + preLoaderRoute: typeof WidgetLabRouteImport + parentRoute: typeof rootRouteImport + } + '/alerts/': { + id: '/alerts/' + path: '/alerts' + fullPath: '/alerts/' + preLoaderRoute: typeof AlertsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/alerts/$ruleId': { + id: '/alerts/$ruleId' + path: '/alerts/$ruleId' + fullPath: '/alerts/$ruleId' + preLoaderRoute: typeof AlertsRuleIdRouteImport + parentRoute: typeof rootRouteImport + } + '/alerts/create': { + id: '/alerts/create' + path: '/alerts/create' + fullPath: '/alerts/create' + preLoaderRoute: typeof AlertsCreateRouteImport + parentRoute: typeof rootRouteImport + } + '/analytics/': { + id: '/analytics/' + path: '/analytics' + fullPath: '/analytics/' + preLoaderRoute: typeof AnalyticsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/anomalies/': { + id: '/anomalies/' + path: '/anomalies' + fullPath: '/anomalies/' + preLoaderRoute: typeof AnomaliesIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/anomalies/$incidentId': { + id: '/anomalies/$incidentId' + path: '/anomalies/$incidentId' + fullPath: '/anomalies/$incidentId' + preLoaderRoute: typeof AnomaliesIncidentIdRouteImport + parentRoute: typeof rootRouteImport + } + '/dashboards/': { + id: '/dashboards/' + path: '/dashboards' + fullPath: '/dashboards/' + preLoaderRoute: typeof DashboardsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/dashboards/$dashboardId': { + id: '/dashboards/$dashboardId' + path: '/dashboards/$dashboardId' + fullPath: '/dashboards/$dashboardId' + preLoaderRoute: typeof DashboardsDashboardIdRouteImport + parentRoute: typeof rootRouteImport + } + '/dashboards/templates': { + id: '/dashboards/templates' + path: '/dashboards/templates' + fullPath: '/dashboards/templates' + preLoaderRoute: typeof DashboardsTemplatesRouteImport + parentRoute: typeof rootRouteImport + } + '/errors/': { + id: '/errors/' + path: '/errors' + fullPath: '/errors/' + preLoaderRoute: typeof ErrorsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/errors/$errorType': { + id: '/errors/$errorType' + path: '/errors/$errorType' + fullPath: '/errors/$errorType' + preLoaderRoute: typeof ErrorsErrorTypeRouteImport + parentRoute: typeof rootRouteImport + } + '/infra/': { + id: '/infra/' + path: '/infra' + fullPath: '/infra/' + preLoaderRoute: typeof InfraIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/infra/$hostName': { + id: '/infra/$hostName' + path: '/infra/$hostName' + fullPath: '/infra/$hostName' + preLoaderRoute: typeof InfraHostNameRouteImport + parentRoute: typeof rootRouteImport + } + '/investigations/': { + id: '/investigations/' + path: '/investigations' + fullPath: '/investigations/' + preLoaderRoute: typeof InvestigationsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/investigations/$id': { + id: '/investigations/$id' + path: '/investigations/$id' + fullPath: '/investigations/$id' + preLoaderRoute: typeof InvestigationsIdRouteImport + parentRoute: typeof rootRouteImport + } + '/logs/': { + id: '/logs/' + path: '/logs' + fullPath: '/logs/' + preLoaderRoute: typeof LogsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/logs/$logId': { + id: '/logs/$logId' + path: '/logs/$logId' + fullPath: '/logs/$logId' + preLoaderRoute: typeof LogsLogIdRouteImport + parentRoute: typeof rootRouteImport + } + '/metrics/': { + id: '/metrics/' + path: '/metrics' + fullPath: '/metrics/' + preLoaderRoute: typeof MetricsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/metrics/$metricName': { + id: '/metrics/$metricName' + path: '/metrics/$metricName' + fullPath: '/metrics/$metricName' + preLoaderRoute: typeof MetricsMetricNameRouteImport + parentRoute: typeof rootRouteImport + } + '/recommendations/$recommendationKey': { + id: '/recommendations/$recommendationKey' + path: '/recommendations/$recommendationKey' + fullPath: '/recommendations/$recommendationKey' + preLoaderRoute: typeof RecommendationsRecommendationKeyRouteImport + parentRoute: typeof rootRouteImport + } + '/replays/': { + id: '/replays/' + path: '/replays' + fullPath: '/replays/' + preLoaderRoute: typeof ReplaysIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/replays/$sessionId': { + id: '/replays/$sessionId' + path: '/replays/$sessionId' + fullPath: '/replays/$sessionId' + preLoaderRoute: typeof ReplaysSessionIdRouteImport + parentRoute: typeof rootRouteImport + } + '/services/': { + id: '/services/' + path: '/services' + fullPath: '/services/' + preLoaderRoute: typeof ServicesIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/services/$serviceName': { + id: '/services/$serviceName' + path: '/services/$serviceName' + fullPath: '/services/$serviceName' + preLoaderRoute: typeof ServicesServiceNameRouteImport + parentRoute: typeof rootRouteImport + } + '/traces/': { + id: '/traces/' + path: '/traces' + fullPath: '/traces/' + preLoaderRoute: typeof TracesIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/traces/$traceId': { + id: '/traces/$traceId' + path: '/traces/$traceId' + fullPath: '/traces/$traceId' + preLoaderRoute: typeof TracesTraceIdRouteImport + parentRoute: typeof rootRouteImport + } + '/alerts/incidents/$incidentId': { + id: '/alerts/incidents/$incidentId' + path: '/alerts/incidents/$incidentId' + fullPath: '/alerts/incidents/$incidentId' + preLoaderRoute: typeof AlertsIncidentsIncidentIdRouteImport + parentRoute: typeof rootRouteImport + } + '/errors/issues/': { + id: '/errors/issues/' + path: '/errors/issues' + fullPath: '/errors/issues/' + preLoaderRoute: typeof ErrorsIssuesIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/errors/issues/$issueId': { + id: '/errors/issues/$issueId' + path: '/errors/issues/$issueId' + fullPath: '/errors/issues/$issueId' + preLoaderRoute: typeof ErrorsIssuesIssueIdRouteImport + parentRoute: typeof rootRouteImport + } + '/infra/cloudflare/': { + id: '/infra/cloudflare/' + path: '/infra/cloudflare' + fullPath: '/infra/cloudflare/' + preLoaderRoute: typeof InfraCloudflareIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/infra/cloudflare/$zoneName': { + id: '/infra/cloudflare/$zoneName' + path: '/infra/cloudflare/$zoneName' + fullPath: '/infra/cloudflare/$zoneName' + preLoaderRoute: typeof InfraCloudflareZoneNameRouteImport + parentRoute: typeof rootRouteImport + } + '/infra/planetscale/': { + id: '/infra/planetscale/' + path: '/infra/planetscale' + fullPath: '/infra/planetscale/' + preLoaderRoute: typeof InfraPlanetscaleIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/infra/planetscale/$dbName': { + id: '/infra/planetscale/$dbName' + path: '/infra/planetscale/$dbName' + fullPath: '/infra/planetscale/$dbName' + preLoaderRoute: typeof InfraPlanetscaleDbNameRouteImport + parentRoute: typeof rootRouteImport + } + '/services/$serviceName_/endpoints': { + id: '/services/$serviceName_/endpoints' + path: '/services/$serviceName/endpoints' + fullPath: '/services/$serviceName/endpoints' + preLoaderRoute: typeof ServicesServiceNameEndpointsRouteImport + parentRoute: typeof rootRouteImport + } + '/dashboards/$dashboardId_/widgets/$widgetId': { + id: '/dashboards/$dashboardId_/widgets/$widgetId' + path: '/dashboards/$dashboardId/widgets/$widgetId' + fullPath: '/dashboards/$dashboardId/widgets/$widgetId' + preLoaderRoute: typeof DashboardsDashboardIdWidgetsWidgetIdRouteImport + parentRoute: typeof rootRouteImport + } + '/infra/kubernetes/nodes/': { + id: '/infra/kubernetes/nodes/' + path: '/infra/kubernetes/nodes' + fullPath: '/infra/kubernetes/nodes/' + preLoaderRoute: typeof InfraKubernetesNodesIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/infra/kubernetes/nodes/$nodeName': { + id: '/infra/kubernetes/nodes/$nodeName' + path: '/infra/kubernetes/nodes/$nodeName' + fullPath: '/infra/kubernetes/nodes/$nodeName' + preLoaderRoute: typeof InfraKubernetesNodesNodeNameRouteImport + parentRoute: typeof rootRouteImport + } + '/infra/kubernetes/pods/': { + id: '/infra/kubernetes/pods/' + path: '/infra/kubernetes/pods' + fullPath: '/infra/kubernetes/pods/' + preLoaderRoute: typeof InfraKubernetesPodsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/infra/kubernetes/pods/$podName': { + id: '/infra/kubernetes/pods/$podName' + path: '/infra/kubernetes/pods/$podName' + fullPath: '/infra/kubernetes/pods/$podName' + preLoaderRoute: typeof InfraKubernetesPodsPodNameRouteImport + parentRoute: typeof rootRouteImport + } + '/infra/kubernetes/workloads/': { + id: '/infra/kubernetes/workloads/' + path: '/infra/kubernetes/workloads' + fullPath: '/infra/kubernetes/workloads/' + preLoaderRoute: typeof InfraKubernetesWorkloadsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/infra/kubernetes/workloads/$kind/$workloadName': { + id: '/infra/kubernetes/workloads/$kind/$workloadName' + path: '/infra/kubernetes/workloads/$kind/$workloadName' + fullPath: '/infra/kubernetes/workloads/$kind/$workloadName' + preLoaderRoute: typeof InfraKubernetesWorkloadsKindWorkloadNameRouteImport + parentRoute: typeof rootRouteImport + } + } } const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, - AccountRoute: AccountRoute, - ChatRoute: ChatRoute, - CliLoginRoute: CliLoginRoute, - ConnectorsRoute: ConnectorsRoute, - DeveloperRoute: DeveloperRoute, - FlowLabRoute: FlowLabRoute, - InfraBenchRoute: InfraBenchRoute, - IntegrationsRoute: IntegrationsRoute, - LogsBenchRoute: LogsBenchRoute, - McpRoute: McpRoute, - McpAuthorizeRoute: McpAuthorizeRoute, - NodeLabRoute: NodeLabRoute, - OrgRequiredRoute: OrgRequiredRoute, - OverviewBenchRoute: OverviewBenchRoute, - QueryBuilderLabRoute: QueryBuilderLabRoute, - QuickStartRoute: QuickStartRoute, - SelectPlanRoute: SelectPlanRoute, - ServiceDetailBenchRoute: ServiceDetailBenchRoute, - ServiceMapRoute: ServiceMapRoute, - ServiceMapBenchRoute: ServiceMapBenchRoute, - SettingsRoute: SettingsRoute, - SignInRoute: SignInRoute, - SignUpRoute: SignUpRoute, - TimelineLabRoute: TimelineLabRoute, - WidgetLabRoute: WidgetLabRoute, - AlertsRuleIdRoute: AlertsRuleIdRoute, - AlertsCreateRoute: AlertsCreateRoute, - AnomaliesIncidentIdRoute: AnomaliesIncidentIdRoute, - DashboardsDashboardIdRoute: DashboardsDashboardIdRoute, - DashboardsTemplatesRoute: DashboardsTemplatesRoute, - ErrorsErrorTypeRoute: ErrorsErrorTypeRoute, - InfraHostNameRoute: InfraHostNameRoute, - InvestigationsIdRoute: InvestigationsIdRoute, - LogsLogIdRoute: LogsLogIdRoute, - MetricsMetricNameRoute: MetricsMetricNameRoute, - RecommendationsRecommendationKeyRoute: RecommendationsRecommendationKeyRoute, - ReplaysSessionIdRoute: ReplaysSessionIdRoute, - ServicesServiceNameRoute: ServicesServiceNameRoute, - TracesTraceIdRoute: TracesTraceIdRoute, - AlertsIndexRoute: AlertsIndexRoute, - AnalyticsIndexRoute: AnalyticsIndexRoute, - AnomaliesIndexRoute: AnomaliesIndexRoute, - DashboardsIndexRoute: DashboardsIndexRoute, - ErrorsIndexRoute: ErrorsIndexRoute, - InfraIndexRoute: InfraIndexRoute, - InvestigationsIndexRoute: InvestigationsIndexRoute, - LogsIndexRoute: LogsIndexRoute, - MetricsIndexRoute: MetricsIndexRoute, - ReplaysIndexRoute: ReplaysIndexRoute, - ServicesIndexRoute: ServicesIndexRoute, - TracesIndexRoute: TracesIndexRoute, - AlertsIncidentsIncidentIdRoute: AlertsIncidentsIncidentIdRoute, - ErrorsIssuesIssueIdRoute: ErrorsIssuesIssueIdRoute, - InfraCloudflareZoneNameRoute: InfraCloudflareZoneNameRoute, - InfraPlanetscaleDbNameRoute: InfraPlanetscaleDbNameRoute, - ErrorsIssuesIndexRoute: ErrorsIssuesIndexRoute, - InfraCloudflareIndexRoute: InfraCloudflareIndexRoute, - InfraPlanetscaleIndexRoute: InfraPlanetscaleIndexRoute, - DashboardsDashboardIdWidgetsWidgetIdRoute: DashboardsDashboardIdWidgetsWidgetIdRoute, - InfraKubernetesNodesNodeNameRoute: InfraKubernetesNodesNodeNameRoute, - InfraKubernetesPodsPodNameRoute: InfraKubernetesPodsPodNameRoute, - InfraKubernetesNodesIndexRoute: InfraKubernetesNodesIndexRoute, - InfraKubernetesPodsIndexRoute: InfraKubernetesPodsIndexRoute, - InfraKubernetesWorkloadsIndexRoute: InfraKubernetesWorkloadsIndexRoute, - InfraKubernetesWorkloadsKindWorkloadNameRoute: InfraKubernetesWorkloadsKindWorkloadNameRoute, + IndexRoute: IndexRoute, + AccountRoute: AccountRoute, + ChatRoute: ChatRoute, + CliLoginRoute: CliLoginRoute, + ConnectorsRoute: ConnectorsRoute, + DeveloperRoute: DeveloperRoute, + FlowLabRoute: FlowLabRoute, + InfraBenchRoute: InfraBenchRoute, + IntegrationsRoute: IntegrationsRoute, + LogsBenchRoute: LogsBenchRoute, + McpRoute: McpRoute, + McpAuthorizeRoute: McpAuthorizeRoute, + NodeLabRoute: NodeLabRoute, + OrgRequiredRoute: OrgRequiredRoute, + OverviewBenchRoute: OverviewBenchRoute, + QueryBuilderLabRoute: QueryBuilderLabRoute, + QuickStartRoute: QuickStartRoute, + SelectPlanRoute: SelectPlanRoute, + ServiceDetailBenchRoute: ServiceDetailBenchRoute, + ServiceMapRoute: ServiceMapRoute, + ServiceMapBenchRoute: ServiceMapBenchRoute, + SettingsRoute: SettingsRoute, + SignInRoute: SignInRoute, + SignUpRoute: SignUpRoute, + TimelineLabRoute: TimelineLabRoute, + WidgetLabRoute: WidgetLabRoute, + AlertsRuleIdRoute: AlertsRuleIdRoute, + AlertsCreateRoute: AlertsCreateRoute, + AnomaliesIncidentIdRoute: AnomaliesIncidentIdRoute, + DashboardsDashboardIdRoute: DashboardsDashboardIdRoute, + DashboardsTemplatesRoute: DashboardsTemplatesRoute, + ErrorsErrorTypeRoute: ErrorsErrorTypeRoute, + InfraHostNameRoute: InfraHostNameRoute, + InvestigationsIdRoute: InvestigationsIdRoute, + LogsLogIdRoute: LogsLogIdRoute, + MetricsMetricNameRoute: MetricsMetricNameRoute, + RecommendationsRecommendationKeyRoute: RecommendationsRecommendationKeyRoute, + ReplaysSessionIdRoute: ReplaysSessionIdRoute, + ServicesServiceNameRoute: ServicesServiceNameRoute, + TracesTraceIdRoute: TracesTraceIdRoute, + AlertsIndexRoute: AlertsIndexRoute, + AnalyticsIndexRoute: AnalyticsIndexRoute, + AnomaliesIndexRoute: AnomaliesIndexRoute, + DashboardsIndexRoute: DashboardsIndexRoute, + ErrorsIndexRoute: ErrorsIndexRoute, + InfraIndexRoute: InfraIndexRoute, + InvestigationsIndexRoute: InvestigationsIndexRoute, + LogsIndexRoute: LogsIndexRoute, + MetricsIndexRoute: MetricsIndexRoute, + ReplaysIndexRoute: ReplaysIndexRoute, + ServicesIndexRoute: ServicesIndexRoute, + TracesIndexRoute: TracesIndexRoute, + AlertsIncidentsIncidentIdRoute: AlertsIncidentsIncidentIdRoute, + ErrorsIssuesIssueIdRoute: ErrorsIssuesIssueIdRoute, + InfraCloudflareZoneNameRoute: InfraCloudflareZoneNameRoute, + InfraPlanetscaleDbNameRoute: InfraPlanetscaleDbNameRoute, + ServicesServiceNameEndpointsRoute: ServicesServiceNameEndpointsRoute, + ErrorsIssuesIndexRoute: ErrorsIssuesIndexRoute, + InfraCloudflareIndexRoute: InfraCloudflareIndexRoute, + InfraPlanetscaleIndexRoute: InfraPlanetscaleIndexRoute, + DashboardsDashboardIdWidgetsWidgetIdRoute: + DashboardsDashboardIdWidgetsWidgetIdRoute, + InfraKubernetesNodesNodeNameRoute: InfraKubernetesNodesNodeNameRoute, + InfraKubernetesPodsPodNameRoute: InfraKubernetesPodsPodNameRoute, + InfraKubernetesNodesIndexRoute: InfraKubernetesNodesIndexRoute, + InfraKubernetesPodsIndexRoute: InfraKubernetesPodsIndexRoute, + InfraKubernetesWorkloadsIndexRoute: InfraKubernetesWorkloadsIndexRoute, + InfraKubernetesWorkloadsKindWorkloadNameRoute: + InfraKubernetesWorkloadsKindWorkloadNameRoute, } -export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes() +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/apps/web/src/routes/services/$serviceName.tsx b/apps/web/src/routes/services/$serviceName.tsx index 217d067d7..7092317bd 100644 --- a/apps/web/src/routes/services/$serviceName.tsx +++ b/apps/web/src/routes/services/$serviceName.tsx @@ -15,6 +15,8 @@ import { getServiceDetailThroughputRefinementResultAtom, } from "@/lib/services/atoms/warehouse-query-atoms" import { mergeExactThroughput } from "@/api/warehouse/custom-charts" +import type { ServiceDetailOverviewResult } from "@/api/warehouse/custom-charts" +import type { QueryAtomFailure } from "@/lib/services/atoms/warehouse-query-atoms" import type { ServiceDetailTimeSeriesPoint } from "@/api/warehouse/services" import { useCommitMarkers } from "@/components/vcs/commit-markers/use-commit-markers" import type { ReleasePoint } from "@/components/vcs/commit-markers/marker-layout" @@ -24,12 +26,16 @@ import { TimeRangeHeaderControls } from "@/components/time-range-picker/time-ran import { Button } from "@maple/ui/components/ui/button" import { BellIcon } from "@/components/icons" import { ServiceDependenciesTab } from "@/components/services/service-dependencies-tab" +import { ServiceEndpointsTab } from "@/components/services/service-endpoints-tab" import { ServiceOperationsTab } from "@/components/services/service-operations-tab" import { ServiceDependencyStrip } from "@/components/services/service-dependency-strip" import { ServiceEnvironmentSwitcher } from "@/components/services/service-environment-switcher" import { ServiceErrorsPanel } from "@/components/services/service-errors-panel" import { ServiceRecentDeploys } from "@/components/services/service-recent-deploys" -import { ServiceTopOperationsPanel } from "@/components/services/service-top-operations-panel" +import { + ServiceTopEndpointsPanel, + ServiceTopOperationsPanel, +} from "@/components/services/service-top-operations-panel" import { ServiceUsagePanel } from "@/components/services/service-usage-panel" import { ServiceWorkloadsPanel } from "@/components/services/service-workloads-panel" import { OptionalStringArrayParam } from "@/lib/search-params" @@ -43,7 +49,7 @@ import { LONG_RANGE_PRESET_OPTIONS } from "@/lib/time-utils" const EMPTY_RELEASES: ReadonlyArray = [] const ONE_YEAR_SECONDS = 365 * 24 * 60 * 60 -const ServiceDetailTab = Schema.Literals(["overview", "operations", "dependencies"]) +const ServiceDetailTab = Schema.Literals(["overview", "endpoints", "operations", "dependencies"]) type ServiceDetailTabValue = Schema.Schema.Type const decodeServiceDetailTab = Schema.decodeUnknownOption(ServiceDetailTab) @@ -145,10 +151,35 @@ function ServiceDetailContent() { [navigate], ) - const activeTab: ServiceDetailTabValue = Option.getOrElse( + // Lifted out of OverviewTab: the header's environment switcher and the tab + // list below both read it, so it must resolve on every tab — a deep link to + // `?tab=operations` still needs to know whether to offer Endpoints. Subscribing + // here rather than in two children also collapses the duplicate subscription. + const overviewAtom = getServiceDetailOverviewResultAtom({ + data: { + serviceName, + startTime: effectiveStartTime, + endTime: effectiveEndTime, + environments: search.environments, + }, + }) + const overviewResult = useRetainedRefreshableResultValue(overviewAtom) + + // Detection rides the overview bundle (see the `serviceDetailOverview` + // handler), so the Endpoints trigger appears with the first paint of the page + // rather than after a second round-trip. + const isHttpApi = Result.builder(overviewResult) + .onSuccess((r) => r.apiProfile.isHttpApi) + .orElse(() => false) + + const rawTab: ServiceDetailTabValue = Option.getOrElse( decodeServiceDetailTab(search.tab), (): ServiceDetailTabValue => "overview", ) + // A stale `?tab=endpoints` on a service that isn't an API (or one whose + // detection hasn't resolved yet) falls back rather than rendering a tab with + // no trigger. + const activeTab: ServiceDetailTabValue = rawTab === "endpoints" && !isHttpApi ? "overview" : rawTab const handleTabChange = useCallback( (value: unknown) => { const next = Option.getOrElse( @@ -180,6 +211,7 @@ function ServiceDetailContent() { const handleShowDependencies = useCallback(() => handleTabChange("dependencies"), [handleTabChange]) const handleShowOperations = useCallback(() => handleTabChange("operations"), [handleTabChange]) + const handleShowEndpoints = useCallback(() => handleTabChange("endpoints"), [handleTabChange]) return ( @@ -214,6 +246,17 @@ function ServiceDetailContent() { > Overview + {/* Only for services detected as HTTP APIs. Additive — the + generic Operations tab stays, since an API service still + has internal spans worth seeing. */} + {isHttpApi && ( + + Endpoints + + )} + )} + {activeTab === "endpoints" && ( + )} {activeTab === "operations" && ( @@ -307,8 +365,22 @@ interface OverviewTabProps { effectiveStartTime: string effectiveEndTime: string environments?: string[] + /** + * The overview bundle, subscribed by the page shell rather than here: the + * header's environment switcher and the tab list need it on every tab, so the + * subscription lives one level up and the atom + its result are passed down. + * Still one fetch for the whole tab (the switcher shares this atom key). + */ + /** Raw search params, forwarded to the endpoint detail route. */ + startTime?: string + endTime?: string + timePreset?: string + overviewAtom: ReturnType + overviewResult: Result.Result + isHttpApi: boolean onShowDependencies: () => void onShowOperations: () => void + onShowEndpoints: () => void } function OverviewTab({ @@ -316,21 +388,16 @@ function OverviewTab({ effectiveStartTime, effectiveEndTime, environments, + startTime, + endTime, + timePreset, + overviewAtom, + overviewResult, + isHttpApi, onShowDependencies, onShowOperations, + onShowEndpoints, }: OverviewTabProps) { - // One fetch for the whole Overview tab — the primary chart and the environment - // switcher's options (the switcher reads this same atom key, so it shares this - // round-trip instead of issuing its own overview query). - const overviewAtom = getServiceDetailOverviewResultAtom({ - data: { - serviceName, - startTime: effectiveStartTime, - endTime: effectiveEndTime, - environments, - }, - }) - const overviewResult = useRetainedRefreshableResultValue(overviewAtom) const refreshOverview = useAtomRefresh(overviewAtom) // Sampling verdict from the already-loaded primary chart. Drives a separate, @@ -448,13 +515,29 @@ function OverviewTab({ // of these metrics' tick labels (latency ms). yAxisWidth={72} /> - + {/* An API service leads with its endpoints; everything else leads with + raw operations. Both read the same atom key their tab uses, so the + tab that follows is a cache hit. */} + {isHttpApi ? ( + + ) : ( + + )}
    + + + ) +} + +function EndpointDetailContent() { + const { serviceName } = Route.useParams() + const search = Route.useSearch() + const navigate = useNavigate({ from: Route.fullPath }) + + const { startTime: effectiveStartTime, endTime: effectiveEndTime } = useEffectiveTimeRange( + search.startTime, + search.endTime, + search.timePreset ?? "12h", + ) + + // Every warehouse filter on this page keys on the display span name, which is + // what the endpoints table stored before splitting it for display. + const spanName = endpointSpanName(search.method, search.route) + + const handleTimeChange = useCallback( + ( + range: { startTime?: string; endTime?: string; presetValue?: string }, + options?: { replace?: boolean }, + ) => { + navigate({ + replace: options?.replace, + // Spread `prev` first: the endpoint identity is required search, so a + // reducer that only returns time keys would drop method/route. + search: (prev: EndpointSearch) => ({ ...prev, ...applyTimeRangeSearch(prev, range) }), + }) + }, + [navigate], + ) + + const handleEnvironmentChange = useCallback( + (environment: string | undefined) => { + navigate({ + search: (prev: EndpointSearch) => ({ + ...prev, + environments: environment ? [environment] : undefined, + }), + }) + }, + [navigate], + ) + + const chartsAtom = getEndpointDetailChartsResultAtom({ + data: { + serviceName, + startTime: effectiveStartTime, + endTime: effectiveEndTime, + environments: search.environments, + spanNames: [spanName], + }, + }) + const chartsResult = useRetainedRefreshableResultValue(chartsAtom) + const refreshCharts = useAtomRefresh(chartsAtom) + + const points: ReadonlyArray = useMemo( + () => + Result.builder(chartsResult) + .onSuccess((r) => r.data) + .orElse(() => []), + [chartsResult], + ) + + // Widened to the generic record shape MetricsGrid consumes; every field on a + // detail point is primitive, so this needs no `as unknown` round-trip. + const chartPoints: Record[] = useMemo( + () => points.map((point) => ({ ...point })), + [points], + ) + + const isChartsLoading = Result.isInitial(chartsResult) + const isWaiting = Result.isSuccess(chartsResult) && chartsResult.waiting + + const metrics = useMemo( + () => + ENDPOINT_CHARTS.map((chart) => ({ + id: chart.id, + chartId: chart.chartId, + title: chart.title, + layout: chart.layout, + data: chartPoints, + legend: chart.legend, + tooltip: chart.tooltip, + rateMode: chart.rateMode, + isLoading: isChartsLoading, + })), + [chartPoints, isChartsLoading], + ) + + return ( + + + + + + + + + {/* Same middle-elision as the table: the last segment is + what tells this endpoint apart from its siblings. */} + + + } + > +
    + +
    + + +
    +
    +
    +
    + + {Result.isFailure(chartsResult) ? ( + + ) : ( +
    + +
    + + +
    +
    + )} +
    +
    +
    +
    + ) +} + +interface EndpointPanelProps { + serviceName: string + spanName: string + effectiveStartTime: string + effectiveEndTime: string + environments?: string[] +} + +/** Tailwind tint per status class — green through red, `unknown` neutral. */ +const STATUS_TONE: Record = { + "1xx": "bg-muted-foreground/40", + "2xx": "bg-severity-ok", + "3xx": "bg-severity-info", + "4xx": "bg-severity-warn", + "5xx": "bg-severity-error", + unknown: "bg-muted-foreground/30", +} + +/** + * Status-class split for this endpoint. Complements the error-rate chart above: + * that says *when* it broke, this says *how* — a wall of 404s and a wall of 503s + * are the same error rate and completely different problems. + */ +function StatusBreakdownPanel({ + serviceName, + spanName, + effectiveStartTime, + effectiveEndTime, + environments, +}: EndpointPanelProps) { + const result = useRetainedRefreshableResultValue( + getEndpointStatusBreakdownResultAtom({ + data: { + serviceName, + spanName, + startTime: effectiveStartTime, + endTime: effectiveEndTime, + environments: environments?.length ? environments : undefined, + }, + }), + ) + + const slices = Result.builder(result) + .onSuccess((r) => r.slices) + .orElse(() => []) + + if (Result.isInitial(result)) { + return ( + +
    + + +
    +
    + ) + } + + const total = slices.reduce((acc, slice) => acc + slice.estimatedSpanCount, 0) + if (total === 0) { + return ( + +
    + No responses recorded in this window. +
    +
    + ) + } + + return ( + +
    + {/* Single stacked bar: the proportions are the whole point, and a + 100%-width row reads them faster than five separate bars. */} +
    + {slices.map((slice) => ( +
    + ))} +
    +
      + {slices.map((slice) => ( +
    • + + {slice.statusClass} + + {((slice.estimatedSpanCount / total) * 100).toFixed(1)}% + + + {slice.estimatedSpanCount > slice.spanCount ? "~" : ""} + {formatNumber(Math.round(slice.estimatedSpanCount))} + +
    • + ))} +
    +
    + + ) +} + +interface SlowTracesPanelProps extends EndpointPanelProps { + startTime?: string + endTime?: string + timePreset?: string +} + +/** The slowest individual requests to this endpoint, newest-first within the sort. */ +function SlowTracesPanel({ + serviceName, + spanName, + effectiveStartTime, + effectiveEndTime, + environments, + startTime, + endTime, + timePreset, +}: SlowTracesPanelProps) { + const windowSecs = + (Date.parse(effectiveEndTime.replace(" ", "T") + "Z") - + Date.parse(effectiveStartTime.replace(" ", "T") + "Z")) / + 1000 + const detailLimited = windowSecs > TRACE_DETAIL_WINDOW_SECONDS + const listStartTime = detailLimited + ? new Date(Date.parse(effectiveEndTime.replace(" ", "T") + "Z") - TRACE_DETAIL_WINDOW_SECONDS * 1000) + .toISOString() + .replace("T", " ") + .slice(0, 19) + : effectiveStartTime + + const result = useRetainedRefreshableResultValue( + listTracesResultAtom({ + data: { + services: [serviceName], + spanNames: [spanName], + deploymentEnvs: environments?.length ? environments : undefined, + startTime: listStartTime, + endTime: effectiveEndTime, + // The endpoint's span is a server span, but not necessarily a trace + // root (a gateway may sit in front), so search at span level. + rootOnly: false, + sortBy: "durationMs" as const, + sortDir: "desc" as const, + limit: SLOW_TRACES_LIMIT, + }, + }), + ) + + const traces = Result.builder(result) + .onSuccess((r) => r.data.slice(0, SLOW_TRACES_LIMIT)) + .orElse(() => []) + + return ( + + {detailLimited && ( + Latest 30 days + )} + + View all → + +
    + } + > + {Result.isInitial(result) ? ( +
    + {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
    + ) : traces.length === 0 ? ( +
    + No traces recorded in this window. +
    + ) : ( +
      + {traces.map((trace) => ( +
    • + + + + {trace.traceId} + + + + {formatRelativeTimeOrDate(trace.startTime)} + + +
    • + ))} +
    + )} +
    + ) +} diff --git a/packages/domain/src/http/query-engine.ts b/packages/domain/src/http/query-engine.ts index e1312e0ae..c7fc4a0e3 100644 --- a/packages/domain/src/http/query-engine.ts +++ b/packages/domain/src/http/query-engine.ts @@ -656,6 +656,20 @@ export class ServiceDetailOverviewResponse extends Schema.Class( @@ -866,6 +880,76 @@ export class ServiceOperationsResponse extends Schema.Class("ServiceEndpointsRequest")( + { + serviceName: ServiceName, + startTime: TinybirdDateTime, + endTime: TinybirdDateTime, + environments: Schema.optional(Schema.Array(DeploymentEnvironment)), + // Bucket size for the per-endpoint sparkline sub-query (client-computed, + // like ServiceOperationsRequest.bucketSeconds). + bucketSeconds: Schema.optional(Schema.Number), + limit: Schema.optional(Schema.Number), + }, +) {} + +export class ServiceEndpointsResponse extends Schema.Class( + "ServiceEndpointsResponse", +)({ + data: Schema.Array( + Schema.Struct({ + // Display span name ("GET /api/users"). Kept alongside the split halves + // because it — not `route` — is what the /traces `spanNames` filter and + // the sparkline timeseries key on. + spanName: Schema.String, + method: Schema.String, + route: Schema.String, + spanCount: Schema.Number, + estimatedSpanCount: Schema.Number, + errorCount: Schema.Number, + estimatedErrorCount: Schema.Number, + // 0–1 ratio, sampling-weighted. + errorRate: Schema.Number, + avgDurationMs: Schema.Number, + p50DurationMs: Schema.Number, + p95DurationMs: Schema.Number, + p99DurationMs: Schema.Number, + // Sampling-weighted per-bucket counts, joined per endpoint server-side. + sparkline: Schema.Array( + Schema.Struct({ + bucket: Schema.String, + count: Schema.Number, + }), + ), + }), + ), +}) {} + +export class EndpointStatusBreakdownRequest extends Schema.Class( + "EndpointStatusBreakdownRequest", +)({ + serviceName: ServiceName, + // Display span name ("GET /api/users") — matched against both the raw and + // rewritten spelling by `tracesBaseWhereConditions`. + spanName: Schema.String, + startTime: TinybirdDateTime, + endTime: TinybirdDateTime, + environments: Schema.optional(Schema.Array(DeploymentEnvironment)), +}) {} + +export class EndpointStatusBreakdownResponse extends Schema.Class( + "EndpointStatusBreakdownResponse", +)({ + data: Schema.Array( + Schema.Struct({ + /** "1xx" | "2xx" | "3xx" | "4xx" | "5xx" | "unknown" */ + statusClass: Schema.String, + spanCount: Schema.Number, + estimatedSpanCount: Schema.Number, + }), + ), +}) {} + export class ListLogsRequest extends Schema.Class("ListLogsRequest")({ startTime: TinybirdDateTime, endTime: TinybirdDateTime, @@ -1939,6 +2023,20 @@ export class QueryEngineApiGroup extends HttpApiGroup.make("queryEngine") error: queryEngineEndpointErrors, }), ) + .add( + HttpApiEndpoint.post("serviceEndpoints", "/service-endpoints", { + payload: ServiceEndpointsRequest, + success: ServiceEndpointsResponse, + error: queryEngineEndpointErrors, + }), + ) + .add( + HttpApiEndpoint.post("endpointStatusBreakdown", "/endpoint-status-breakdown", { + payload: EndpointStatusBreakdownRequest, + success: EndpointStatusBreakdownResponse, + error: queryEngineEndpointErrors, + }), + ) .add( HttpApiEndpoint.post("listLogs", "/list-logs", { payload: ListLogsRequest, diff --git a/packages/query-engine/src/__sql_baseline__/catalog.sql b/packages/query-engine/src/__sql_baseline__/catalog.sql index 1390307a8..f5642bee9 100644 --- a/packages/query-engine/src/__sql_baseline__/catalog.sql +++ b/packages/query-engine/src/__sql_baseline__/catalog.sql @@ -539,6 +539,178 @@ SELECT ORDER BY bucket ASC FORMAT JSON +-- builder:service-endpoints:endpointStatusBreakdownQuery:default [adeeed45] +SELECT + multiIf(left(if(SpanAttributes['http.response.status_code'] != '', SpanAttributes['http.response.status_code'], SpanAttributes['http.status_code']), 1) = '1', '1xx', left(if(SpanAttributes['http.response.status_code'] != '', SpanAttributes['http.response.status_code'], SpanAttributes['http.status_code']), 1) = '2', '2xx', left(if(SpanAttributes['http.response.status_code'] != '', SpanAttributes['http.response.status_code'], SpanAttributes['http.status_code']), 1) = '3', '3xx', left(if(SpanAttributes['http.response.status_code'] != '', SpanAttributes['http.response.status_code'], SpanAttributes['http.status_code']), 1) = '4', '4xx', left(if(SpanAttributes['http.response.status_code'] != '', SpanAttributes['http.response.status_code'], SpanAttributes['http.status_code']), 1) = '5', '5xx', 'unknown') AS statusClass, + count() AS spanCount, + sum(SampleRate) AS estimatedSpanCount + FROM traces + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND (SpanName = 'GET /v2/services' OR if(((SpanName LIKE 'http.server %' OR SpanName IN ('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS')) AND (SpanAttributes['http.route'] != '' OR SpanAttributes['url.path'] != '')), concat(if(SpanName LIKE 'http.server %', replaceOne(SpanName, 'http.server ', ''), SpanName), ' ', if(SpanAttributes['http.route'] != '', SpanAttributes['http.route'], SpanAttributes['url.path'])), SpanName) = 'GET /v2/services') + GROUP BY statusClass + ORDER BY statusClass ASC + LIMIT 10 + FORMAT JSON + +-- builder:service-endpoints:serviceApiProfileQuery:default [a61ca7c0] +SELECT + countIf((SpanKind = 'Server' AND ((SpanAttributes['http.route'] != '' OR SpanAttributes['url.path'] != '') OR SpanAttributes['http.target'] != ''))) AS httpServerSpans, + countIf(IsEntryPoint = 1) AS entrySpans, + uniqIf(if(((SpanName LIKE 'http.server %' OR SpanName IN ('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS')) AND (SpanAttributes['http.route'] != '' OR SpanAttributes['url.path'] != '')), concat(if(SpanName LIKE 'http.server %', replaceOne(SpanName, 'http.server ', ''), SpanName), ' ', if(SpanAttributes['http.route'] != '', SpanAttributes['http.route'], SpanAttributes['url.path'])), SpanName), (SpanKind = 'Server' AND ((SpanAttributes['http.route'] != '' OR SpanAttributes['url.path'] != '') OR SpanAttributes['http.target'] != ''))) AS distinctEndpoints + FROM traces + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + LIMIT 1 + FORMAT JSON + +-- builder:service-endpoints:serviceEndpointsSummaryQuery:default [f66bdcbf] +SELECT + bSpanName AS spanName, + extract(bSpanName, '^([A-Z]+) ') AS method, + extract(bSpanName, '^[A-Z]+ (.*)$') AS route, + sum(bSpanCount) AS spanCount, + sum(bEstimatedSpanCount) AS estimatedSpanCount, + sum(bErrorCount) AS errorCount, + sum(bEstimatedErrorCount) AS estimatedErrorCount, + if(sum(bEstimatedSpanCount) > 0, sum(bEstimatedErrorCount) / sum(bEstimatedSpanCount), 0) AS errorRate, + if(sum(bSpanCount) > 0, sum(bDurationSum) / sum(bSpanCount) / 1000000, 0) AS avgDurationMs, + if(sum(bSpanCount) > 0, arrayElement(quantilesTDigestMerge(0.5, 0.95, 0.99)(bDurationQuantiles), 1) / 1000000, 0) AS p50DurationMs, + if(sum(bSpanCount) > 0, arrayElement(quantilesTDigestMerge(0.5, 0.95, 0.99)(bDurationQuantiles), 2) / 1000000, 0) AS p95DurationMs, + if(sum(bSpanCount) > 0, arrayElement(quantilesTDigestMerge(0.5, 0.95, 0.99)(bDurationQuantiles), 3) / 1000000, 0) AS p99DurationMs + FROM ( +SELECT + if(((SpanName LIKE 'http.server %' OR SpanName IN ('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS')) AND (SpanAttributes['http.route'] != '' OR SpanAttributes['url.path'] != '')), concat(if(SpanName LIKE 'http.server %', replaceOne(SpanName, 'http.server ', ''), SpanName), ' ', if(SpanAttributes['http.route'] != '', SpanAttributes['http.route'], SpanAttributes['url.path'])), SpanName) AS bSpanName, + count() AS bSpanCount, + sum(SampleRate) AS bEstimatedSpanCount, + countIf(StatusCode = 'Error') AS bErrorCount, + sumIf(SampleRate, StatusCode = 'Error') AS bEstimatedErrorCount, + sum(toFloat64(Duration)) AS bDurationSum, + quantilesTDigestState(0.5, 0.95, 0.99)(Duration) AS bDurationQuantiles + FROM traces + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfMinute(toDateTime('2026-01-01 10:30:00')), toStartOfMinute(toDateTime('2026-01-01 10:30:00')), toStartOfMinute(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 MINUTE) OR Timestamp >= toStartOfMinute(toDateTime('2026-01-03 14:15:00'))) + AND (SpanKind = 'Server' AND ((SpanAttributes['http.route'] != '' OR SpanAttributes['url.path'] != '') OR SpanAttributes['http.target'] != '')) + GROUP BY bSpanName +UNION ALL +SELECT + SpanName AS bSpanName, + sum(SpanCount) AS bSpanCount, + sum(EstimatedSpanCount) AS bEstimatedSpanCount, + sum(ErrorCount) AS bErrorCount, + sum(EstimatedErrorCount) AS bEstimatedErrorCount, + sum(DurationSum) AS bDurationSum, + quantilesTDigestMergeState(0.5, 0.95, 0.99)(DurationQuantiles) AS bDurationQuantiles + FROM service_operations_minutely + WHERE OrgId = 'org_sql_catalog' + AND ServiceName = 'api' + AND Minute >= if(toDateTime('2026-01-01 10:30:00') = toStartOfMinute(toDateTime('2026-01-01 10:30:00')), toStartOfMinute(toDateTime('2026-01-01 10:30:00')), toStartOfMinute(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 MINUTE) + AND Minute < toStartOfMinute(toDateTime('2026-01-03 14:15:00')) + AND (Minute < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Minute >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND match(SpanName, '^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) /') + GROUP BY bSpanName +UNION ALL +SELECT + SpanName AS bSpanName, + sum(SpanCount) AS bSpanCount, + sum(EstimatedSpanCount) AS bEstimatedSpanCount, + sum(ErrorCount) AS bErrorCount, + sum(EstimatedErrorCount) AS bEstimatedErrorCount, + sum(DurationSum) AS bDurationSum, + quantilesTDigestMergeState(0.5, 0.95, 0.99)(DurationQuantiles) AS bDurationQuantiles + FROM service_operations_hourly + WHERE OrgId = 'org_sql_catalog' + AND ServiceName = 'api' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND match(SpanName, '^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) /') + GROUP BY bSpanName +) AS endpoint_windows + GROUP BY spanName, method, route + ORDER BY estimatedSpanCount DESC + LIMIT 50 + FORMAT JSON + +-- builder:service-endpoints:serviceEndpointsSummaryQuery:envFiltered [38283eef] +SELECT + bSpanName AS spanName, + extract(bSpanName, '^([A-Z]+) ') AS method, + extract(bSpanName, '^[A-Z]+ (.*)$') AS route, + sum(bSpanCount) AS spanCount, + sum(bEstimatedSpanCount) AS estimatedSpanCount, + sum(bErrorCount) AS errorCount, + sum(bEstimatedErrorCount) AS estimatedErrorCount, + if(sum(bEstimatedSpanCount) > 0, sum(bEstimatedErrorCount) / sum(bEstimatedSpanCount), 0) AS errorRate, + if(sum(bSpanCount) > 0, sum(bDurationSum) / sum(bSpanCount) / 1000000, 0) AS avgDurationMs, + if(sum(bSpanCount) > 0, arrayElement(quantilesTDigestMerge(0.5, 0.95, 0.99)(bDurationQuantiles), 1) / 1000000, 0) AS p50DurationMs, + if(sum(bSpanCount) > 0, arrayElement(quantilesTDigestMerge(0.5, 0.95, 0.99)(bDurationQuantiles), 2) / 1000000, 0) AS p95DurationMs, + if(sum(bSpanCount) > 0, arrayElement(quantilesTDigestMerge(0.5, 0.95, 0.99)(bDurationQuantiles), 3) / 1000000, 0) AS p99DurationMs + FROM ( +SELECT + if(((SpanName LIKE 'http.server %' OR SpanName IN ('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS')) AND (SpanAttributes['http.route'] != '' OR SpanAttributes['url.path'] != '')), concat(if(SpanName LIKE 'http.server %', replaceOne(SpanName, 'http.server ', ''), SpanName), ' ', if(SpanAttributes['http.route'] != '', SpanAttributes['http.route'], SpanAttributes['url.path'])), SpanName) AS bSpanName, + count() AS bSpanCount, + sum(SampleRate) AS bEstimatedSpanCount, + countIf(StatusCode = 'Error') AS bErrorCount, + sumIf(SampleRate, StatusCode = 'Error') AS bEstimatedErrorCount, + sum(toFloat64(Duration)) AS bDurationSum, + quantilesTDigestState(0.5, 0.95, 0.99)(Duration) AS bDurationQuantiles + FROM traces + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND ResourceAttributes['deployment.environment'] IN ('production') + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfMinute(toDateTime('2026-01-01 10:30:00')), toStartOfMinute(toDateTime('2026-01-01 10:30:00')), toStartOfMinute(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 MINUTE) OR Timestamp >= toStartOfMinute(toDateTime('2026-01-03 14:15:00'))) + AND (SpanKind = 'Server' AND ((SpanAttributes['http.route'] != '' OR SpanAttributes['url.path'] != '') OR SpanAttributes['http.target'] != '')) + GROUP BY bSpanName +UNION ALL +SELECT + SpanName AS bSpanName, + sum(SpanCount) AS bSpanCount, + sum(EstimatedSpanCount) AS bEstimatedSpanCount, + sum(ErrorCount) AS bErrorCount, + sum(EstimatedErrorCount) AS bEstimatedErrorCount, + sum(DurationSum) AS bDurationSum, + quantilesTDigestMergeState(0.5, 0.95, 0.99)(DurationQuantiles) AS bDurationQuantiles + FROM service_operations_minutely + WHERE OrgId = 'org_sql_catalog' + AND ServiceName = 'api' + AND DeploymentEnv IN ('production') + AND Minute >= if(toDateTime('2026-01-01 10:30:00') = toStartOfMinute(toDateTime('2026-01-01 10:30:00')), toStartOfMinute(toDateTime('2026-01-01 10:30:00')), toStartOfMinute(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 MINUTE) + AND Minute < toStartOfMinute(toDateTime('2026-01-03 14:15:00')) + AND (Minute < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Minute >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND match(SpanName, '^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) /') + GROUP BY bSpanName +UNION ALL +SELECT + SpanName AS bSpanName, + sum(SpanCount) AS bSpanCount, + sum(EstimatedSpanCount) AS bEstimatedSpanCount, + sum(ErrorCount) AS bErrorCount, + sum(EstimatedErrorCount) AS bEstimatedErrorCount, + sum(DurationSum) AS bDurationSum, + quantilesTDigestMergeState(0.5, 0.95, 0.99)(DurationQuantiles) AS bDurationQuantiles + FROM service_operations_hourly + WHERE OrgId = 'org_sql_catalog' + AND ServiceName = 'api' + AND DeploymentEnv IN ('production') + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND match(SpanName, '^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) /') + GROUP BY bSpanName +) AS endpoint_windows + GROUP BY spanName, method, route + ORDER BY estimatedSpanCount DESC + LIMIT 50 + FORMAT JSON + -- builder:service-map-rollup:serviceMapEdgesExistingHoursSQL:default [6a2a284a] SELECT toUnixTimestamp(Hour) AS hourTs diff --git a/packages/query-engine/src/ch/builder-fixtures.ts b/packages/query-engine/src/ch/builder-fixtures.ts index 68cf4455f..74c78b44b 100644 --- a/packages/query-engine/src/ch/builder-fixtures.ts +++ b/packages/query-engine/src/ch/builder-fixtures.ts @@ -359,6 +359,50 @@ export const builderFixtures: ReadonlyArray = [ // fixture, a builder contributes nothing to `__sql_baseline__/catalog.sql` // and its SQL can drift silently. + // Service-endpoint splice — same three-tier shape as service-operations, plus + // the HTTP-server narrowing and the method/route split. + { + // routes/internal/query-engine.http.ts — service detail "Endpoints" tab. + module: "service-endpoints", + name: "serviceEndpointsSummaryQuery", + label: "default", + compile: () => CH.compile(CH.serviceEndpointsSummaryQuery({ serviceName: "api", limit: 50 }), window), + }, + { + // Env filter exercises the extra predicate on every tier of the splice. + module: "service-endpoints", + name: "serviceEndpointsSummaryQuery", + label: "envFiltered", + compile: () => + CH.compile( + CH.serviceEndpointsSummaryQuery({ + serviceName: "api", + environments: ["production"], + limit: 50, + }), + window, + ), + }, + { + // routes/internal/query-engine.http.ts — the API-detection probe carried on + // the service-detail overview bundle. + module: "service-endpoints", + name: "serviceApiProfileQuery", + label: "default", + compile: () => CH.compile(CH.serviceApiProfileQuery({ serviceName: "api" }), window), + }, + { + // routes/internal/query-engine.http.ts — endpoint detail status classes. + module: "service-endpoints", + name: "endpointStatusBreakdownQuery", + label: "default", + compile: () => + CH.compile( + CH.endpointStatusBreakdownQuery({ serviceName: "api", spanName: "GET /v2/services" }), + window, + ), + }, + // Service-operation splice across raw, minutely, and hourly sources. { // routes/v2/services.http.ts — service detail "Operations" tab. diff --git a/packages/query-engine/src/ch/index.ts b/packages/query-engine/src/ch/index.ts index 8d74dde75..037ef1673 100644 --- a/packages/query-engine/src/ch/index.ts +++ b/packages/query-engine/src/ch/index.ts @@ -351,6 +351,25 @@ export { type ServiceOperationsTimeseriesOutput, } from "./queries/service-operations" +// Queries — Service Endpoints (HTTP-API view of the service detail page) +export { + endpointStatusBreakdownQuery, + endpointStatusBreakdownRowSchema, + isHttpApiService, + serviceApiProfileQuery, + serviceApiProfileRowSchema, + serviceEndpointsSummaryQuery, + serviceEndpointsSummaryRawQuery, + serviceEndpointsSummaryRowSchema, + HTTP_METHODS, + type ServiceApiProfileOpts, + type ServiceApiProfileOutput, + type EndpointStatusBreakdownOpts, + type EndpointStatusBreakdownOutput, + type ServiceEndpointsSummaryOpts, + type ServiceEndpointsSummaryOutput, +} from "./queries/service-endpoints" + // Queries — Alert Checks (historical rule evaluations) export { listRuleChecksQuery, diff --git a/packages/query-engine/src/ch/queries/overview-rollup-route.test.ts b/packages/query-engine/src/ch/queries/overview-rollup-route.test.ts new file mode 100644 index 000000000..871dddfa2 --- /dev/null +++ b/packages/query-engine/src/ch/queries/overview-rollup-route.test.ts @@ -0,0 +1,107 @@ +// Routing guard for the service-overview rollup tiers. +// +// `canUseAnnualServiceOverview` decides whether an all-metrics timeseries can +// be answered from `service_overview_minutely`/`_hourly` instead of raw +// `traces`. The rollups aggregate SpanName, SpanKind and every attribute away, +// so a filter the predicate forgets to check is not a slow query — it is a +// SILENTLY WRONG one: service-wide numbers returned under an endpoint's name, +// with no error anywhere. +// +// These tests exist because exactly that shipped: the predicate checked the +// singular `spanName` but not the plural `spanNames`, which is the spelling +// every modern caller uses. + +import { describe, expect, it } from "vitest" +import { compileCH } from "@maple-dev/clickhouse-builder" +import { canUseAnnualServiceOverview, tracesTimeseriesQuery } from "./traces" + +/** The shape that legitimately routes to the rollup — each test spoils one thing. */ +const routable = { + allMetrics: true as const, + rootOnly: true, + bucketSeconds: 300, + metric: "count" as const, + needsSampling: true, +} + +describe("canUseAnnualServiceOverview", () => { + it("routes a plain service-scoped all-metrics query to the rollup", () => { + expect(canUseAnnualServiceOverview({ ...routable, serviceName: "api" })).toBe(true) + }) + + it("refuses BOTH spellings of the span-name filter", () => { + // The rollups have no SpanName column, so either spelling routed here + // returns the whole service's traffic under one endpoint's name. + expect(canUseAnnualServiceOverview({ ...routable, spanName: "GET /v1/users" })).toBe(false) + expect(canUseAnnualServiceOverview({ ...routable, spanNames: ["GET /v1/users"] })).toBe(false) + }) + + it("treats an empty spanNames array as no filter", () => { + expect(canUseAnnualServiceOverview({ ...routable, spanNames: [] })).toBe(true) + }) + + it("refuses the other filters the rollups aggregate away", () => { + expect(canUseAnnualServiceOverview({ ...routable, excludedSpanNames: ["x"] })).toBe(false) + expect(canUseAnnualServiceOverview({ ...routable, statusCode: "Error" })).toBe(false) + expect(canUseAnnualServiceOverview({ ...routable, errorsOnly: true })).toBe(false) + expect(canUseAnnualServiceOverview({ ...routable, minDurationMs: 100 })).toBe(false) + expect( + canUseAnnualServiceOverview({ + ...routable, + attributeFilters: [{ key: "http.route", value: "/x", mode: "equals" }], + }), + ).toBe(false) + }) + + it("refuses a non-rootOnly query, whose population the rollup does not match", () => { + expect(canUseAnnualServiceOverview({ ...routable, rootOnly: false })).toBe(false) + }) + + it("refuses a sub-minute bucket, and a sub-hour bucket on the hour-only tier", () => { + expect(canUseAnnualServiceOverview({ ...routable, bucketSeconds: 30 })).toBe(false) + expect(canUseAnnualServiceOverview({ ...routable, bucketSeconds: 300, overviewTiers: "hour" })).toBe( + false, + ) + expect(canUseAnnualServiceOverview({ ...routable, bucketSeconds: 3600, overviewTiers: "hour" })).toBe( + true, + ) + }) + + it("refuses a non-default apdex threshold, which the rollup baked in", () => { + expect(canUseAnnualServiceOverview({ ...routable, apdexThresholdMs: 250 })).toBe(false) + expect(canUseAnnualServiceOverview({ ...routable, apdexThresholdMs: 500 })).toBe(true) + }) +}) + +// The predicate is internal; what a caller actually gets is the SQL. These +// assert the end result — that an endpoint-scoped chart really does read raw +// traces and really does carry its filter. +describe("tracesTimeseriesQuery routing under a span-name filter", () => { + const params = { + orgId: "org_1", + startTime: "2026-08-14 00:00:00", + endTime: "2026-08-14 12:00:00", + bucketSeconds: 300, + } + + it("reads the overview rollup when nothing blocks it", () => { + const { sql } = compileCH(tracesTimeseriesQuery({ ...routable, serviceName: "api" }), params) + expect(sql).toContain("service_overview") + }) + + it("falls back to raw traces and applies the filter when spanNames is set", () => { + const { sql } = compileCH( + tracesTimeseriesQuery({ + ...routable, + serviceName: "api", + spanNames: ["GET /v1/users/:id/entitlements"], + }), + params, + ) + expect(sql).not.toContain("service_overview") + expect(sql).toContain("FROM traces") + // The filter must actually reach the WHERE clause — routing to raw without + // emitting the predicate would be the same wrong number by another path. + expect(sql).toContain("GET /v1/users/:id/entitlements") + }) +}) diff --git a/packages/query-engine/src/ch/queries/service-endpoints.test.ts b/packages/query-engine/src/ch/queries/service-endpoints.test.ts new file mode 100644 index 000000000..e03af4d51 --- /dev/null +++ b/packages/query-engine/src/ch/queries/service-endpoints.test.ts @@ -0,0 +1,160 @@ +import { describe, expect, it } from "vitest" +import { Schema } from "effect" +import { compileCH } from "@maple-dev/clickhouse-builder" +import { + HTTP_METHODS, + isHttpApiService, + serviceApiProfileQuery, + serviceApiProfileRowSchema, + serviceEndpointsSummaryQuery, + serviceEndpointsSummaryRawQuery, + serviceEndpointsSummaryRowSchema, +} from "./service-endpoints" + +const baseParams = { + orgId: "org_1", + startTime: "2024-01-01 00:00:00", + endTime: "2024-01-02 00:00:00", +} + +describe("serviceApiProfileQuery", () => { + it("counts HTTP server spans, entry spans, and distinct endpoints in one scan", () => { + const { sql } = compileCH(serviceApiProfileQuery({ serviceName: "api" }), baseParams) + expect(sql).toContain("FROM traces") + expect(sql).toContain("OrgId = 'org_1'") + expect(sql).toContain("ServiceName = 'api'") + expect(sql).toContain("AS httpServerSpans") + expect(sql).toContain("countIf(IsEntryPoint = 1) AS entrySpans") + expect(sql).toContain("AS distinctEndpoints") + expect(sql).toContain("uniqIf(") + }) + + it("detects HTTP server spans from route, url.path, and the pre-1.0 http.target", () => { + const { sql } = compileCH(serviceApiProfileQuery({ serviceName: "api" }), baseParams) + expect(sql).toContain("SpanKind = 'Server'") + expect(sql).toContain("SpanAttributes['http.route'] != ''") + expect(sql).toContain("SpanAttributes['url.path'] != ''") + expect(sql).toContain("SpanAttributes['http.target'] != ''") + }) + + it("applies the environment filter", () => { + const { sql } = compileCH( + serviceApiProfileQuery({ serviceName: "api", environments: ["production"] }), + baseParams, + ) + expect(sql).toContain("ResourceAttributes['deployment.environment'] IN ('production')") + }) + + it("decodes JSON-string UInt64 counts from a BYO ClickHouse gateway", () => { + const decoded = Schema.decodeUnknownSync(serviceApiProfileRowSchema)({ + httpServerSpans: "9007199254740", + entrySpans: "12", + distinctEndpoints: "3", + }) + expect(decoded).toEqual({ httpServerSpans: 9007199254740, entrySpans: 12, distinctEndpoints: 3 }) + }) +}) + +describe("isHttpApiService", () => { + const profile = (over: Partial[0]>) => + isHttpApiService({ httpServerSpans: 100, entrySpans: 100, distinctEndpoints: 5, ...over }) + + it("accepts a service with endpoints and enough server traffic", () => { + expect(profile({})).toBe(true) + }) + + it("accepts a mostly-async service that also exposes a single endpoint", () => { + // No ratio gate: a worker with a health endpoint still gets the tab. + expect(profile({ httpServerSpans: 20, entrySpans: 100_000, distinctEndpoints: 1 })).toBe(true) + }) + + it("rejects a service with no HTTP server spans at all", () => { + expect(profile({ httpServerSpans: 0, distinctEndpoints: 0 })).toBe(false) + }) + + it("rejects a handful of stray probe requests below the span floor", () => { + expect(profile({ httpServerSpans: 19, distinctEndpoints: 1 })).toBe(false) + }) +}) + +describe("serviceEndpointsSummaryQuery", () => { + it("combines raw edges, minutely boundary hours, and complete hourly interiors", () => { + const { sql } = compileCH(serviceEndpointsSummaryQuery({ serviceName: "api" }), baseParams) + expect(sql).toContain("FROM traces") + expect(sql).toContain("UNION ALL") + expect(sql).toContain("FROM service_operations_minutely") + expect(sql).toContain("FROM service_operations_hourly") + expect(sql).toContain("ORDER BY estimatedSpanCount DESC") + expect(sql).toContain("LIMIT 25") + expect(sql).toContain("FORMAT JSON") + }) + + it("filters raw edges on SpanKind, and rollup interiors on the display-name shape", () => { + const { sql } = compileCH(serviceEndpointsSummaryQuery({ serviceName: "api" }), baseParams) + // Raw rows have the columns the rollups dropped, so they get the accurate filter. + expect(sql).toContain("SpanKind = 'Server'") + // The rollups only kept the normalized name, so they match its shape. + expect(sql).toContain(`match(SpanName, '^(${HTTP_METHODS.join("|")}) /')`) + }) + + it("splits the fused display name into method and route server-side", () => { + const { sql } = compileCH(serviceEndpointsSummaryQuery({ serviceName: "api" }), baseParams) + expect(sql).toContain("extract(bSpanName, '^([A-Z]+) ') AS method") + expect(sql).toContain("extract(bSpanName, '^[A-Z]+ (.*)$') AS route") + // The fused name survives — it, not `route`, is the /traces filter key. + expect(sql).toContain("AS spanName") + }) + + it("carries p99 through both t-digest states", () => { + const { sql } = compileCH(serviceEndpointsSummaryQuery({ serviceName: "api" }), baseParams) + expect(sql).toContain("quantilesTDigestState(0.5, 0.95, 0.99)(Duration)") + expect(sql).toContain("quantilesTDigestMergeState(0.5, 0.95, 0.99)(DurationQuantiles)") + expect(sql).toContain("quantilesTDigestMerge(0.5, 0.95, 0.99)(bDurationQuantiles), 3") + }) + + it("applies the environment filter to raw and rollup fragments alike", () => { + const { sql } = compileCH( + serviceEndpointsSummaryQuery({ serviceName: "api", environments: ["production"] }), + baseParams, + ) + expect(sql).toContain("ResourceAttributes['deployment.environment'] IN ('production')") + expect(sql).toContain("DeploymentEnv IN ('production')") + }) + + it("respects a custom limit", () => { + const { sql } = compileCH(serviceEndpointsSummaryQuery({ serviceName: "api", limit: 5 }), baseParams) + expect(sql).toContain("LIMIT 5") + }) +}) + +describe("serviceEndpointsSummaryRawQuery", () => { + it("reads only raw traces, for clusters missing the rollup tables", () => { + const { sql } = compileCH(serviceEndpointsSummaryRawQuery({ serviceName: "api" }), baseParams) + expect(sql).toContain("FROM traces") + expect(sql).not.toContain("service_operations_minutely") + expect(sql).not.toContain("service_operations_hourly") + expect(sql).toContain("SpanKind = 'Server'") + expect(sql).toContain("quantile(0.99)(Duration)") + }) + + it("returns the same row shape as the rollup query", () => { + const row = { + spanName: "GET /api/users", + method: "GET", + route: "/api/users", + spanCount: "10", + estimatedSpanCount: "100", + errorCount: "1", + estimatedErrorCount: "10", + errorRate: 0.1, + avgDurationMs: 12.5, + p50DurationMs: 10, + p95DurationMs: 40, + p99DurationMs: 90, + } + const decoded = Schema.decodeUnknownSync(serviceEndpointsSummaryRowSchema)(row) + expect(decoded.spanCount).toBe(10) + expect(decoded.method).toBe("GET") + expect(decoded.route).toBe("/api/users") + }) +}) diff --git a/packages/query-engine/src/ch/queries/service-endpoints.ts b/packages/query-engine/src/ch/queries/service-endpoints.ts new file mode 100644 index 000000000..3de436cf7 --- /dev/null +++ b/packages/query-engine/src/ch/queries/service-endpoints.ts @@ -0,0 +1,395 @@ +// Service Endpoints +// +// The HTTP-API view of a service: the same per-operation breakdown as +// `service-operations.ts`, narrowed to HTTP *server* spans and split into +// `method` + `route` so the UI never parses a fused span name — plus a cheap +// profile probe that decides whether a service is an HTTP API at all. +// +// v1 rides the existing rollups. `service_operations_minutely`/`_hourly` store +// the *normalized* span name (`NORMALIZED_SPAN_NAME_SQL`), so an HTTP server +// span is already recorded as "GET /api/users" — the rollup is an endpoint +// rollup in disguise. What it does NOT store is `SpanKind`, so the rollup +// fragments below fall back to matching the name shape: +// +// - raw edges filter accurately: `SpanKind = 'Server'` AND a non-empty +// route/url.path/target, the columns that actually exist there; +// - rollup interiors filter on `match(SpanName, '^(GET|POST|…) /')`. +// +// The known consequence: a non-HTTP span literally named "GET /foo" is counted +// as an endpoint inside the rollup window. A dedicated `service_endpoints_hourly` +// MV keyed (OrgId, Hour, ServiceName, DeploymentEnv, HttpMethod, HttpRoute) and +// filtered to `SpanKind = 'Server'` at write time removes both the name-shape +// heuristic and the string split. THIS FILE IS THE SWAP POINT for that — nothing +// downstream of it knows how the rows are sourced. + +import { Schema } from "effect" +import * as CH from "@maple-dev/clickhouse-builder/expr" +import { param } from "@maple-dev/clickhouse-builder" +import { from, fromUnion, unionAll, type ColumnAccessor } from "@maple-dev/clickhouse-builder" +import { httpDisplaySpanName } from "../../traces-shared" +import { CHNumber } from "../schema" +import { ServiceOperationsHourly, ServiceOperationsMinutely, Traces } from "../tables" +import { tracesBaseWhereConditions } from "./query-helpers" +import { edgeCondition, hourGrain, interiorConditions, minuteGrain } from "./rollup-splice" + +/** + * The verbs `normalizedSpanNameExpr` rewrites into a display name. Kept in sync + * with `packages/domain/src/tinybird/span-display-name.ts` — a verb missing here + * is an endpoint the rollup fragments silently drop. + */ +export const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] as const + +/** Matches a stored display name: " /path". */ +const ENDPOINT_NAME_PATTERN = `^(${HTTP_METHODS.join("|")}) /` + +const displaySpanName = ($: ColumnAccessor) => + httpDisplaySpanName($.SpanName, $.SpanAttributes.get("http.route"), $.SpanAttributes.get("url.path")) + +/** + * "This span served an HTTP request." Uses the columns raw `traces` actually + * has, so it does not depend on the display-name rewrite. `http.target` is the + * pre-1.0 semconv spelling of `url.path` and is included for older SDKs. + */ +const httpServerSpanCondition = ($: ColumnAccessor) => + $.SpanKind.eq("Server").and( + $.SpanAttributes.get("http.route") + .neq("") + .or($.SpanAttributes.get("url.path").neq("")) + .or($.SpanAttributes.get("http.target").neq("")), + ) + +// -- Profile / detection ---------------------------------------------------- + +export interface ServiceApiProfileOpts { + serviceName: string + environments?: readonly string[] +} + +export interface ServiceApiProfileOutput { + readonly httpServerSpans: number + readonly entrySpans: number + readonly distinctEndpoints: number +} + +export const serviceApiProfileRowSchema = Schema.Struct({ + httpServerSpans: CHNumber, + entrySpans: CHNumber, + distinctEndpoints: CHNumber, +}) + +/** + * Whether to offer the Endpoints view for a service. + * + * Deliberately permissive, and deliberately NOT a ratio: the Endpoints tab is + * additive (Operations stays), so a false positive costs a thin extra tab while + * a false negative hides the feature entirely. A worker that also exposes a + * health endpoint should still get it. + * + * The span floor exists only to keep a handful of stray probe requests from + * turning every background job into an "API". + */ +export const isHttpApiService = (profile: ServiceApiProfileOutput): boolean => + profile.distinctEndpoints >= 1 && profile.httpServerSpans >= 20 + +/** + * One-row probe: how much of this service's traffic is HTTP server traffic. + * + * Callers clamp this to a short recent window (see `serviceApiProfile` in the + * query registry) — a service that serves HTTP now served HTTP an hour ago, and + * scanning a year of spans to answer a yes/no question is pure waste. + */ +export function serviceApiProfileQuery(opts: ServiceApiProfileOpts) { + return from(Traces) + .select(($) => { + const isHttpServer = httpServerSpanCondition($) + return { + httpServerSpans: CH.countIf(isHttpServer), + entrySpans: CH.countIf($.IsEntryPoint.eq(1)), + distinctEndpoints: CH.uniqIf(displaySpanName($), isHttpServer), + } + }) + .where(($) => + tracesBaseWhereConditions($, { + serviceName: opts.serviceName, + environments: opts.environments, + }), + ) + .limit(1) + .format("JSON") +} + +// -- Status-class distribution (one endpoint) ------------------------------- + +export interface EndpointStatusBreakdownOpts { + serviceName: string + /** Display span name, e.g. "GET /api/users". */ + spanName: string + environments?: readonly string[] +} + +export interface EndpointStatusBreakdownOutput { + /** "2xx" | "3xx" | "4xx" | "5xx" | "1xx" | "unknown" */ + readonly statusClass: string + readonly spanCount: number + readonly estimatedSpanCount: number +} + +export const endpointStatusBreakdownRowSchema = Schema.Struct({ + statusClass: Schema.String, + spanCount: CHNumber, + estimatedSpanCount: CHNumber, +}) + +/** + * Status classes for one endpoint. + * + * Written as a builder here rather than driven through the generic breakdown + * path (`groupBy: "attribute"`) because that path takes a single attribute key, + * and HTTP status has two live spellings: `http.response.status_code` (semconv + * ≥1.0) and `http.status_code` (before it). Coalescing them in SQL — the same + * precedence `traceListMv` uses — is one query; picking one key is a silent + * blind spot for every older SDK. + */ +export function endpointStatusBreakdownQuery(opts: EndpointStatusBreakdownOpts) { + const statusCodeExpr = ($: ColumnAccessor) => + CH.if_( + $.SpanAttributes.get("http.response.status_code").neq(""), + $.SpanAttributes.get("http.response.status_code"), + $.SpanAttributes.get("http.status_code"), + ) + + return from(Traces) + .select(($) => { + const leadingDigit = CH.left_(statusCodeExpr($), CH.lit(1)) + return { + // `left(code, 1)` classifies without parsing: "503" -> "5" -> "5xx". + // An absent or non-numeric code lands in "unknown" rather than being + // dropped, so the classes always sum to the endpoint's throughput. + statusClass: CH.multiIf( + [ + [leadingDigit.eq("1"), CH.lit("1xx")], + [leadingDigit.eq("2"), CH.lit("2xx")], + [leadingDigit.eq("3"), CH.lit("3xx")], + [leadingDigit.eq("4"), CH.lit("4xx")], + [leadingDigit.eq("5"), CH.lit("5xx")], + ], + CH.lit("unknown"), + ), + spanCount: CH.count(), + estimatedSpanCount: CH.sum($.SampleRate), + } + }) + .where(($) => + tracesBaseWhereConditions($, { + serviceName: opts.serviceName, + spanName: opts.spanName, + environments: opts.environments, + }), + ) + .groupBy("statusClass") + .orderBy(["statusClass", "asc"]) + .limit(10) + .format("JSON") +} + +// -- Endpoint summary ------------------------------------------------------- + +export interface ServiceEndpointsSummaryOpts { + serviceName: string + environments?: readonly string[] + limit?: number +} + +export interface ServiceEndpointsSummaryOutput { + /** Display span name ("GET /api/users") — the key the /traces `spanNames` filter accepts. */ + readonly spanName: string + readonly method: string + readonly route: string + readonly spanCount: number + readonly estimatedSpanCount: number + readonly errorCount: number + readonly estimatedErrorCount: number + readonly errorRate: number + readonly avgDurationMs: number + readonly p50DurationMs: number + readonly p95DurationMs: number + readonly p99DurationMs: number +} + +/** + * UInt64 columns (`count`, `countIf`) arrive as JSON strings from BYO + * ClickHouse; {@link CHNumber} coerces them centrally via `decodeRows`. + */ +export const serviceEndpointsSummaryRowSchema = Schema.Struct({ + spanName: Schema.String, + method: Schema.String, + route: Schema.String, + spanCount: CHNumber, + estimatedSpanCount: CHNumber, + errorCount: CHNumber, + estimatedErrorCount: CHNumber, + errorRate: CHNumber, + avgDurationMs: CHNumber, + p50DurationMs: CHNumber, + p95DurationMs: CHNumber, + p99DurationMs: CHNumber, +}) + +const RAW_DURATION_STATE = "quantilesTDigestState(0.5, 0.95, 0.99)(Duration)" +const ROLLUP_DURATION_STATE = "quantilesTDigestMergeState(0.5, 0.95, 0.99)(DurationQuantiles)" + +const mergedDurationQuantile = (index: 1 | 2 | 3) => + CH.rawExpr( + `if(sum(bSpanCount) > 0, arrayElement(quantilesTDigestMerge(0.5, 0.95, 0.99)(bDurationQuantiles), ${index}) / 1000000, 0)`, + ) + +/** `extract` returns the first capture group, so both halves come from one split rule. */ +const methodOf = (spanName: CH.Expr) => CH.extract_(spanName, "^([A-Z]+) ") +const routeOf = (spanName: CH.Expr) => CH.extract_(spanName, "^[A-Z]+ (.*)$") + +function rollupEnvironmentCondition( + $: ColumnAccessor, + environments: readonly string[] | undefined, +) { + return environments?.length ? CH.inList($.DeploymentEnv, environments) : undefined +} + +function hourlyEnvironmentCondition( + $: ColumnAccessor, + environments: readonly string[] | undefined, +) { + return environments?.length ? CH.inList($.DeploymentEnv, environments) : undefined +} + +/** + * All-raw rollback companion, mirroring `serviceOperationsSummaryRawQuery` — the + * path taken per-org when a cluster is missing the rollup tables (UNKNOWN_TABLE). + */ +export function serviceEndpointsSummaryRawQuery(opts: ServiceEndpointsSummaryOpts) { + return from(Traces) + .select(($) => { + const weight = CH.sum($.SampleRate) + const errorWeight = CH.sumIf($.SampleRate, $.StatusCode.eq("Error")) + const name = displaySpanName($) + return { + spanName: name, + method: methodOf(name), + route: routeOf(name), + spanCount: CH.count(), + estimatedSpanCount: weight, + errorCount: CH.countIf($.StatusCode.eq("Error")), + estimatedErrorCount: errorWeight, + errorRate: CH.if_(weight.gt(0), errorWeight.div(weight), CH.lit(0)), + avgDurationMs: CH.avg($.Duration).div(1_000_000), + p50DurationMs: CH.quantile(0.5)($.Duration).div(1_000_000), + p95DurationMs: CH.quantile(0.95)($.Duration).div(1_000_000), + p99DurationMs: CH.quantile(0.99)($.Duration).div(1_000_000), + } + }) + .where(($) => [ + ...tracesBaseWhereConditions($, { + serviceName: opts.serviceName, + environments: opts.environments, + }), + httpServerSpanCondition($), + ]) + .groupBy("spanName", "method", "route") + .orderBy(["estimatedSpanCount", "desc"]) + .limit(opts.limit ?? 25) + .format("JSON") +} + +export function serviceEndpointsSummaryQuery(opts: ServiceEndpointsSummaryOpts) { + const rawEdges = from(Traces) + .select(($) => ({ + bSpanName: displaySpanName($), + bSpanCount: CH.count(), + bEstimatedSpanCount: CH.sum($.SampleRate), + bErrorCount: CH.countIf($.StatusCode.eq("Error")), + bEstimatedErrorCount: CH.sumIf($.SampleRate, $.StatusCode.eq("Error")), + bDurationSum: CH.sum(CH.rawExpr("toFloat64(Duration)")), + bDurationQuantiles: CH.rawExpr(RAW_DURATION_STATE), + })) + .where(($) => [ + ...tracesBaseWhereConditions($, { + serviceName: opts.serviceName, + environments: opts.environments, + }), + edgeCondition("Timestamp", minuteGrain), + // Accurate filter — the raw table has the columns the rollups dropped. + httpServerSpanCondition($), + ]) + .groupBy("bSpanName") + + const minutelyEdges = from(ServiceOperationsMinutely) + .select(($) => ({ + bSpanName: $.SpanName, + bSpanCount: CH.sum($.SpanCount), + bEstimatedSpanCount: CH.sum($.EstimatedSpanCount), + bErrorCount: CH.sum($.ErrorCount), + bEstimatedErrorCount: CH.sum($.EstimatedErrorCount), + bDurationSum: CH.sum($.DurationSum), + bDurationQuantiles: CH.rawExpr(ROLLUP_DURATION_STATE), + })) + .where(($) => [ + $.OrgId.eq(param.string("orgId")), + $.ServiceName.eq(opts.serviceName), + rollupEnvironmentCondition($, opts.environments), + ...interiorConditions($.Minute, minuteGrain), + edgeCondition("Minute", hourGrain), + CH.matchCond($.SpanName, ENDPOINT_NAME_PATTERN), + ]) + .groupBy("bSpanName") + + const hourlyInterior = from(ServiceOperationsHourly) + .select(($) => ({ + bSpanName: $.SpanName, + bSpanCount: CH.sum($.SpanCount), + bEstimatedSpanCount: CH.sum($.EstimatedSpanCount), + bErrorCount: CH.sum($.ErrorCount), + bEstimatedErrorCount: CH.sum($.EstimatedErrorCount), + bDurationSum: CH.sum($.DurationSum), + bDurationQuantiles: CH.rawExpr(ROLLUP_DURATION_STATE), + })) + .where(($) => [ + $.OrgId.eq(param.string("orgId")), + $.ServiceName.eq(opts.serviceName), + hourlyEnvironmentCondition($, opts.environments), + ...interiorConditions($.Hour, hourGrain), + CH.matchCond($.SpanName, ENDPOINT_NAME_PATTERN), + ]) + .groupBy("bSpanName") + + return fromUnion(unionAll(rawEdges, minutelyEdges, hourlyInterior), "endpoint_windows") + .select(($) => { + const spanCount = CH.sum($.bSpanCount) + const estimatedSpanCount = CH.sum($.bEstimatedSpanCount) + const estimatedErrorCount = CH.sum($.bEstimatedErrorCount) + return { + spanName: $.bSpanName, + method: methodOf($.bSpanName), + route: routeOf($.bSpanName), + spanCount, + estimatedSpanCount, + errorCount: CH.sum($.bErrorCount), + estimatedErrorCount, + errorRate: CH.if_( + estimatedSpanCount.gt(0), + estimatedErrorCount.div(estimatedSpanCount), + CH.lit(0), + ), + avgDurationMs: CH.if_( + spanCount.gt(0), + CH.sum($.bDurationSum).div(spanCount).div(1_000_000), + CH.lit(0), + ), + p50DurationMs: mergedDurationQuantile(1), + p95DurationMs: mergedDurationQuantile(2), + p99DurationMs: mergedDurationQuantile(3), + } + }) + .groupBy("spanName", "method", "route") + .orderBy(["estimatedSpanCount", "desc"]) + .limit(opts.limit ?? 25) + .format("JSON") +} diff --git a/packages/query-engine/src/ch/queries/traces.ts b/packages/query-engine/src/ch/queries/traces.ts index 13cb49a93..cee41a776 100644 --- a/packages/query-engine/src/ch/queries/traces.ts +++ b/packages/query-engine/src/ch/queries/traces.ts @@ -400,7 +400,12 @@ export function canUseAnnualServiceOverview(opts: TracesTimeseriesOpts): boolean bucketSeconds >= 60 && tierIsAvailable && (opts.groupBy ?? []).every((key) => OVERVIEW_ROLLUP_GROUP_KEYS.has(key)) && + // BOTH spellings of the span-name filter. The rollups aggregate SpanName + // away, so either one routed here comes back silently unfiltered — + // service-wide numbers under an endpoint's name. `canUseServiceOverviewMv` + // has always checked both; this predicate only checked the singular. opts.spanName == null && + !opts.spanNames?.length && opts.statusCode == null && !opts.errorsOnly && opts.minDurationMs == null && diff --git a/packages/query-engine/src/registry/queries.ts b/packages/query-engine/src/registry/queries.ts index 125570a48..bc9d5942e 100644 --- a/packages/query-engine/src/registry/queries.ts +++ b/packages/query-engine/src/registry/queries.ts @@ -1,5 +1,7 @@ import type { ServiceOperationsRequest, + ServiceEndpointsRequest, + EndpointStatusBreakdownRequest, ListPodsRequest, NodeFacetsRequest, PodFacetsRequest, @@ -41,6 +43,7 @@ import type { import { Match } from "effect" import { attributeIndexMode, logBodySearchMode } from "../capabilities" import * as CH from "../ch" +import { formatWarehouseDateTime, parseWarehouseDateTime } from "../datetime" import { LOGS_BODY_SEARCH_SETTINGS } from "../profiles" import { makeDirectRouteCachePolicy } from "../runtime/query-engine" import { defineQuery } from "./query-definition" @@ -951,3 +954,91 @@ export const serviceOperationsTimeseriesRaw = defineQuery({ { rowSchema: CH.serviceOperationsTimeseriesRowSchema }, ), }) + +// -- Service endpoints (HTTP-API view) -------------------------------------- + +/** + * The profile probe answers a yes/no question, so it reads a short recent + * window rather than the page's selected range: a service that serves HTTP now + * served HTTP an hour ago, and scanning a year of spans to learn that is pure + * waste. Clamped, never widened — a range shorter than this stays as-is. + */ +const API_PROFILE_WINDOW_MS = 24 * 60 * 60 * 1000 + +const apiProfileStartTime = (payload: ServiceEndpointsRequest): string => { + const endMs = parseWarehouseDateTime(payload.endTime) + const startMs = parseWarehouseDateTime(payload.startTime) + const clampedMs = Math.max(startMs, endMs - API_PROFILE_WINDOW_MS) + return clampedMs > startMs ? formatWarehouseDateTime(clampedMs) : payload.startTime +} + +export const serviceApiProfile = defineQuery({ + id: "serviceApiProfile", + profile: "aggregation", + // Detection is stable over minutes and rides the overview bundle on every + // service page load, so it is the one query here worth caching outright. + cache: 60, + compile: (payload: ServiceEndpointsRequest, orgId: string) => + CH.compile( + CH.serviceApiProfileQuery({ + serviceName: payload.serviceName, + environments: payload.environments, + }), + { orgId, startTime: apiProfileStartTime(payload), endTime: payload.endTime }, + { rowSchema: CH.serviceApiProfileRowSchema }, + ), +}) + +const serviceEndpointsSummaryOptions = (payload: ServiceEndpointsRequest) => ({ + serviceName: payload.serviceName, + environments: payload.environments, + limit: payload.limit, +}) + +const serviceEndpointsParams = (payload: ServiceEndpointsRequest, orgId: string) => ({ + orgId, + startTime: payload.startTime, + endTime: payload.endTime, +}) + +export const serviceEndpointsSummary = defineQuery({ + id: "serviceEndpoints", + profile: "aggregation", + cache: undefined, + compile: (payload: ServiceEndpointsRequest, orgId: string) => + CH.compile( + CH.serviceEndpointsSummaryQuery(serviceEndpointsSummaryOptions(payload)), + serviceEndpointsParams(payload, orgId), + { rowSchema: CH.serviceEndpointsSummaryRowSchema }, + ), +}) + +export const endpointStatusBreakdown = defineQuery({ + id: "endpointStatusBreakdown", + profile: "aggregation", + cache: 30, + compile: (payload: EndpointStatusBreakdownRequest, orgId: string) => + CH.compile( + CH.endpointStatusBreakdownQuery({ + serviceName: payload.serviceName, + spanName: payload.spanName, + environments: payload.environments, + }), + { orgId, startTime: payload.startTime, endTime: payload.endTime }, + { rowSchema: CH.endpointStatusBreakdownRowSchema }, + ), +}) + +/** Same rollup-absent fallback shape (and same fail-fast budget) as serviceOperations. */ +export const serviceEndpointsSummaryRaw = defineQuery({ + id: "serviceEndpoints", + profile: "aggregation", + settings: SERVICE_OPERATIONS_RAW_SETTINGS, + cache: undefined, + compile: (payload: ServiceEndpointsRequest, orgId: string) => + CH.compile( + CH.serviceEndpointsSummaryRawQuery(serviceEndpointsSummaryOptions(payload)), + serviceEndpointsParams(payload, orgId), + { rowSchema: CH.serviceEndpointsSummaryRowSchema }, + ), +}) diff --git a/packages/query-engine/src/sql-catalog.test.ts b/packages/query-engine/src/sql-catalog.test.ts index 3957057a6..093aa66b0 100644 --- a/packages/query-engine/src/sql-catalog.test.ts +++ b/packages/query-engine/src/sql-catalog.test.ts @@ -25,6 +25,7 @@ import * as metricQueries from "./ch/queries/metrics" import * as serviceInfraQueries from "./ch/queries/service-infra" import * as serviceMapRollupQueries from "./ch/queries/service-map-rollup" import * as serviceMapQueries from "./ch/queries/service-map" +import * as serviceEndpointQueries from "./ch/queries/service-endpoints" import * as serviceOperationQueries from "./ch/queries/service-operations" import * as serviceQueries from "./ch/queries/services" import * as sessionEventQueries from "./ch/queries/session-events" @@ -185,6 +186,7 @@ const QUERY_MODULES: Record> = { "service-infra": serviceInfraQueries, "service-map-rollup": serviceMapRollupQueries, "service-map": serviceMapQueries, + "service-endpoints": serviceEndpointQueries, "service-operations": serviceOperationQueries, services: serviceQueries, "session-events": sessionEventQueries, @@ -288,6 +290,7 @@ const EXEMPT_BUILDERS: ReadonlySet = new Set([ // todo batch ④ — remainder (billing, service detail, operations, stray trace/log lookups) "logs/getLogByKeyQuery", + "service-endpoints/serviceEndpointsSummaryRawQuery", "service-operations/serviceOperationsSummaryRawQuery", "service-operations/serviceOperationsTimeseriesRawQuery", "services/serviceHealthSnapshotQuery",