Files
supabase/apps/docs/features/docs/Troubleshooting.utils.ts
T
Miranda Limonczenko b5cae478bc fix(docs) Add smoke test for local development without credentials (#48218)
Closes DOCS-1210
Closes DOCS-1209

#48226 needs to merge first for CI failure

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Test coverage and several small bug fixes discovered during
implementation.

## What is the current behavior?

Nothing verified that `pnpm run dev:docs` keeps working without private
credentials.
We value this command working, especially for community contributors.

However, this issue can go undetected by employees at Supabase since
many of us have credentials in place. We do not want this to go a week
before finding and fixing like in the previous instance.

## What is the new behavior?

- **New Playwright test suite**:
`e2e/docs/local-smoke/no-credentials.spec.ts` boots the docs dev server
with zero GitHub App/Supabase secrets and checks 5 routes covering each
known failure point.
- **CI**: a new `local-dev-smoke` job in `docs-tests.yml` runs this
suite with no credentials configured.


## Additional bugs resolved

Setting up this test exposed other issues that are fixed in this PR:

- **Troubleshooting.utils.ts crash** — Unguarded Supabase call pattern,
crashing every troubleshooting article. Added the same guard as previous
fixes.
- **Missing manifest.json** — middleware.ts statically imports
public/markdown/manifest.json, which is gitignored and only generated by
a build step that's skipped in local dev. On a fresh checkout it doesn't
exist, so middleware fails to compile and takes down every page. Fixed
by committing a placeholder [] (real builds still regenerate the full
file).
- **Phantom @code-hike/mdx import** — apps/docs/app/layout.tsx imported
@code-hike/mdx/styles.css, but only apps/www actually declares that
dependency. Worked by accident whenever both apps were installed
together; broke in CI's docs-only install. Turned out to be dead code
(nothing in docs actually uses code-hike), so fixed by deleting the
unused imports rather than adding the dependency.


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

## Summary by CodeRabbit

* **New Features**
* Added a credential-free “local smoke” end-to-end test suite for key
documentation routes.

* **Bug Fixes**
* Improved troubleshooting behavior when required external service
credentials are missing.
* Updated federated “wrappers” documentation pages to gracefully show a
fallback message when external content can’t be fetched.

* **Tests**
* Added a dedicated local-smoke Playwright runner and enhanced CI
path-based triggering and reporting (failure-focused artifacts).

* **Chores**
* Refined docs workflow path filters and adjusted docs markdown
manifest/ignore rules for generated content.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-24 10:29:12 -07:00

171 lines
5.2 KiB
TypeScript

import { cache_fullProcess_withDevCacheBust } from '~/features/helpers.fs'
import { supabase } from '~/lib/supabase'
import { cache } from 'react'
import { z } from 'zod'
import {
getAllTroubleshootingEntriesInternal,
getArticleSlug as getArticleSlugInternal,
TROUBLESHOOTING_DIRECTORY,
TroubleshootingSchema,
} from './Troubleshooting.utils.common.mjs'
import { formatError } from './Troubleshooting.utils.shared'
// We don't have strictNullChecks on because we still have too many violations
// in the code base, so required types aren't typed properly by Zod.
export type ITroubleshootingMetadata = z.infer<typeof TroubleshootingSchema> &
Pick<Required<z.infer<typeof TroubleshootingSchema>>, 'title' | 'topics' | 'database_id'>
export interface ITroubleshootingEntry {
filePath: string
content: string
contentWithoutJsx: string
data: ITroubleshootingMetadata
}
export const getArticleSlug = getArticleSlugInternal
async function getAllTroubleshootingEntriesTyped() {
const result: ITroubleshootingEntry[] =
(await getAllTroubleshootingEntriesInternal()) as ITroubleshootingEntry[]
return result ?? []
}
export const getAllTroubleshootingEntries = cache_fullProcess_withDevCacheBust(
getAllTroubleshootingEntriesTyped,
TROUBLESHOOTING_DIRECTORY,
() => JSON.stringify([])
)
export async function getAllTroubleshootingKeywords() {
const entries = await getAllTroubleshootingEntries()
const keywords = new Set<string>()
for (const entry of entries) {
for (const topic of entry.data.topics) {
keywords.add(topic)
}
for (const keyword of entry.data.keywords ?? []) {
keywords.add(keyword)
}
}
return Array.from(keywords).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
}
export async function getAllTroubleshootingProducts() {
const entries = await getAllTroubleshootingEntries()
const products = new Set<string>()
for (const entry of entries) {
for (const topic of entry.data.topics) {
products.add(topic)
}
}
return Array.from(products).sort((a, b) => a.localeCompare(b))
}
export async function getAllTroubleshootingErrors() {
const entries = await getAllTroubleshootingEntries()
const allErrors = new Set(
entries
.flatMap((entry) => entry.data.errors ?? [])
.filter((error) => error?.http_status_code || error?.code)
)
const seen = new Set<string>()
for (const error of allErrors) {
const key = formatError(error)
if (seen.has(key)) {
allErrors.delete(error)
}
seen.add(key)
}
function sortErrors(
a: NonNullable<ITroubleshootingMetadata['errors']>[number],
b: NonNullable<ITroubleshootingMetadata['errors']>[number]
) {
return formatError(a).localeCompare(formatError(b))
}
return Array.from(allErrors).sort(sortErrors)
}
async function getTroubleshootingUpdatedDatesInternal() {
const databaseIds = (await getAllTroubleshootingEntries())
.map((entry) => entry.data.database_id)
.filter((id) => !id.startsWith('pseudo-'))
if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
return new Map<string, Date>()
}
const { data, error } = await supabase()
.from('troubleshooting_entries')
.select('id, date_updated')
.in('id', databaseIds)
if (error) {
console.error(error)
}
return (data ?? []).reduce((acc, entry) => {
acc.set(entry.id, new Date(entry.date_updated))
return acc
}, new Map<string, Date>())
}
export const getTroubleshootingUpdatedDates = cache(getTroubleshootingUpdatedDatesInternal)
async function getTroubleshootingEntriesByTopicImpl(
topic: ITroubleshootingMetadata['topics'][number]
) {
const allEntries = await getAllTroubleshootingEntries()
return allEntries.filter((entry) => entry.data.topics.includes(topic))
}
export const getTroubleshootingEntriesByTopic = cache_fullProcess_withDevCacheBust(
getTroubleshootingEntriesByTopicImpl,
TROUBLESHOOTING_DIRECTORY,
() => JSON.stringify([])
)
export async function getTroubleshootingKeywordsByTopic(
topic: ITroubleshootingMetadata['topics'][number]
) {
const entries = await getTroubleshootingEntriesByTopic(topic)
const keywords = new Set<string>()
for (const entry of entries) {
for (const entryTopic of entry.data.topics) {
keywords.add(entryTopic)
}
for (const keyword of entry.data.keywords ?? []) {
keywords.add(keyword)
}
}
return Array.from(keywords).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
}
export async function getTroubleshootingErrorsByTopic(
topic: ITroubleshootingMetadata['topics'][number]
) {
const entries = await getTroubleshootingEntriesByTopic(topic)
const allErrors = new Set(
entries
.flatMap((entry) => entry.data.errors ?? [])
.filter((error) => error?.http_status_code || error?.code)
)
const seen = new Set<string>()
for (const error of allErrors) {
const key = formatError(error)
if (seen.has(key)) {
allErrors.delete(error)
}
seen.add(key)
}
function sortErrors(
a: NonNullable<ITroubleshootingMetadata['errors']>[number],
b: NonNullable<ITroubleshootingMetadata['errors']>[number]
) {
return formatError(a).localeCompare(formatError(b))
}
return Array.from(allErrors).sort(sortErrors)
}