diff --git a/apps/docs/app/guides/ai-tools/ai-prompts/[slug]/page.tsx b/apps/docs/app/guides/ai-tools/ai-prompts/[slug]/page.tsx index 7eaf8f3ceae..df39f6ee8eb 100644 --- a/apps/docs/app/guides/ai-tools/ai-prompts/[slug]/page.tsx +++ b/apps/docs/app/guides/ai-tools/ai-prompts/[slug]/page.tsx @@ -6,8 +6,8 @@ import { wrapInMarkdownCodeBlock, } from '~/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { source } from 'common-tags' -import { notFound } from 'next/navigation' export const dynamicParams = false @@ -18,7 +18,7 @@ export default async function AiPromptsPage(props: { params: Promise<{ slug: str const prompt = await getAiPrompt(slug) if (!prompt) { - notFound() + notFoundWithPathname(`/guides/ai-tools/ai-prompts/${slug}`) } let { heading, content } = prompt diff --git a/apps/docs/app/guides/ai/python/[slug]/page.tsx b/apps/docs/app/guides/ai/python/[slug]/page.tsx index e31bc6b1595..e64133a1c39 100644 --- a/apps/docs/app/guides/ai/python/[slug]/page.tsx +++ b/apps/docs/app/guides/ai/python/[slug]/page.tsx @@ -1,14 +1,13 @@ -import { notFound } from 'next/navigation' import { relative } from 'path' -import rehypeSlug from 'rehype-slug' - import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils' -import { getGitHubFileContents } from '~/lib/octokit' -import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' +import { notFoundWithPathname } from '~/features/docs/notFound.utils' +import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' +import { getGitHubFileContents } from '~/lib/octokit' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' +import rehypeSlug from 'rehype-slug' export const dynamicParams = false @@ -76,7 +75,7 @@ const getContent = async ({ slug }: Params) => { const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug) if (!page) { - notFound() + notFoundWithPathname(`/guides/ai/python/${slug}`) } const { remoteFile, meta } = page diff --git a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx index ebafbd690b7..f85355e895a 100644 --- a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx @@ -5,6 +5,7 @@ import { genGuidesStaticParams, removeRedundantH1, } from '~/features/docs/GuidesMdx.utils' +import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { newEditLink } from '~/features/helpers.edit-link' import { Guide, GuideArticle, GuideFooter, GuideHeader, GuideMdxContent } from '~/features/ui/guide' // End of third-party imports @@ -20,7 +21,6 @@ import type { SerializeOptions } from '~/types/next-mdx-remote-serialize' import { isFeatureEnabled } from 'common' import matter from 'gray-matter' import Link from 'next/link' -import { notFound } from 'next/navigation' import rehypeSlug from 'rehype-slug' import emoji from 'remark-emoji' import { Button } from 'ui' @@ -334,11 +334,14 @@ interface Params { } const WrappersDocs = async (props: { params: Promise }) => { + const params = await props.params + if (!isFeatureEnabled('docs:fdw')) { - notFound() + notFoundWithPathname( + `/guides/database/extensions/wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}` + ) } - const params = await props.params const { isExternal, meta, assetsBaseUrl, ...data } = await getContent(params) // Create a combined URL transformer that handles both regular URLs and asset URLs diff --git a/apps/docs/app/guides/deployment/ci/[slug]/page.tsx b/apps/docs/app/guides/deployment/ci/[slug]/page.tsx index 4053787f65e..2a2cf31e917 100644 --- a/apps/docs/app/guides/deployment/ci/[slug]/page.tsx +++ b/apps/docs/app/guides/deployment/ci/[slug]/page.tsx @@ -1,15 +1,14 @@ -import { notFound } from 'next/navigation' import { relative } from 'node:path' -import rehypeSlug from 'rehype-slug' - import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils' -import { getGitHubFileContents } from '~/lib/octokit' -import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' +import { notFoundWithPathname } from '~/features/docs/notFound.utils' +import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' +import { getGitHubFileContents } from '~/lib/octokit' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' +import rehypeSlug from 'rehype-slug' export const dynamicParams = false @@ -75,7 +74,7 @@ const getContent = async ({ slug }: Params) => { const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug) if (!page) { - notFound() + notFoundWithPathname(`/guides/deployment/ci/${slug}`) } const { remoteFile, meta } = page diff --git a/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx b/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx index 153c814eff3..2fafd8ea780 100644 --- a/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx @@ -1,5 +1,6 @@ import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils' +import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { getEmptyArray } from '~/features/helpers.fn' import { IS_DEV } from '~/lib/constants' import { isValidGuideFrontmatter } from '~/lib/docs' @@ -10,7 +11,6 @@ import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' import { getGitHubFileContents } from '~/lib/octokit' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' import matter from 'gray-matter' -import { notFound } from 'next/navigation' import rehypeSlug from 'rehype-slug' import { @@ -106,7 +106,7 @@ const getContent = async ({ slug }: Params) => { const page = pageMap.find((page) => page.slug === requestedSlug) if (!page) { - notFound() + notFoundWithPathname(`/guides/deployment/terraform${slug?.length ? `/${slug.join('/')}` : ''}`) } const { meta, remoteFile, useRoot } = page diff --git a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx index eb0c01b48f5..dda40f712a9 100644 --- a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx @@ -1,6 +1,7 @@ import { isAbsolute, relative } from 'path' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' +import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { getEmptyArray } from '~/features/helpers.fn' import { IS_DEV } from '~/lib/constants' import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform' @@ -9,7 +10,6 @@ import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' import { getGitHubFileContents } from '~/lib/octokit' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' -import { notFound } from 'next/navigation' import rehypeSlug from 'rehype-slug' // We fetch these docs at build time from an external repo @@ -128,7 +128,7 @@ const getContent = async ({ slug }: Params) => { const page = pageMap.find((page) => page.slug === slug?.at(0)) if (!page) { - notFound() + notFoundWithPathname(`/guides/graphql${slug?.length ? `/${slug.join('/')}` : ''}`) } const { remoteFile, meta } = page diff --git a/apps/docs/app/guides/troubleshooting/[slug]/page.tsx b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx index 9f15cac7d53..f0868e8c167 100644 --- a/apps/docs/app/guides/troubleshooting/[slug]/page.tsx +++ b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx @@ -1,5 +1,4 @@ -import { notFound } from 'next/navigation' - +import { notFoundWithPathname } from '~/features/docs/notFound.utils' import TroubleshootingPage from '~/features/docs/Troubleshooting.page' import { getAllTroubleshootingEntries, getArticleSlug } from '~/features/docs/Troubleshooting.utils' import { PROD_URL } from '~/lib/constants' @@ -20,7 +19,7 @@ export default async function TroubleshootingEntryPage(props: { const entry = allTroubleshootingEntries.find((entry) => getArticleSlug(entry) === slug) if (!entry) { - notFound() + notFoundWithPathname(`/guides/troubleshooting/${slug}`) } return diff --git a/apps/docs/app/reference/[...slug]/page.tsx b/apps/docs/app/reference/[...slug]/page.tsx index 0ed5497704c..d224a81cb7e 100644 --- a/apps/docs/app/reference/[...slug]/page.tsx +++ b/apps/docs/app/reference/[...slug]/page.tsx @@ -1,6 +1,5 @@ -import { notFound } from 'next/navigation' - import { REFERENCES } from '~/content/navigation.references' +import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { ApiReferencePage } from '~/features/docs/Reference.apiPage' import { CliReferencePage } from '~/features/docs/Reference.cliPage' import { ClientSdkReferencePage } from '~/features/docs/Reference.sdkPage' @@ -18,9 +17,10 @@ export default async function ReferencePage(props: { params: Promise<{ slug: Arr const params = await props.params const { slug } = params + const referencePath = `/reference/${slug.join('/')}` if (!Object.keys(REFERENCES).includes(slug[0].replaceAll('-', '_'))) { - notFound() + notFoundWithPathname(referencePath) } const parsedPath = parseReferencePath(slug) @@ -34,7 +34,7 @@ export default async function ReferencePage(props: { params: Promise<{ slug: Arr const sdkData = REFERENCES[sdkId] if (sdkData.enabled === false) { - notFound() + notFoundWithPathname(referencePath) } const latestVersion = sdkData.versions[0] @@ -52,7 +52,7 @@ export default async function ReferencePage(props: { params: Promise<{ slug: Arr ) } else { - notFound() + notFoundWithPathname(referencePath) } } diff --git a/apps/docs/features/docs/GuidesMdx.utils.tsx b/apps/docs/features/docs/GuidesMdx.utils.tsx index 1cd8e04a9eb..e719d2caa9f 100644 --- a/apps/docs/features/docs/GuidesMdx.utils.tsx +++ b/apps/docs/features/docs/GuidesMdx.utils.tsx @@ -14,10 +14,10 @@ import { fromMarkdown } from 'mdast-util-from-markdown' import { gfmFromMarkdown } from 'mdast-util-gfm' import { gfm } from 'micromark-extension-gfm' import { type Metadata, type ResolvingMetadata } from 'next' -import { notFound } from 'next/navigation' import { newEditLink } from './GuidesMdx.template' import { checkGuidePageEnabled } from './NavigationPageStatus.utils' +import { notFoundWithPathname } from './notFound.utils' const { metadataTitle } = getCustomContent(['metadata:title']) @@ -56,7 +56,7 @@ const getGuidesMarkdownInternal = async (slug: string[]) => { !fullPath.startsWith(GUIDES_DIRECTORY) || !PUBLISHED_SECTIONS.some((section) => relPath.startsWith(section)) ) { - notFound() + notFoundWithPathname(guidesPath) } /** @@ -65,7 +65,7 @@ const getGuidesMarkdownInternal = async (slug: string[]) => { */ if (!checkGuidePageEnabled(guidesPath)) { console.log('Page is disabled: %s', guidesPath) - notFound() + notFoundWithPathname(guidesPath) } try { @@ -100,7 +100,7 @@ const getGuidesMarkdownInternal = async (slug: string[]) => { ) Sentry.captureException(error) } - notFound() + notFoundWithPathname(guidesPath) } } diff --git a/apps/docs/features/docs/notFound.utils.ts b/apps/docs/features/docs/notFound.utils.ts new file mode 100644 index 00000000000..cd090a1e546 --- /dev/null +++ b/apps/docs/features/docs/notFound.utils.ts @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/nextjs' +import { notFound } from 'next/navigation' + +/** + * Triggers a Next.js 404 while recording the requested path as a Sentry tag. + * + * `notFound()` itself does not create a Sentry event, but tagging the active + * scope means any error captured during the same request (and any event the + * Sentry SDK records for the render) carries the exact path under + * `404.pathname`. That makes 404s filterable and groupable in Sentry/Discover + * instead of only being inferable from the route's transaction name. + * + * @param pathname - The user-facing path that was not found, e.g. + * `/guides/functions/runtimes/node-22`. + */ +export function notFoundWithPathname(pathname: string): never { + Sentry.setTag('404.pathname', pathname) + notFound() +} diff --git a/apps/docs/resources/guide/guideModelLoader.ts b/apps/docs/resources/guide/guideModelLoader.ts index 551bd98abfe..1fa7858336a 100644 --- a/apps/docs/resources/guide/guideModelLoader.ts +++ b/apps/docs/resources/guide/guideModelLoader.ts @@ -1,13 +1,13 @@ -import matter from 'gray-matter' import { promises as fs } from 'node:fs' import { join, relative, resolve } from 'node:path' - import { extractMessageFromAnyError, FileNotFoundError, MultiError } from '~/app/api/utils' import { preprocessMdxWithDefaults } from '~/features/directives/utils' import { checkGuidePageEnabled } from '~/features/docs/NavigationPageStatus.utils' import { Both, Result } from '~/features/helpers.fn' import { GUIDES_DIRECTORY } from '~/lib/docs' import { processMdx } from '~/scripts/helpers.mdx' +import matter from 'gray-matter' + import { GuideModel } from './guideModel' /** @@ -140,7 +140,7 @@ export class GuideModelLoader { }, (error) => { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { - throw new FileNotFoundError('', error) + throw new FileNotFoundError(join(GUIDES_DIRECTORY, relPath), error) } throw new Error( `Failed to load guide from ${relPath}: ${extractMessageFromAnyError(error)}`,