From 2e3332ec883c6dda533952c67e147dfbf2fe9f93 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 1 Sep 2026 13:01:30 +0200 Subject: [PATCH] fix(web): stop the self-hosted ingestion UI throwing on a Clerk hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `useGuidedFramework` called Clerk's `useAuth()` unconditionally. `main.tsx` mounts `ClerkProvider` only when `isClerkAuthEnabled`, and the hook throws without one, so self-hosted `/settings` — which opens on the ingestion tab — and the dashboard setup checklist both failed to render instead of showing the ingestion configuration. The variant is now chosen at module scope off the build-time constant, the same shape `useOrganizationFeatureFlags` uses and for the reason documented there: an early return inside the hook runs after `useAuth()` has already thrown. The self-hosted build keeps its selection under the default key, having one org. --- .../components/ingest/guided-setup.test.tsx | 26 ++++++++++++- .../src/components/ingest/guided-setup.tsx | 39 +++++++++++++++---- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/ingest/guided-setup.test.tsx b/apps/web/src/components/ingest/guided-setup.test.tsx index 4955d6eff..99bf7e772 100644 --- a/apps/web/src/components/ingest/guided-setup.test.tsx +++ b/apps/web/src/components/ingest/guided-setup.test.tsx @@ -4,7 +4,8 @@ import { cleanup, fireEvent, render, screen } from "@testing-library/react" import { afterEach, describe, expect, it } from "vitest" import { REPLAY_BLOCK_CLASS } from "@/components/common/replay-privacy" -import { ConnectInstructions } from "./guided-setup" +import { isClerkAuthEnabled } from "@/lib/services/common/auth-mode" +import { ConnectInstructions, useGuidedFramework } from "./guided-setup" const API_KEY = "mpl_ingest_supersecretkey123" @@ -37,3 +38,26 @@ describe("ConnectInstructions", () => { expect(leaked).toEqual([]) }) }) + +function FrameworkProbe() { + const { framework } = useGuidedFramework() + return {framework} +} + +/** + * Rendered with no `ClerkProvider`, which is what a self-hosted build is: + * `main.tsx` mounts one only when `isClerkAuthEnabled`. A Clerk hook called + * unconditionally in here threw, and self-hosted `/settings` opens on the + * ingestion tab that renders it. + */ +describe("useGuidedFramework", () => { + afterEach(cleanup) + + it("renders self-hosted, outside a ClerkProvider", () => { + expect(isClerkAuthEnabled).toBe(false) + + render() + + expect(screen.getByTestId("framework").textContent).toBe("nodejs") + }) +}) diff --git a/apps/web/src/components/ingest/guided-setup.tsx b/apps/web/src/components/ingest/guided-setup.tsx index 5491be383..c4e9c4f0a 100644 --- a/apps/web/src/components/ingest/guided-setup.tsx +++ b/apps/web/src/components/ingest/guided-setup.tsx @@ -15,6 +15,7 @@ import { } from "@/components/quick-start/framework-icons" import { sdkSnippets, type FrameworkId } from "@/components/quick-start/sdk-snippets" import { ingestUrl } from "@/lib/services/common/ingest-url" +import { isClerkAuthEnabled } from "@/lib/services/common/auth-mode" import { useQuickStart } from "@/hooks/use-quick-start" import type { RoleOption } from "@/atoms/quick-start-atoms" import { CopyableField } from "@maple/ui/components/ui/copyable-field" @@ -42,19 +43,43 @@ interface GuidedSetupProps { showCredentials?: boolean } -/** - * Per-org framework selection for the guided ingestion flow, defaulting from the - * quick-start qualify answers. Shared by `GuidedSetup` and the ingestion - * settings page (which composes the picker into its own section header). - */ -export function useGuidedFramework() { - const { orgId } = useAuth() +interface GuidedFramework { + readonly framework: FrameworkId + readonly setFramework: (framework: FrameworkId) => void +} + +const useGuidedFrameworkFor = (orgId: string | null | undefined): GuidedFramework => { const { selectedFramework, setSelectedFramework, qualifyAnswers } = useQuickStart(orgId) const roleDefault = qualifyAnswers.role ? ROLE_DEFAULT_FRAMEWORK[qualifyAnswers.role] : "nodejs" return { framework: selectedFramework ?? roleDefault, setFramework: setSelectedFramework } } +function useClerkGuidedFramework(): GuidedFramework { + const { orgId } = useAuth() + return useGuidedFrameworkFor(orgId) +} + +/** Self-hosted: one org, so the selection lives under the default key. */ +function useSelfHostedGuidedFramework(): GuidedFramework { + return useGuidedFrameworkFor(null) +} + +/** + * Per-org framework selection for the guided ingestion flow, defaulting from the + * quick-start qualify answers. Shared by `GuidedSetup` and the ingestion + * settings page (which composes the picker into its own section header). + * + * The variant is chosen at module scope off a build-time constant, the same + * shape as `useOrganizationFeatureFlags`: `main.tsx` mounts `ClerkProvider` only + * when `isClerkAuthEnabled`, and `useAuth()` throws without one — an early + * return inside the hook would come after it had already run. Self-hosted + * `/settings` opens on the ingestion tab, so this threw before rendering it. + */ +export const useGuidedFramework: () => GuidedFramework = isClerkAuthEnabled + ? useClerkGuidedFramework + : useSelfHostedGuidedFramework + /** * Framework picker + Install / Instrument / Claude Code tabs. The shared body of * the guided ingestion flow, used by the dashboard setup checklist and the