feat(observability): consolidate observability UI updates - #1975
feat(observability): consolidate observability UI updates#1975mantrakp04 wants to merge 1 commit into
Conversation
Align issue, trace, performance, source-map, analytics-path, and shared dashboard surfaces with the new backend contracts. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
14 issues found across 47 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/stack-frame-list.tsx">
<violation number="1" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/stack-frame-list.tsx:186">
P2: When occurrence navigation changes an all-library run to the same run followed by an app frame, this key stays unchanged although `defaultExpanded` changes. React preserves `CollapsedFrameGroup`'s old `expanded` state, so the new occurrence ignores its default; include `defaultExpanded` and the complete run identity in the key.</violation>
</file>
<file name="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/paths/normalize-url.ts">
<violation number="1" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/paths/normalize-url.ts:22">
P2: 16–19-character URL-safe base64 IDs are no longer normalized, so routes containing them remain split instead of grouping by route shape. The `api` and `auth` examples already fail the 16-character regex; keep the runtime check aligned with `BASE64_TOKEN_REGEX` and add a regression test for the intended minimum.</violation>
</file>
<file name="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/issue-format.ts">
<violation number="1" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/issue-format.ts:92">
P3: This insertion detaches the existing `issueCulprit` JSDoc from the function and associates it with `SERVER_UNKNOWN_CULPRIT_SENTINEL`, so editor/API documentation describes the wrong symbol. Move the sentinel declaration before the helper's JSDoc, or place the helper JSDoc after the sentinel declaration.</violation>
</file>
<file name="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/releases/page-client.tsx">
<violation number="1" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/releases/page-client.tsx:287">
P2: When an add-commit request completes after the admin selects another release, this guard leaves the new release selected, but the following setters clear its form and show a success notice for the old release. Guard the commit form resets and notice with the same stale-operation check.</violation>
<violation number="2" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/releases/page-client.tsx:287">
P3: The guarded-refresh pattern is now duplicated verbatim between addCommit and addDeployment (fetch by selectedRelease.version, then setSelectedRelease with the id-match guard). Consider extracting a small `refreshSelectedRelease(refreshedRelease)` helper so the stale-selection logic stays in one place and is used consistently (the create path still sets selectedRelease unconditionally).</violation>
</file>
<file name="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/traces/page-client.tsx">
<violation number="1" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/traces/page-client.tsx:790">
P2: When a trace has more than 10,000 spans, events from spans after the retained oldest 10,000 are omitted from the waterfall. Extend the event window to the current time, or obtain the uncapped trace interval, whenever the span result is capped.</violation>
<violation number="2" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/traces/page-client.tsx:813">
P3: When a trace has exactly 1,000 span links, this condition still shows a warning claiming that further links exist. Fetch one extra row and compare with the cap, or use a count query before setting this flag.</violation>
</file>
<file name="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/page-client.tsx">
<violation number="1" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/page-client.tsx:86">
P3: This re-implements the namespaced select-value codec that already exists for the service filter in `service-identity.ts`. The environment variant is simpler (plain string rather than an object) but the sentinel/prefix convention is duplicated in the page component instead of next to that codec, risking drift. Consider colocating the environment codec with service-identity's or documenting the shared convention.</violation>
<violation number="2" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/page-client.tsx:251">
P2: When a list refresh completes while a status PATCH is pending, this reverse adjustment applies to fresh server counts instead of the optimistic baseline, so the status tabs can show incorrect totals. Track whether the optimistic delta is still present before undoing it, or reconcile with a fresh server response.</violation>
</file>
<file name="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/format.ts">
<violation number="1" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/format.ts:72">
P3: At the top of the k range the output rolls over to "1000k" rather than into M. `formatCount(999_500)` produces "1000k" because `(value/1000).toFixed(0)` rounds to 1000 and the `k` suffix is appended, while the M branch handles its own rollover gracefully (`9_999_999` → "10.0M"). The two magnitude branches are inconsistent: the k branch never carries the decimal at its top, so it prints the malformed "1000k" instead of "1.0M". Guard the `k` result so it rolls into M when it rounds to 1000.</violation>
</file>
<file name="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/issue-event-search.tsx">
<violation number="1" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/issue-event-search.tsx:55">
P2: When Load more fails, this catch removes all successfully loaded pages and the cursor needed to retry. Only clear `items` and `nextCursor` for a fresh search; preserve them when `cursor` is non-null.</violation>
<violation number="2" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/issue-event-search.tsx:106">
P2: After page 1, editing a search field or parent filter makes Load more send the old cursor with new filters, which the backend rejects as an invalid cursor. Store the complete filter snapshot with the cursor and use that snapshot for subsequent pages.</violation>
</file>
<file name="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/[issueId]/page-client.tsx">
<violation number="1" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/[issueId]/page-client.tsx:184">
P2: `PageClient` now calls `useSearchParams()`, but `page.tsx` renders it directly as a static server component with no `<Suspense>` boundary. A client component that reads search params inside a statically prerendered page must be wrapped in `Suspense`, or the page can fail prerendering with `useSearchParams() should be wrapped in a suspense boundary`. Wrap the component in `<Suspense>` (or confirm the page isn't statically prerendered).</violation>
</file>
<file name="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/use-issue-data.ts">
<violation number="1" location="apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/use-issue-data.ts:173">
P3: The sparkline grid is anchored to the browser's `Date.now()`, but the query window is anchored to the server's `now64(3)`. If the two clocks disagree by more than one bucket step (an hour on the 24h range, a minute on the 1h range), the newest server bucket falls outside the client-rebuilt grid and is silently dropped by the bounds check, so the most recent activity vanishes with no error surfaced. This mirrors the existing services `buildServiceTimelines`, which has the same skew dependency, but the added `Date.now()`-based grid here reintroduces it for issues. Anchoring to server time (e.g. snapping via the same `now64(3)` the query uses, or deriving `nowMs` from the returned data) would avoid the silent loss; at minimum, document the skew assumption rather than treating the drop as only the intentional partial-bucket clipping.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // default) onto a DIFFERENT frame run after occurrence | ||
| // navigation swaps the stack. First/last location plus length is | ||
| // enough to distinguish runs without hashing every frame. | ||
| key={`collapsed-${group.startIndex}-${group.frames.length}-${frameLocationLabel(group.frames[0] ?? throwMissingCollapsedFrame())}-${frameLocationLabel(group.frames[group.frames.length - 1] ?? throwMissingCollapsedFrame())}`} |
There was a problem hiding this comment.
P2: When occurrence navigation changes an all-library run to the same run followed by an app frame, this key stays unchanged although defaultExpanded changes. React preserves CollapsedFrameGroup's old expanded state, so the new occurrence ignores its default; include defaultExpanded and the complete run identity in the key.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/stack-frame-list.tsx, line 186:
<comment>When occurrence navigation changes an all-library run to the same run followed by an app frame, this key stays unchanged although `defaultExpanded` changes. React preserves `CollapsedFrameGroup`'s old `expanded` state, so the new occurrence ignores its default; include `defaultExpanded` and the complete run identity in the key.</comment>
<file context>
@@ -163,7 +177,13 @@ export function StackFrameList({
+ // default) onto a DIFFERENT frame run after occurrence
+ // navigation swaps the stack. First/last location plus length is
+ // enough to distinguish runs without hashing every frame.
+ key={`collapsed-${group.startIndex}-${group.frames.length}-${frameLocationLabel(group.frames[0] ?? throwMissingCollapsedFrame())}-${frameLocationLabel(group.frames[group.frames.length - 1] ?? throwMissingCollapsedFrame())}`}
frames={group.frames}
defaultExpanded={group.defaultExpanded}
</file context>
| key={`collapsed-${group.startIndex}-${group.frames.length}-${frameLocationLabel(group.frames[0] ?? throwMissingCollapsedFrame())}-${frameLocationLabel(group.frames[group.frames.length - 1] ?? throwMissingCollapsedFrame())}`} | |
| key={`collapsed-${group.startIndex}-${group.defaultExpanded ? "expanded" : "collapsed"}-${group.frames.map((frame) => `${frameFunctionLabel(frame)}:${frameLocationLabel(frame)}`).join("|")}`} |
| // Length 20, not the regex's 16: shorter base64-shaped words (`api`, `auth`) | ||
| // would otherwise collapse into `:id`. | ||
| if (segment.length >= 20 && BASE64_TOKEN_REGEX.test(segment)) return true; |
There was a problem hiding this comment.
P2: 16–19-character URL-safe base64 IDs are no longer normalized, so routes containing them remain split instead of grouping by route shape. The api and auth examples already fail the 16-character regex; keep the runtime check aligned with BASE64_TOKEN_REGEX and add a regression test for the intended minimum.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/paths/normalize-url.ts, line 22:
<comment>16–19-character URL-safe base64 IDs are no longer normalized, so routes containing them remain split instead of grouping by route shape. The `api` and `auth` examples already fail the 16-character regex; keep the runtime check aligned with `BASE64_TOKEN_REGEX` and add a regression test for the intended minimum.</comment>
<file context>
@@ -4,45 +4,36 @@
- // Base64 tokens (only for longer segments to avoid false positives on
- // short path segments like "api" or "auth")
+ // Length 20, not the regex's 16: shorter base64-shaped words (`api`, `auth`)
+ // would otherwise collapse into `:id`.
if (segment.length >= 20 && BASE64_TOKEN_REGEX.test(segment)) return true;
</file context>
| // Length 20, not the regex's 16: shorter base64-shaped words (`api`, `auth`) | |
| // would otherwise collapse into `:id`. | |
| if (segment.length >= 20 && BASE64_TOKEN_REGEX.test(segment)) return true; | |
| // Base64-like tokens use the regex's 16-character lower bound. | |
| if (BASE64_TOKEN_REGEX.test(segment)) return true; |
| // flight, and unconditionally setting would yank the view back to the | ||
| // release the commit was added to. | ||
| const refreshedRelease = await fetchReleaseByVersion(adminApp, selectedRelease.version); | ||
| setSelectedRelease((current) => (current?.id === refreshedRelease.id ? refreshedRelease : current)); |
There was a problem hiding this comment.
P2: When an add-commit request completes after the admin selects another release, this guard leaves the new release selected, but the following setters clear its form and show a success notice for the old release. Guard the commit form resets and notice with the same stale-operation check.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/releases/page-client.tsx, line 287:
<comment>When an add-commit request completes after the admin selects another release, this guard leaves the new release selected, but the following setters clear its form and show a success notice for the old release. Guard the commit form resets and notice with the same stale-operation check.</comment>
<file context>
@@ -270,7 +279,12 @@ export default function PageClient() {
+ // flight, and unconditionally setting would yank the view back to the
+ // release the commit was added to.
+ const refreshedRelease = await fetchReleaseByVersion(adminApp, selectedRelease.version);
+ setSelectedRelease((current) => (current?.id === refreshedRelease.id ? refreshedRelease : current));
setCommitSha("");
setCommitMessage("");
</file context>
| // Open spans (endMs == null) extend the interval to "now". A capped | ||
| // trace can under-report its true end; the capped-trace alert already | ||
| // flags that view as partial. | ||
| const spanEndMs = Math.max(...spans.map((span) => span.endMs ?? loadedAtMs)); |
There was a problem hiding this comment.
P2: When a trace has more than 10,000 spans, events from spans after the retained oldest 10,000 are omitted from the waterfall. Extend the event window to the current time, or obtain the uncapped trace interval, whenever the span result is capped.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/traces/page-client.tsx, line 790:
<comment>When a trace has more than 10,000 spans, events from spans after the retained oldest 10,000 are omitted from the waterfall. Extend the event window to the current time, or obtain the uncapped trace interval, whenever the span result is capped.</comment>
<file context>
@@ -738,28 +764,53 @@ export default function PageClient() {
+ // Open spans (endMs == null) extend the interval to "now". A capped
+ // trace can under-report its true end; the capped-trace alert already
+ // flags that view as partial.
+ const spanEndMs = Math.max(...spans.map((span) => span.endMs ?? loadedAtMs));
+ const eventQuery = getSelectedTraceEventQuery(traceId, highlightEventAtMs, {
+ // A deep-linked highlight timestamp widens the window so clock skew
</file context>
| const spanEndMs = Math.max(...spans.map((span) => span.endMs ?? loadedAtMs)); | |
| const spanEndMs = Math.max( | |
| ...spans.map((span) => span.endMs ?? loadedAtMs), | |
| spansResponse.result.length >= 10000 ? loadedAtMs : -Infinity, | |
| ); |
| // Narrow catch around one call: revert and surface. Never swallowed, and | ||
| // never a toast — a failed state change must stay on screen. | ||
| setOverrides(clearOptimisticStatus(overridesRef.current, issue.id)); | ||
| setCounts((current) => adjustIssueStatusCounts(current, status, from)); |
There was a problem hiding this comment.
P2: When a list refresh completes while a status PATCH is pending, this reverse adjustment applies to fresh server counts instead of the optimistic baseline, so the status tabs can show incorrect totals. Track whether the optimistic delta is still present before undoing it, or reconcile with a fresh server response.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/page-client.tsx, line 251:
<comment>When a list refresh completes while a status PATCH is pending, this reverse adjustment applies to fresh server counts instead of the optimistic baseline, so the status tabs can show incorrect totals. Track whether the optimistic delta is still present before undoing it, or reconcile with a fresh server response.</comment>
<file context>
@@ -193,19 +221,37 @@ export default function PageClient() {
// Narrow catch around one call: revert and surface. Never swallowed, and
// never a toast — a failed state change must stay on screen.
setOverrides(clearOptimisticStatus(overridesRef.current, issue.id));
+ setCounts((current) => adjustIssueStatusCounts(current, status, from));
setStatusError(error instanceof Error ? error.message : String(error));
+ } finally {
</file context>
| .map(parseTraceLinkRow) | ||
| .filter((link): link is TraceLink => link != null)); | ||
| setTraceResultWasCapped(spansResponse.result.length >= 10000); | ||
| setLinksResultWasCapped(linksResponse.result.length >= SPAN_LINKS_CAP); |
There was a problem hiding this comment.
P3: When a trace has exactly 1,000 span links, this condition still shows a warning claiming that further links exist. Fetch one extra row and compare with the cap, or use a count query before setting this flag.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/traces/page-client.tsx, line 813:
<comment>When a trace has exactly 1,000 span links, this condition still shows a warning claiming that further links exist. Fetch one extra row and compare with the cap, or use a count query before setting this flag.</comment>
<file context>
@@ -738,28 +764,53 @@ export default function PageClient() {
.map(parseTraceLinkRow)
.filter((link): link is TraceLink => link != null));
setTraceResultWasCapped(spansResponse.result.length >= 10000);
+ setLinksResultWasCapped(linksResponse.result.length >= SPAN_LINKS_CAP);
setNowMs(Date.now());
} catch (e) {
</file context>
| if (!Number.isFinite(value)) throw new Error(`Cannot format a non-finite count: ${value}`); | ||
| if (value < 0) throw new Error(`Cannot format a negative count: ${value}`); | ||
| if (value < 10_000) return value.toLocaleString(); | ||
| if (value < 1_000_000) return `${(value / 1_000).toFixed(value < 100_000 ? 1 : 0)}k`; |
There was a problem hiding this comment.
P3: At the top of the k range the output rolls over to "1000k" rather than into M. formatCount(999_500) produces "1000k" because (value/1000).toFixed(0) rounds to 1000 and the k suffix is appended, while the M branch handles its own rollover gracefully (9_999_999 → "10.0M"). The two magnitude branches are inconsistent: the k branch never carries the decimal at its top, so it prints the malformed "1000k" instead of "1.0M". Guard the k result so it rolls into M when it rounds to 1000.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/format.ts, line 72:
<comment>At the top of the k range the output rolls over to "1000k" rather than into M. `formatCount(999_500)` produces "1000k" because `(value/1000).toFixed(0)` rounds to 1000 and the `k` suffix is appended, while the M branch handles its own rollover gracefully (`9_999_999` → "10.0M"). The two magnitude branches are inconsistent: the k branch never carries the decimal at its top, so it prints the malformed "1000k" instead of "1.0M". Guard the `k` result so it rolls into M when it rounds to 1000.</comment>
<file context>
@@ -51,6 +55,24 @@ export function formatDuration(ms: number | null): string {
+ if (!Number.isFinite(value)) throw new Error(`Cannot format a non-finite count: ${value}`);
+ if (value < 0) throw new Error(`Cannot format a negative count: ${value}`);
+ if (value < 10_000) return value.toLocaleString();
+ if (value < 1_000_000) return `${(value / 1_000).toFixed(value < 100_000 ? 1 : 0)}k`;
+ return `${(value / 1_000_000).toFixed(value < 10_000_000 ? 1 : 0)}M`;
+}
</file context>
| if (value < 1_000_000) return `${(value / 1_000).toFixed(value < 100_000 ? 1 : 0)}k`; | |
| if (value < 1_000_000) { | |
| const k = (value / 1_000).toFixed(value < 100_000 ? 1 : 0); | |
| return k === "1000" ? "1.0M" : `${k}k`; | |
| } |
| // flight, and unconditionally setting would yank the view back to the | ||
| // release the commit was added to. | ||
| const refreshedRelease = await fetchReleaseByVersion(adminApp, selectedRelease.version); | ||
| setSelectedRelease((current) => (current?.id === refreshedRelease.id ? refreshedRelease : current)); |
There was a problem hiding this comment.
P3: The guarded-refresh pattern is now duplicated verbatim between addCommit and addDeployment (fetch by selectedRelease.version, then setSelectedRelease with the id-match guard). Consider extracting a small refreshSelectedRelease(refreshedRelease) helper so the stale-selection logic stays in one place and is used consistently (the create path still sets selectedRelease unconditionally).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/releases/page-client.tsx, line 287:
<comment>The guarded-refresh pattern is now duplicated verbatim between addCommit and addDeployment (fetch by selectedRelease.version, then setSelectedRelease with the id-match guard). Consider extracting a small `refreshSelectedRelease(refreshedRelease)` helper so the stale-selection logic stays in one place and is used consistently (the create path still sets selectedRelease unconditionally).</comment>
<file context>
@@ -270,7 +279,12 @@ export default function PageClient() {
+ // flight, and unconditionally setting would yank the view back to the
+ // release the commit was added to.
+ const refreshedRelease = await fetchReleaseByVersion(adminApp, selectedRelease.version);
+ setSelectedRelease((current) => (current?.id === refreshedRelease.id ? refreshedRelease : current));
setCommitSha("");
setCommitMessage("");
</file context>
| const { query, params } = getIssueSparklineQuery(hours, wanted); | ||
| const response = await queryObservability(adminApp, { query, params }); | ||
| const parsed = parseIssueSparklineRows(response.result, wanted); | ||
| const parsed = parseIssueSparklineRows(response.result, wanted, hours, Date.now()); |
There was a problem hiding this comment.
P3: The sparkline grid is anchored to the browser's Date.now(), but the query window is anchored to the server's now64(3). If the two clocks disagree by more than one bucket step (an hour on the 24h range, a minute on the 1h range), the newest server bucket falls outside the client-rebuilt grid and is silently dropped by the bounds check, so the most recent activity vanishes with no error surfaced. This mirrors the existing services buildServiceTimelines, which has the same skew dependency, but the added Date.now()-based grid here reintroduces it for issues. Anchoring to server time (e.g. snapping via the same now64(3) the query uses, or deriving nowMs from the returned data) would avoid the silent loss; at minimum, document the skew assumption rather than treating the drop as only the intentional partial-bucket clipping.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/use-issue-data.ts, line 173:
<comment>The sparkline grid is anchored to the browser's `Date.now()`, but the query window is anchored to the server's `now64(3)`. If the two clocks disagree by more than one bucket step (an hour on the 24h range, a minute on the 1h range), the newest server bucket falls outside the client-rebuilt grid and is silently dropped by the bounds check, so the most recent activity vanishes with no error surfaced. This mirrors the existing services `buildServiceTimelines`, which has the same skew dependency, but the added `Date.now()`-based grid here reintroduces it for issues. Anchoring to server time (e.g. snapping via the same `now64(3)` the query uses, or deriving `nowMs` from the returned data) would avoid the silent loss; at minimum, document the skew assumption rather than treating the drop as only the intentional partial-bucket clipping.</comment>
<file context>
@@ -166,7 +170,7 @@ export function useIssueSparklines(
const { query, params } = getIssueSparklineQuery(hours, wanted);
const response = await queryObservability(adminApp, { query, params });
- const parsed = parseIssueSparklineRows(response.result, wanted);
+ const parsed = parseIssueSparklineRows(response.result, wanted, hours, Date.now());
if (cancelled) return;
setError(null);
</file context>
| * dropdown — the URL codec and the API both carry the raw environment name. | ||
| */ | ||
| const ALL_ENVIRONMENTS_SELECT_VALUE = "all"; | ||
| const ENVIRONMENT_SELECT_VALUE_PREFIX = "env:"; |
There was a problem hiding this comment.
P3: This re-implements the namespaced select-value codec that already exists for the service filter in service-identity.ts. The environment variant is simpler (plain string rather than an object) but the sentinel/prefix convention is duplicated in the page component instead of next to that codec, risking drift. Consider colocating the environment codec with service-identity's or documenting the shared convention.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/observability/issues/page-client.tsx, line 86:
<comment>This re-implements the namespaced select-value codec that already exists for the service filter in `service-identity.ts`. The environment variant is simpler (plain string rather than an object) but the sentinel/prefix convention is duplicated in the page component instead of next to that codec, risking drift. Consider colocating the environment codec with service-identity's or documenting the shared convention.</comment>
<file context>
@@ -71,7 +76,28 @@ import { useIssueFacets, useIssueSparklines } from "./use-issue-data";
+ * dropdown — the URL codec and the API both carry the raw environment name.
+ */
const ALL_ENVIRONMENTS_SELECT_VALUE = "all";
+const ENVIRONMENT_SELECT_VALUE_PREFIX = "env:";
+
+function environmentToSelectValue(environment: string | null): string {
</file context>
Summary
Stack
Base: SDK and config contracts
Final replacement PR in this stack.
Test plan
Summary by cubic
Consolidates Observability UI around new backend contracts and shared helpers. Pages now use one
formatCount, issue links carry the selectedrange(default 24h), trace event scans are bounded to the selected trace ±15m with link caps, and source‑map uploads only reject fully injected debug‑IDs. Also fixes exception chain ordering, makes empty sparklines accessible, and removes duplicated issue link helpers.?range=;DEFAULT_ISSUE_RANGE_HOURSandISSUE_RANGE_PARAM_KEYlive inissues/issue-links.exception.valuesentry is the primary; cause list is reversed to read direct‑cause first.nextStatusForActionand count adjustment helpers.<unknown>sentinel as missing; never renders blank.preand show mapped status from frame symbolication.SPAN_LINKS_CAP.window.scrollTotyping.putPresignedArtifactand tight detection for injected debug‑ID identifiers; barehexclave-dbid-mentions no longer false‑fail.observability/formatand adopts it in Services and Performance; updates docs to clarifyenvironmentdefault.ariaLabelinstead of hiding the hairline.zincbadge tone; documents row click target in the data grid./warehouse/*paths (queries/tables moved from/analytics/*).Migration
projects/[projectId]/observability/issues/issue-links(issuesListHref,issueDetailHref, …); duplicates were removed fromobservability-links.formatCountfrom../formatinstead of page‑local copies in Services/Performance.Written for commit 9f45bba. Summary will update on new commits.