Files
supabase/apps/docs/features/docs/Troubleshooting.page.tsx
Miranda LimonczenkoandClaude Opus 5 61b2a18724 fix(docs): stop rendering empty troubleshooting error-code pills (#49344)
Closes DOCS-1281

## Problem

Two defects in the "Related error codes" list, both from the page
diverging from what `Troubleshooting.utils.ts` already does.

* **Empty pills.** `formatError` returns an empty string when an error
has neither an HTTP status code nor a code. The page renders the pill
anyway, giving a link with no text whose `href` ends in `errorCodes=`
with no value. So it is both an unnamed link and a pill filtering on
nothing.
* **Duplicate pills.** The same formatted code renders once per
underlying error object, so one entry shows seven identical "500
unexpected_failure" pills.

Measured on production, across the 59 entries that render the section:

| | Count |
| -- | -- |
| Entries with an empty pill | 23 |
| Empty pills | 33 |
| Entries with duplicate pills | 4 |
| Redundant pills | 9 |

The guard also evaluated to `0` rather than `false` for an empty array,
which React renders as a literal "0".

## Solution

* Derive the formatted codes once, drop the empties, and dedupe. An
entry whose every code formats empty no longer renders a heading and
rule with nothing under them.
* Call `formatError` once per code instead of twice per pill, and key on
the code now that codes are unique.
* Fix the same `0`-rendering guard on the keywords section.

`Troubleshooting.utils.ts` already filters on `error?.http_status_code
|| error?.code` at lines 69 and 150, and already dedupes by formatted
code at lines 72 to 79. This brings the page in line with the sidebar
and filter list rather than introducing a new pattern.

`formatError` itself is unchanged. It also produces grouping and sort
keys in `Troubleshooting.utils.ts` and `Troubleshooting.ui.tsx`, so
changing its return contract would reach well beyond this fix.

## Manual testing

Compare each page against production, which still shows both defects.

1. Open [dashboard-errors-when-managing-users on
production](https://supabase.com/docs/guides/troubleshooting/dashboard-errors-when-managing-users-N1ls4A).
It shows 8 pills: seven identical "500 unexpected_failure" and one
empty.
2. Open [the same page on the
preview](https://docs-git-docs-troubleshooting-empty-error-pills-supabase.vercel.app/docs/guides/troubleshooting/dashboard-errors-when-managing-users-N1ls4A).
One "500 unexpected_failure" pill remains.
3. Open [prisma-error-management on
production](https://supabase.com/docs/guides/troubleshooting/prisma-error-management-Cm5P_o).
It shows 6 empty pills.
4. Open [the same page on the
preview](https://docs-git-docs-troubleshooting-empty-error-pills-supabase.vercel.app/docs/guides/troubleshooting/prisma-error-management-Cm5P_o).
The section is gone, because every code on that entry formats empty.
5. Run axe on either preview page. `link-name` reports zero elements.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved troubleshooting displays by formatting and deduplicating
error values.
  * Removed empty or invalid error entries from the rendered results.
* Related error-code links now appear only when valid error codes are
available.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:44:28 -07:00

133 lines
5.6 KiB
TypeScript

import { Github } from 'lucide-react'
import Link from 'next/link'
import { cn } from 'ui'
import Breadcrumbs from '~/components/Breadcrumbs'
import { Feedback } from '~/components/Feedback'
import { SidebarSkeleton } from '~/layouts/MainSkeleton'
import { MDXRemoteBase } from './MdxBase'
import { getTroubleshootingUpdatedDates, type ITroubleshootingEntry } from './Troubleshooting.utils'
import { formatError, serializeTroubleshootingSearchParams } from './Troubleshooting.utils.shared'
export default async function TroubleshootingPage({ entry }: { entry: ITroubleshootingEntry }) {
const dateUpdated = entry.data.database_id.startsWith('pseudo-')
? new Date()
: (await getTroubleshootingUpdatedDates()).get(entry.data.database_id)
const errorCodes = [...new Set(entry.data.errors?.map(formatError).filter(Boolean) ?? [])]
return (
<SidebarSkeleton
hideSideNav
className="@container/troubleshooting-entry-layout w-full max-w-(--breakpoint-lg) mx-auto lg:py-8 lg:px-5"
>
<div className="px-5 py-8 lg:px-0 lg:py-0">
<Breadcrumbs minLength={1} forceDisplayOnMobile />
<article
id="sb-docs-troubleshooting-main-article"
data-testid="sb-docs-troubleshooting-main-article"
className="prose max-w-none mt-4"
>
<h1>{entry.data.title}</h1>
{dateUpdated && (
<p className="text-sm text-foreground-lighter">
Last edited: {dateUpdated.toLocaleDateString()}
</p>
)}
<hr className="my-7" aria-hidden />
<div className="grid gap-10 @3xl/troubleshooting-entry-layout:grid-cols-[1fr_250px]">
<div className="min-w-0">
<MDXRemoteBase source={entry.content} />
</div>
<aside aria-labelledby="heading--metadata" className="not-prose mt-5">
<h2
id="heading--metadata"
className="text-foreground-lighter uppercase text-sm tracking-wide @3xl/troubleshooting-entry-layout:sr-only"
>
Metadata
</h2>
<hr className="my-6 @3xl/troubleshooting-entry-layout:hidden" aria-hidden />
{entry.data.topics?.length > 0 && (
<>
<h3 className="text-sm text-foreground-lighter mb-3">Products</h3>
<div className="flex flex-wrap gap-0.5">
{entry.data.topics.map((topic) => (
<Link
key={topic}
href={`/guides/troubleshooting${serializeTroubleshootingSearchParams({ products: [topic] })}`}
>
<PillTag className="hover:bg-200 focus-visible:bg-foreground-muted hover:border-control focus-visible:border-control transition-colors">
{topic[0].toUpperCase() + topic.slice(1)}
</PillTag>
</Link>
))}
</div>
<hr className="my-6" aria-hidden />
</>
)}
{errorCodes.length > 0 && (
<>
<h3 className="text-sm text-foreground-lighter mb-3">Related error codes</h3>
<div className="flex flex-wrap gap-0.5">
{errorCodes.map((errorCode) => (
<Link
key={errorCode}
href={`/guides/troubleshooting${serializeTroubleshootingSearchParams({ errorCodes: [errorCode] })}`}
>
<PillTag className="hover:bg-200 focus-visible:bg-foreground-muted hover:border-control focus-visible:border-control transition-colors">
{errorCode}
</PillTag>
</Link>
))}
</div>
<hr className="my-6" aria-hidden />
</>
)}
{!!entry.data.keywords?.length && (
<>
<h3 className="text-sm text-foreground-lighter mb-3">Keywords</h3>
<div className="flex flex-wrap gap-0.5">
{entry.data.keywords.map((keyword) => (
<Link
key={keyword}
href={`/guides/troubleshooting${serializeTroubleshootingSearchParams({ tags: [keyword] })}`}
>
<PillTag className="hover:bg-200 focus-visible:bg-foreground-muted hover:border-control focus-visible:border-control transition-colors">
{keyword}
</PillTag>
</Link>
))}
</div>
<hr className="my-6" aria-hidden />
</>
)}
<Feedback className="px-0 mb-6 lg:mb-8" />
{entry.data.github_url && (
<>
<hr className="my-6" aria-hidden />
<Link
target="_blank"
href={entry.data.github_url}
className="flex items-center gap-2 text-sm text-foreground-lighter"
>
<Github size={14} />
View discussion on GitHub
</Link>
</>
)}
</aside>
</div>
</article>
</div>
</SidebarSkeleton>
)
}
function PillTag({ children, className }: { children: React.ReactNode; className?: string }) {
return (
<span
className={cn('px-2 py-px border rounded-full inline-flex items-center text-xs', className)}
>
{children}
</span>
)
}