Dashboard › spotlight › Session sc28b5a79801
sc28b5a798012b159cch-seed-check.yml
ci.yml
cleanup-nightlies.yml
docs-preview.yml
eval.yml
publish.yml
release.yml
---
name: CI
on:
push:
branches: [main, 'release/**']
pull_request:
branches: [main]
# packages:write is needed for publish-nightly to push to GHCR
permissions:
contents: read
packages: write
env:
# Commit timestamp used for deterministic nightly version strings.
# Defined at workflow level so all jobs agree on the same value.
COMMIT_TIMESTAMP: ${{ github.event.head_commit.timestamp }}
jobs:
# ---------------------------------------------------------------------------
# Change detection: skip heavy CI for docs-only changes
# ---------------------------------------------------------------------------
changes:
runs-on: ubuntu-latest
permissions:
pull-requests: read
outputs:
code: ${{ steps.force.outputs.code || steps.filter.outputs.code }}
docs: ${{ steps.filter.outputs.docs }}
steps:
CI triggers on pull_request targeting main, which is what we have. The workflow might just be slow to start. Let me wait a bit longer:
=== E2E UI status === E2E UI Tests pass 3m34s https://github.com/getsentry/spotlight/actions/runs/27305417940/job/80663495202 === any new bot reviews/comments? === inline review comments: 0
Please address this message and continue with your tasks. </system-reminder>
completed failure CCH Seed Check CCH Seed Check main schedule 27304907660 46s 2026-06-10T20:40:08Z
completed success fix(gateway): replace DecompressionStream('zstd') with node:zlib streaming CI fix/delta-upgrade-zstd pull_request 27301650493 4m37s 2026-06-10T19:44:10Z
completed success fix(gateway): replace DecompressionStream('zstd') with node:zlib streaming Docs Preview fix/delta-upgrade-zstd pull_request 27301650420 24s 2026-06-10T19:44:10Z
CI runs are happening for other PRs but not ours. This might be because the branch was pushed from a detached-HEAD worktree and GitHub didn't associate the push with the PR event. Let me try force-pushing to trigger the CI:
You are a harsh, objective code reviewer. Review the following PR for getsentry/spotlight (https://github.com/getsentry/spotlight/pull/1320). Your job is to find ALL bugs, logic flaws, edge cases, incomplete fixes, naming issues, test gaps, and description inaccuracies. Be ruthless — a PASS should only be given if you genuinely cannot find any issues.
The PR fixes URL path stacking in the sidebar and detail tab bars of the Spotlight UI. The root cause is that a pnpm override silently upgraded react-router-dom from v6 to v7, where relative links in splat routes resolve against the full current URL (including splat segments), causing path stacking.
Read all modified files in /tmp/opencode/spotlight-eval by running git show HEAD on that repo to get the full diff. Also read the PR description at /tmp/opencode/pr-body.md.
For each changed file, read the FULL file (not just the diff) to understand the surrounding context and whether the fix is complete.
Specifically check for:
span.span_id, span.trace_id, trace.trace_id, or eventId are undefined/null? The types show span_id can be optional.basePath prop actually used correctly? What happens when basePath is provided but nested is also true — does the ternary behave correctly?I'll start by reading the PR description and the full diff, then examine each modified file in context.
<path>/home/byk/Code/opencode-lore/packages/gateway/src/ui.ts</path>
<type>file</type>
<content>
2826: }
2827: return params;
2828: }
2829:
2830: // ---------------------------------------------------------------------------
2831: // Entities pages
2832: // ---------------------------------------------------------------------------
2833:
2834: async function pageEntities(): Promise<string> {
2835: const all = entities.listAll();
2836:
2837: let body = breadcrumb([
2838: { label: "Dashboard", href: "/ui" },
2839: { label: "Entities" },
2840: ]);
2841: body += `<h1>Entities (${all.length})</h1>`;
2842:
2843: if (!all.length) {
2844: body += `<p class="empty">No entities found. Entities are created automatically when the curator detects recurring people, services, tools, and other named references in conversations.</p>`;
2845: return layout("Entities", body);
2846: }
2847:
2848: // Merge suggestions (#462): surface duplicate candidates as a dry-run ...
I need to locate where the /ui/api/merge POST routes are handled in ui.ts so I can add a similar rebuild route to the dispatcher.
<path>/tmp/opencode/pr-body.md</path> <type>file</type> <content> 1: Resolves #1319. 2: 3: ### Root cause 4: 5: The sidebar links were relative (`<Link to="errors">`) and are rendered inside a **splat route** (`/telemetry/*`). They started stacking the URL path because the app silently moved from React Router **v6 to v7**: 6: 7: - `packages/spotlight/package.json` declares `react-router-dom: ^6.30.2`, but the root `pnpm.overrides` entry `react-router-dom@>=6.0.0 <7.0.0: ">=6.30.4"` has an **unbounded value**, so pnpm rewrote the spec to `>=6.30.4` and resolved it to **7.17.0**. 8: - In v7 the `v7_relativeSplatPath` behavior is the default: a relative link/redirect rendered inside a splat route resolves against the **full current location, including the matched splat segment**. So from `/telemetry/traces/<id>` the sidebar's `to="errors"` resolved to `/telemetry/traces/<id>/errors` instead of `/telemetry/errors`. ...
commit 94a4662f5cac322a062d934a6de22ee29535e10b
Author: Burak Yigit Kaya <byk@sentry.io>
Date: Wed Jun 10 20:49:11 2026 +0000
fix(ui): make v7 router upgrade intentional and fix all splat path stacking
The sidebar fix in the previous commit addressed one symptom of a wider
issue: the root pnpm override `react-router-dom@>=6.0.0 <7.0.0: ">=6.30.4"`
had an unbounded value, so the declared `^6.30.2` silently resolved to
react-router-dom 7.17.0. Under v7, relative links inside splat routes
resolve against the full current location (including the splat segment), so
the detail tab bars and fallback redirects also stacked the path
(e.g. `/telemetry/traces/<id>/context/context`).
- declare react-router-dom ^7.17.0 (declared == installed); drop the
dead/buggy v6 overrides
- TelemetryTabs: add an absolute `basePath`; pass it from the Trace, Span
and Event detail tab bars (rendered under dynamic splat routes)
- make fallback ...
commit 94a4662f5cac322a062d934a6de22ee29535e10b
Author: Burak Yigit Kaya <byk@sentry.io>
Date: Wed Jun 10 20:49:11 2026 +0000
fix(ui): make v7 router upgrade intentional and fix all splat path stacking
The sidebar fix in the previous commit addressed one symptom of a wider
issue: the root pnpm override `react-router-dom@>=6.0.0 <7.0.0: ">=6.30.4"`
had an unbounded value, so the declared `^6.30.2` silently resolved to
react-router-dom 7.17.0. Under v7, relative links inside splat routes
resolve against the full current location (including the splat segment), so
the detail tab bars and fallback redirects also stacked the path
(e.g. `/telemetry/traces/<id>/context/context`).
- declare react-router-dom ^7.17.0 (declared == installed); drop the
dead/buggy v6 overrides
- TelemetryTabs: add an absolute `basePath`; pass it from the Trace, Span
and Event detail tab bars (rendered under dynamic splat routes)
- make fallback ...
Now let me read the full files for context:
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/App.tsx</path>
<type>file</type>
<content>
1: import { ErrorBoundary } from "@sentry/react";
2: import { ReactComponent as Logo } from "@spotlight/ui/assets/glyph.svg";
3: import { Navigate, Route, Routes } from "react-router-dom";
4: import { ShikiProvider } from "./ShikiProvider";
5: // TODO: we'll lazy load this in case of multiple routes
6: import { Telemetry } from "./telemetry";
7:
8: type AppProps = {
9: sidecarUrl: string;
10: };
11:
12: export default function App({ sidecarUrl }: AppProps) {
13: return (
14: <div className="from-primary-900 to-primary-950 flex h-full overflow-hidden bg-gradient-to-br from-0% to-20% font-sans text-white">
15: <ErrorBoundary fallback={<ErrorFallback />}>
16: <ShikiProvider>
17: <Routes>
18: {/* Default route redirects to telemetry */}
19: <Route path="/" element={<Navigate to="/telemetry" replace />} />
20:
21: ...
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/TelemetryTabs.tsx</path>
<type>file</type>
<content>
1: import type { TabPanel } from "@spotlight/ui/types";
2: import { NavLink } from "react-router-dom";
3:
4: type TabsProps = {
5: tabs: TabPanel<unknown>[];
6: /**
7: * Absolute base path the tabs live under (e.g. `/telemetry/traces/<id>`).
8: * Required for tabs rendered inside splat routes: React Router v7 resolves
9: * relative links against the full current location (including splat
10: * segments), which would otherwise stack the path on repeated navigation.
11: */
12: basePath?: string;
13: nested?: boolean;
14: };
15:
16: export default function TelemetryTabs({ tabs, basePath, nested = false }: TabsProps) {
17: return (
18: <nav className="flex border-b border-primary-700">
19: {tabs.map(tab => {
20: const tabPath = basePath ? `${basePath}/${tab.id}` : nested ? `./${tab.id}` : `/${tab.id}`;
21:
22: ...
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/TelemetryTabs.test.tsx</path>
<type>file</type>
<content>
1: import type { TabPanel } from "@spotlight/ui/types";
2: import { render } from "@testing-library/react";
3: import { MemoryRouter, Route, Routes } from "react-router-dom";
4: import { describe, expect, it } from "vitest";
5: import TelemetryTabs from "./TelemetryTabs";
6:
7: const tabs: TabPanel<unknown>[] = [
8: { id: "context", title: "Context" },
9: { id: "logs", title: "Logs" },
10: ];
11:
12: function renderTabsAt(initialPath: string) {
13: // Mirrors the real app: TelemetryTabs is rendered inside a splat route, as a
14: // sibling of the inner <Routes>. Under React Router v7 a relative link in this
15: // position resolves against the full current location (including the splat),
16: // so we pass an absolute basePath to keep tab targets stable. ...
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/TelemetryView.tsx</path>
<type>file</type>
<content>
1: import { log } from "@spotlight/ui/lib/logger";
2: import { getRouteStorageKey } from "@spotlight/ui/lib/routePersistence";
3: import { ElectronDragbarSpacer } from "@spotlight/ui/ui/electronDragbarSpacer";
4: import { useEffect } from "react";
5: import { Route, Routes, useLocation } from "react-router-dom";
6: import useSentryStore from "../store";
7: import ErrorsTab from "../tabs/ErrorsTab";
8: import InsightsTab from "../tabs/InsightsTab";
9: import LogsTab from "../tabs/LogsTab";
10: import TracesTab from "../tabs/TracesTab";
11: import { isErrorEvent } from "../utils/sentry";
12: import TelemetrySidebar from "./TelemetrySidebar";
13:
14: export default function TelemetryView({
15: isOnline,
16: contextId,
17: }: {
18: isOnline: boolean;
19: contextId: string;
20: }) {
21: const location = useLocation();
22: const store = ...
Now let me read the remaining modified files and the route structures:
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/tabs/TracesTab.tsx</path>
<type>file</type>
<content>
1: import { cn } from "@spotlight/ui/lib/cn";
2: import { useCallback, useRef, useState } from "react";
3: import { Route, Routes } from "react-router-dom";
4: import AITranscription from "../components/insights/aiTraces/AITranscription";
5: import { hasAISpans } from "../components/insights/aiTraces/sdks/aiLibraries";
6: import EmptyState from "../components/shared/EmptyState";
7: import Resizer from "../components/shared/Resizer";
8: import TraceDetails from "../components/traces/TraceDetails";
9: import TraceTreeview from "../components/traces/TraceDetails/components/TraceTreeview";
10: import TraceItem from "../components/traces/TraceItem";
11: import TraceList from "../components/traces/TraceList";
12: import TraceListFilter from "../components/traces/TraceListFilter";
13: import SpanDetails from "../components/traces/spans/SpanDetails";
14: import { ...
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/tabs/ErrorsTab.tsx</path>
<type>file</type>
<content>
1: import EventDetails from "../components/events/EventDetails";
2: import EventList from "../components/events/EventList";
3:
4: import { SentryEventsContextProvider } from "../data/sentryEventsContext";
5:
6: import { Route, Routes } from "react-router-dom";
7:
8: export default function ErrorsTab() {
9: return (
10: <SentryEventsContextProvider>
11: <Routes>
12: <Route path="/" element={<EventList />} />
13: <Route path="/:eventId/*" element={<EventDetails />} />
14: </Routes>
15: </SentryEventsContextProvider>
16: );
17: }
(End of file - total 17 lines)
</content>
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/traces/TraceDetails/index.tsx</path>
<type>file</type>
<content>
1: import { createTab } from "@spotlight/ui/telemetry/utils/tabs";
2: import { useMemo } from "react";
3: import { Navigate, Route, Routes } from "react-router-dom";
4: import TelemetryTabs from "../../TelemetryTabs";
5:
6: import { useSentryEvents } from "@spotlight/ui/telemetry/data/useSentryEvents";
7: import useSentryStore from "@spotlight/ui/telemetry/store/store";
8: import type { Trace } from "@spotlight/ui/telemetry/types";
9: import { getFormattedDuration } from "@spotlight/ui/telemetry/utils/duration";
10: import { isErrorEvent } from "@spotlight/ui/telemetry/utils/sentry";
11: import EventContexts from "../../events/EventContexts";
12: import EventList from "../../events/EventList";
13: import AITraceSplitView from "../../insights/aiTraces/AITraceSplitView";
14: import { hasAISpans } from ...
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/traces/spans/SpanDetails.tsx</path>
<type>file</type>
<content>
1: import JsonViewer from "@spotlight/ui/telemetry/components/shared/JsonViewer";
2: import { getFormattedDuration } from "@spotlight/ui/telemetry/utils/duration";
3: import { isErrorEvent } from "@spotlight/ui/telemetry/utils/sentry";
4: import { createTab } from "@spotlight/ui/telemetry/utils/tabs";
5: import { Navigate, Route, Routes } from "react-router-dom";
6: import { format as formatSQL } from "sql-formatter";
7: import { DB_SPAN_REGEX } from "../../../constants";
8: import useSentryStore from "../../../store";
9: import type { Span } from "../../../types";
10: import TelemetryTabs from "../../TelemetryTabs";
11: import EventList from "../../events/EventList";
12: import LogsList from "../../log/LogsList";
13: import { ContextView } from "../../shared/ContextView";
14: import DateTime from "../../shared/DateTime";
15:
16: function ...
Now let me check the remaining files — EventDetails, insights, TelemetrySidebar, and the Span type definition:
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/events/EventDetails.tsx</path>
<type>file</type>
<content>
1: import { Link, Outlet, Route, Routes, useParams } from "react-router-dom";
2: import useSentryStore from "../../store";
3: import type { SentryErrorEvent, SentryEvent } from "../../types";
4: import { isErrorEvent } from "../../utils/sentry";
5: import { createTab } from "../../utils/tabs";
6: import TelemetryTabs from "../TelemetryTabs";
7: import EmptyState from "../shared/EmptyState";
8: import PlatformIcon from "../shared/PlatformIcon";
9: import AICopyButton from "./AICopyButton";
10: import Event, { EventTitle } from "./Event";
11: import EventBreadcrumbs from "./EventBreadcrumbs";
12: import EventContexts from "./EventContexts";
13:
14: function renderEventTitle(event: SentryEvent) {
15: return <EventTitle event={event} />;
16: }
17:
18: export default function EventDetails() {
19: const { eventId } = useParams();
20: const ...
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/insights/index.tsx</path>
<type>file</type>
<content>
1: import { Navigate, Outlet, Route, Routes } from "react-router-dom";
2: import Profiles from "./Profiles";
3: import Queries from "./Queries";
4: import QuerySummary from "./QuerySummary";
5: import Resources from "./Resources";
6: import AItracesTab from "./aiTraces";
7: import EnvelopesTab from "./envelopes";
8: import SdksTab from "./sdks";
9: import WebVitals from "./webVitals";
10: import WebVitalsDetail from "./webVitals/WebVitalsDetail";
11:
12: export default function InsightsTabDetails() {
13: return (
14: <>
15: <div className="flex min-h-0 flex-1 flex-col overflow-x-hidden overflow-y-auto h-full">
16: <Routes>
17: <Route path="queries" element={<Queries />} />
18: <Route path="queries/:type" element={<QuerySummary />} />
19: <Route path="resources" element={<Resources />} />
20: ...
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/TelemetrySidebar.tsx</path>
<type>file</type>
<content>
1: import { ReactComponent as ChevronDownIcon } from "@spotlight/ui/assets/chevronDown.svg";
2: import { ReactComponent as DeleteIcon } from "@spotlight/ui/assets/deleteIcon.svg";
3: import { ReactComponent as Logo } from "@spotlight/ui/assets/glyph.svg";
4: import { cn } from "@spotlight/ui/lib/cn";
5: import { useSpotlightContext } from "@spotlight/ui/lib/useSpotlightContext";
6: import type { NotificationCount } from "@spotlight/ui/types";
7: import { Badge } from "@spotlight/ui/ui/badge";
8: import { ElectronDragbarSpacer } from "@spotlight/ui/ui/electronDragbarSpacer";
9: import { useCallback, useState } from "react";
10: import { Link, useLocation } from "react-router-dom";
11: import useSentryStore from "../store";
12:
13: interface TelemetrySidebarProps {
14: errorCount: number;
15: traceCount: number;
16: logCount: number;
17: ...
Now let me check the Span type definition to understand optional fields:
Found 7 matches /tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/types.ts: Line 89: span_id?: string; Line 92: parent_span_id?: string; Line 120: trace_id?: TraceId; Line 122: parent_span_id?: string | null; /tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/utils/traces.ts: Line 10: const parentComp = (a.parent_span_id ? 1 : 0) - (b.parent_span_id ? 1 : 0); Line 31: parent_span_id: parentParent ? parentParent.span_id : null, /tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/insights/aiTraces/sdks/vercelAISDK.ts: Line 236: span.span_id === rootSpan.span_id || rootSpan.children?.some(child => child.span_id === span.span_id);
Found 2 matches
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/types.ts:
Line 5: export type SpanId = string;
Line 119: export type Span = {
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/types.ts</path>
<type>file</type>
<content>
115: type?: "error" | "event" | "message" | "default";
116: exception: EventException;
117: };
118:
119: export type Span = {
120: trace_id?: TraceId;
121: span_id: SpanId;
122: parent_span_id?: string | null;
123: op?: string | null;
124: description?: string | null;
125: start_timestamp: number;
126: tags?: Tags | null;
127: timestamp: number;
128: status?: "ok" | string;
129: transaction?: SentryTransactionEvent;
130: children?: Span[];
131: data?: Record<string, unknown>;
132: };
133:
134: export type SentryTransactionEvent = CommonEventAttrs & {
135: type: "transaction";
136: spans?: Span[];
137: start_timestamp: string;
138: contexts: Contexts & {
139: trace: TraceContext;
140: };
141: };
142:
143: export type ProfileSample = {
144: elapsed_since_start_ns: string;
(Showing lines 115-144 of 511. ...
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/types.ts</path>
<type>file</type>
<content>
80: attachments?: EventAttachment[];
81: // Inferred source type for distinguishing browser/server/mobile events
82: __sourceType?: "browser" | "server" | "mobile";
83: };
84:
85: // Note: For some reason the `sentry/core` module doesn't have these additional properties
86: // in `EventEnvelopeHeaders['trace']` but they are present in the actual events.
87: // Follow up?
88: export type TraceContext = EventEnvelopeHeaders["trace"] & {
89: span_id?: string;
90: status?: "ok" | string;
91: description?: string;
92: parent_span_id?: string;
93: data?: Record<string, string>;
94: op?: string;
95: };
96:
97: export type Contexts = {
98: trace?: TraceContext;
99: } & {
(Showing lines 80-99 of 511. Use offset=100 to continue.)
</content>
Now let me check the Trace type and how trace_id is defined there:
Found 1 matches
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/types.ts:
Line 286: export type Trace = TraceContext & {
Found 4 matches
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/types.ts:
Line 4: export type TraceId = string;
Line 88: export type TraceContext = EventEnvelopeHeaders["trace"] & {
Line 286: export type Trace = TraceContext & {
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/store/types.ts:
Line 20: export type TraceSubscription = ["trace", (trace: Trace) => void];
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/types.ts</path>
<type>file</type>
<content>
286: export type Trace = TraceContext & {
287: trace_id: string;
288: transactions: SentryTransactionEvent[];
289: errors: number;
290: start_timestamp: number;
291: timestamp: number;
292: status?: "ok" | string;
293: rootTransaction: SentryTransactionEvent | null;
294: rootTransactionName: string;
295: spans: Map<string, Span>;
296: spanTree: Span[];
297: profileGrafted: boolean;
298: };
299:
300: export type Sdk = {
301: name: string;
302: version: string;
303: lastSeen: number;
304: };
305:
306: export type SentryEventWithPerformanceData = Omit<SentryEvent, "measurements"> & {
307: measurements: Record<
308: string,
309: {
310: value: number;
311: unit: string;
312: }
313: > & {
314: "score.total": {
315: value: number;
(Showing lines 286-315 of 511. Use offset=316 to continue.)
</content>
Now let me check for any remaining relative Link/NavLink/Navigate usages inside splat routes that might have been missed:
Found 10 matches
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/TelemetrySidebar.tsx:
Line 113: to="traces"
Line 119: to="errors"
Line 124: <NavigationLink to="logs" title="Logs" notificationCount={{ count: logCount }} isActive={isActive("logs")} />
Line 146: <NavigationLink to="insights/queries" title="Queries" isActive={isActive("insights/queries")} isSubItem />
Line 148: to="insights/webvitals"
Line 154: to="insights/resources"
Line 160: to="insights/profiles"
Line 166: to="insights/envelopes"
Line 171: <NavigationLink to="insights/sdks" title="SDKs" isActive={isActive("insights/sdks")} isSubItem />
Line 173: to="insights/aitraces"
Found 4 matches
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/App.tsx:
Line 19: <Route path="/" element={<Navigate to="/telemetry" replace />} />
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/traces/spans/SpanDetails.tsx:
Line 129: element={<Navigate to={`/telemetry/traces/${span.trace_id}/spans/${span.span_id}/context`} replace />}
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/insights/index.tsx:
Line 27: <Route path="*" element={<Navigate to="/telemetry/insights/queries" replace />} />
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/traces/TraceDetails/index.tsx:
Line 123: <Route path="*" element={<Navigate to={`/telemetry/traces/${trace.trace_id}/context`} replace />} />
Let me check the TelemetrySidebar more carefully — it uses to="traces" etc. which are passed into NavigationLink, which prepends /telemetry/:
No files found
Found 13 matches
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/TelemetryTabs.tsx:
Line 23: <NavLink
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/events/EventDetails.tsx:
Line 51: <Link className="cursor-pointer underline" to={`/telemetry/traces/${traceCtx.trace_id}`}>
Line 57: <Link
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/events/EventList.tsx:
Line 21: <Link
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/traces/TraceItem.tsx:
Line 81: <Link
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/traces/spans/SpanItem.tsx:
Line 59: <Link
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/TelemetrySidebar.tsx:
Line 34: ...
Let me check the remaining Link usages that could be inside splat routes for completeness:
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/events/EventList.tsx</path>
<type>file</type>
<content>
15: const matchingEvents = events.filter(isErrorEvent);
16:
17: return matchingEvents.length !== 0 ? (
18: <CardList>
19: {matchingEvents.map(e => {
20: return (
21: <Link
22: className="hover:bg-primary-900 flex cursor-pointer items-center gap-x-4 px-6 py-2"
23: key={e.event_id}
24: to={`/telemetry/errors/${e.event_id}/details`}
25: >
26: <PlatformIcon event={e} className="text-primary-300 rounded-md" />
27: <div className="text-primary-300 flex w-48 flex-col truncate font-mono text-sm">
28: <div className="flex items-center gap-x-2">
29: <div>{truncateId(e.event_id)}</div>
30: <OriginBadge sourceType={e.__sourceType} />
31: </div>
32: <span />
33: <TimeSince ...
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/traces/TraceItem.tsx</path>
<type>file</type>
<content>
75:
76: // TODO: if (spanId && !span) -> error
77:
78: // TODO: For this #<traceId> link to work as intended, we need to do something like this:
79: // https://dev.to/mindactuate/scroll-to-anchor-element-with-react-router-v6-38op
80: return (
81: <Link
82: className={cn(
83: "hover:bg-primary-900 flex cursor-pointer items-center gap-x-4 px-6 py-2",
84: isSelected && "bg-primary-800",
85: className,
86: )}
87: to={isSelected && !spanId ? `/telemetry/traces#${trace.trace_id}` : `/telemetry/traces/${trace.trace_id}/context`}
88: >
89: <TraceIcon trace={trace} />
(Showing lines 75-89 of 107. Use offset=90 to continue.)
</content>
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/traces/spans/SpanItem.tsx</path>
<type>file</type>
<content>
50: const newLeftWidth = ((mouseX - containerRect.left) / containerRect.width) * 100;
51: setSpanNodeWidth(newLeftWidth);
52: }
53: };
54:
55: const isQueried = !showOnlyMatched && query && matchesQuery(span);
56:
57: return (
58: <li key={span.span_id} ref={containerRef}>
59: <Link
60: className={cn(
61: "hover:bg-primary-700 group flex rounded-xs text-sm",
62: isQueried ? "bg-primary-200/20" : "",
63: spanId === span.span_id ? "bg-primary-900" : "",
64: span.tags?.source === "profile" ? "text-lime-500" : "",
65: )}
66: style={{
67: pointerEvents: isResizing ? "none" : "auto",
68: }}
69: to={`/telemetry/traces/${span.trace_id}/spans/${span.span_id}`}
(Showing lines 50-69 of 153. Use offset=70 to continue.)
</content>
Now let me check the insights sub-routes for any relative navigations inside splat routes:
Found 1 matches
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/insights/aiTraces/AITranscription.tsx:
Line 242: <Link to={`/telemetry/traces/${traceId}/spans/${message.spanId}`} className="max-w-[80%]">
Found 1 matches
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/insights/envelopes/EnvelopeList.tsx:
Line 37: <Link key={envelopeId} to={`/telemetry/insights/envelopes/${envelopeId}`}>
No files found
Let me also check the LogsList component for any relative links:
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/log/LogsList.tsx</path>
<type>file</type>
<content>
185: );
186: }
187: if (header.id === "trace_id" && isColumnVisible("trace_id")) {
188: return (
189: <td key="trace_id" className={cn("text-sm", paddings)}>
190: {log.trace_id ? (
191: <Link
192: to={`/telemetry/traces/${log.trace_id}`}
193: className="text-blue-400 hover:text-blue-300 underline max-w-[150px] truncate block"
194: onClick={e => e.stopPropagation()}
195: >
196: {log.trace_id}
197: </Link>
198: ) : (
199: <span className="text-primary-300 max-w-[150px] truncate block">N/A</span>
200: )}
201: ...
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/insights/QuerySummary.tsx</path>
<type>file</type>
<content>
125: maxValue={maxTime}
126: title={span.trace_id}
127: className="text-lime-500"
128: >
129: <Link className="truncate hover:underline" to={`/telemetry/traces/${span.trace_id}`}>
130: {truncateId(span.trace_id)}
131: </Link>
132: </TimeBar>
133: </td>
134: <td className="text-primary-200 w-[15%] whitespace-nowrap px-6 py-4 text-right text-sm font-medium">
135: <span className={getSpanDurationClassName(span.timestamp - span.start_timestamp)}>
136: {getFormattedDuration(span.timestamp - span.start_timestamp)}
137: </span>
138: </td>
139: <td className="text-primary-200 w-[15%] whitespace-nowrap px-6 ...
Good — all remaining Link usages across the codebase already use absolute paths. Now let me check if there's a Navigate fallback in EventDetails that was missed:
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/events/EventDetails.tsx</path>
<type>file</type>
<content>
68: <div className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-hidden">
69: <Routes>
70: <Route path="breadcrumbs" element={<EventBreadcrumbs event={event} />} />
71: <Route path="contexts" element={<EventContexts event={event} />} />
72: {/* Default tab */}
73: <Route path="*" element={<Event event={event} />} />
74: </Routes>
75: <Outlet />
76: </div>
77: </div>
78: );
79: }
(End of file - total 79 lines)
</content>
Found 3 matches
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/insights/index.tsx:
Line 27: <Route path="*" element={<Navigate to="/telemetry/insights/queries" replace />} />
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/traces/spans/SpanDetails.tsx:
Line 129: element={<Navigate to={`/telemetry/traces/${span.trace_id}/spans/${span.span_id}/context`} replace />}
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/traces/TraceDetails/index.tsx:
Line 123: <Route path="*" element={<Navigate to={`/telemetry/traces/${trace.trace_id}/context`} replace />} />
Now let me check the InsightsTab to understand the routing context for insights/index.tsx:
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/tabs/InsightsTab.tsx</path>
<type>file</type>
<content>
1: import InsightsTabDetails from "../components/insights";
2: import { SentryEventsContextProvider } from "../data/sentryEventsContext";
3:
4: import { Route, Routes } from "react-router-dom";
5:
6: export default function InsightsTab() {
7: return (
8: <SentryEventsContextProvider>
9: <Routes>
10: <Route path="/*" element={<InsightsTabDetails />} />
11: </Routes>
12: </SentryEventsContextProvider>
13: );
14: }
(End of file - total 14 lines)
</content>
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/tabs/LogsTab.tsx</path>
<type>file</type>
<content>
1: import Logs from "../components/log";
2: import { SentryEventsContextProvider } from "../data/sentryEventsContext";
3:
4: import { Route, Routes } from "react-router-dom";
5:
6: export default function LogsTab() {
7: return (
8: <SentryEventsContextProvider>
9: <Routes>
10: <Route path="/*" element={<Logs />} />
11: </Routes>
12: </SentryEventsContextProvider>
13: );
14: }
(End of file - total 14 lines)
</content>
Let me now check the useParams call and how eventId gets used — specifically whether it's always defined when building the basePath:
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/events/EventDetails.tsx</path>
<type>file</type>
<content>
18: export default function EventDetails() {
19: const { eventId } = useParams();
20: const getEventById = useSentryStore(state => state.getEventById);
21:
22: if (!eventId) {
23: return <EmptyState description="Unknown event id." />;
24: }
25:
26: const event = getEventById(eventId);
27:
28: if (!event) {
29: return <EmptyState description="Event not found." />;
30: }
31:
32: const tabs = [
(Showing lines 18-32 of 79. Use offset=33 to continue.)
</content>
Good — eventId is guarded at line 22-24 before reaching the basePath at line 67. Now let me check the span_id optionality in SpanDetails — the Span type has span_id: SpanId (required) and trace_id?: TraceId (optional):
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/traces/spans/SpanDetails.tsx</path>
<type>file</type>
<content>
94: export default function SpanDetails({
95: span,
96: }: {
97: span: Span;
98: }) {
99: const getEventsByTrace = useSentryStore(state => state.getEventsByTrace);
100:
101: // TODO: try to narrow errors to the span and its children?
102: const errors = span.trace_id ? getEventsByTrace(span.trace_id).filter(isErrorEvent) : [];
103: const errorCount = errors.length;
(Showing lines 94-103 of 135. Use offset=104 to continue.)
</content>
Now let me verify the package.json and pnpm-lock.yaml changes, and check for any react-router-dom v7 breaking changes beyond splat path behavior:
<path>/tmp/opencode/spotlight-eval/packages/spotlight/package.json</path> <type>file</type> <content> 115: "magic-string": "^0.30.11", 116: "nanovis": "^0.1.3", 117: "platformicons": "^7.0.4", 118: "react": "catalog:", 119: "react-diff-viewer-continued": "^3.4.0", 120: "react-dom": "catalog:", 121: "react-router-dom": "^7.17.0", 122: "rollup-plugin-sourcemaps2": "^0.4.1", 123: "shiki": "^3.13.0", 124: "sql-formatter": "^12.2.4", (Showing lines 115-124 of 144. Use offset=125 to continue.) </content>
<path>/tmp/opencode/spotlight-eval/package.json</path>
<type>file</type>
<content>
50: "body-parser": ">=2.2.1",
51: "mdast-util-to-hast": ">=13.2.1",
52: "defu": ">=6.1.5",
53: "fast-uri": ">=3.1.2",
54: "ip-address": ">=10.1.1",
55: "lodash": ">=4.18.0",
56: "@xmldom/xmldom": ">=0.8.13",
57: "postcss": ">=8.5.10",
58: "qs": ">=6.15.2",
59: "svelte": ">=5.55.7",
60: "ws": ">=8.20.1"
61: }
62: },
63: "simple-git-hooks": {
64: "pre-commit": "pnpm exec lint-staged --concurrent false"
(Showing lines 50-64 of 80. Use offset=65 to continue.)
</content>
Now let me check how TracesTab renders the detail views — specifically how SpanDetails gets its span prop to understand the trace_id optionality issue:
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/tabs/TracesTab.tsx</path>
<type>file</type>
<content>
130: setIsResizing={setIsResizing}
131: direction="column"
132: className="w-1 shrink-0 cursor-col-resize bg-gray-600 hover:bg-blue-500"
133: />
134:
135: {/* right panel - selected trace content */}
136: <div className="flex-1 overflow-hidden" style={{ width: `${100 - leftPanelWidth}%` }}>
137: {span && !aiConfig.mode ? ...
Let me trace the path further — TraceList renders TraceSplitViewLayout, which passes span to SpanDetails. Let me check how the span is obtained:
Found 5 matches
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/tabs/TracesTab.tsx:
Line 23: interface TraceSplitViewLayoutProps {
Line 32: export function AITraceToggle({ trace, aiConfig }: { trace: Trace; aiConfig: TraceSplitViewLayoutProps["aiConfig"] }) {
Line 69: export function TraceSplitViewLayout({ trace, span, aiConfig }: TraceSplitViewLayoutProps) {
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/traces/TraceList.tsx:
Line 4: import { TraceSplitViewLayout } from "../../tabs/TracesTab";
Line 55: <TraceSplitViewLayout
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/components/traces/TraceList.tsx</path>
<type>file</type>
<content>
1: import CardList from "@spotlight/ui/telemetry/components/shared/CardList";
2: import { useCallback, useEffect, useRef, useState } from "react";
3: import { useParams } from "react-router-dom";
4: import { TraceSplitViewLayout } from "../../tabs/TracesTab";
5: import type { Trace } from "../../types";
6: import EmptyState from "../shared/EmptyState";
7: import TraceItem from "./TraceItem";
8:
9: type TraceListProps = {
10: traceData: {
11: filtered: Trace[];
12: all: Trace[];
13: };
14: };
15:
16: export default function TraceList({ traceData }: TraceListProps) {
17: const [aiMode, setAiMode] = useState(false);
18: const onToggle = useCallback(() => {
19: setAiMode(prev => !prev);
20: }, []);
21: const { traceId, spanId } = useParams<{ traceId: string; spanId: string }>();
22: const selectedTraceRef = ...
Now I have a complete picture. Let me also verify one more thing — the TabPanel type to understand id:
Found 5 matches
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/types.ts:
Line 14: export type TabPanel<T> = {
Line 47: panels?: (context: { processedEvents: T[] }) => TabPanel<T>[];
/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/telemetry/utils/tabs.ts:
Line 1: import type { TabPanel } from "@spotlight/ui/types";
Line 6: extra: Partial<Omit<TabPanel<T>, "id" | "title">> = {},
Line 7: ): TabPanel<T> {
<path>/tmp/opencode/spotlight-eval/packages/spotlight/src/ui/types.ts</path>
<type>file</type>
<content>
14: export type TabPanel<T> = {
15: /**
16: * Id of the tab. This needs to be a unique name.
17: */
18: id: string;
19:
20: /**
21: * Title of the tab. This is what will be displayed in the UI.
22: */
23: title: string;
24:
25: /**
26: * If this property is set, a count badge will be displayed
27: * next to the tab title with the specified value.
28: */
29: notificationCount?: NotificationCount;
30:
31: /**
32: * JSX content of the tab. Go crazy, this is all yours!
33: */
(Showing lines 14-33 of 52. Use offset=34 to continue.)
</content>
I now have a complete understanding of the entire change. Let me compile my review.
package.json (root)Verdict: PASS
Removes the two buggy pnpm.overrides entries (react-router@>=6.7.0 <7.0.0 and react-router-dom@>=6.0.0 <7.0.0) whose unbounded replacement values (">=6.30.4") caused the silent v6→v7 upgrade. Clean removal.
packages/spotlight/package.jsonVerdict: PASS
Changes react-router-dom from ^6.30.2 to ^7.17.0, making the declared version match what was already installed. The "files": ["dist"] reformatting is cosmetic/lint noise — harmless.
packages/spotlight/src/ui/telemetry/components/TelemetryTabs.tsxVerdict: PASS with NITs
The fix itself is correct: when basePath is provided, tab links resolve to ${basePath}/${tab.id} — fully absolute, immune to splat resolution.
NIT 1: The nested prop is dead code at all three call sites. Every caller passes both basePath AND nested:
TraceDetails/index.tsx:114: basePath={...} nestedSpanDetails.tsx:119: basePath={...} nestedEventDetails.tsx:67: basePath={...} nestedWith the ternary basePath ? ... : nested ? ... : ..., the basePath branch always wins, making nested unreachable at these sites. This isn't a bug — it's a dead-code smell. The nested prop should either be removed from these call sites (cleaner) or the relationship between basePath and nested should be documented as mutually exclusive.
NIT 2: No trailing-slash guard on basePath. If a caller accidentally passes basePath="/telemetry/traces/abc/" (trailing slash), the result would be /telemetry/traces/abc//context (double slash). Low risk since all current callers use template literals without trailing slashes, but a defensive .replace(/\/+$/, '') would be prudent.