From acbec2949da5518cabaaf9d143ef43b4499f345f Mon Sep 17 00:00:00 2001 From: Taha Le Bras <97157504+TahaLeBras@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:20:56 +0200 Subject: [PATCH] feat(docs): route docs page feedback comments to Postgres (#47041) ## Details of change Re-lands DATAENG-1468 (docs page feedback to Postgres) with an **insert-only** design that avoids the cross-project auth issue that caused the prior revert. - New insert-only `feedback_comments` table: anon `insert` policy only (no select/update/delete). Columns: `page`, `vote`, `title`, `comment`, `user_id`, `metadata`. - The docs widget writes the free-text comment to `feedback_comments` via the **anon key**. The votes `feedback` table is untouched (one row per vote). - No user token is sent to the content project anymore (that was the cause of the previous failure): the feedback client uses the anon key only. - The commenter's account user id (gotrue UUID) is read client-side from the session and stored as a plain `user_id` column for attribution (comments are logged-in-only). Org/project association is derived downstream in BigQuery via that id; docs pages aren't project-scoped, so there's no project_ref/org to capture here. - Removed the previous update-by-id approach, the per-user RLS policies, and the obsolete unit test. ## Why the previous version was reverted It authenticated feedback writes with the supabase.com account session token, but the requests target the docs content project (`xguihxuzqibwxjnimxev`), which cannot verify that token. Logged-in users got `PGRST301 / JWSInvalidSignature`. This version removes the user token entirely, so writes succeed for everyone. ## Verification Insert-only RLS means a row can be written but not read/updated/deleted by `anon`. Comments retrievable with `where comment is not null` is not needed (separate table); just query `feedback_comments`. ## Summary by CodeRabbit * **New Features** * The feedback form now captures a vote rating along with an optional title and detailed comments, saving richer context for review. * **Refactor** * Feedback submission has been streamlined to write directly to the database for both vote and comment submissions. * **Maintenance** * Updated the feedback data typings to support the new title, comment, user, page, and vote fields via the new feedback comments storage. --- apps/docs/components/Feedback/Feedback.tsx | 40 +++++++-------- .../components/Feedback/Feedback.utils.ts | 42 +--------------- apps/docs/lib/fetch/feedback.ts | 50 ------------------- packages/common/database-types.ts | 33 ++++++++++++ ...0260617140245_create_feedback_comments.sql | 18 +++++++ 5 files changed, 71 insertions(+), 112 deletions(-) delete mode 100644 apps/docs/lib/fetch/feedback.ts create mode 100644 supabase/migrations/20260617140245_create_feedback_comments.sql diff --git a/apps/docs/components/Feedback/Feedback.tsx b/apps/docs/components/Feedback/Feedback.tsx index ba4afd00ace..dce8d456dda 100644 --- a/apps/docs/components/Feedback/Feedback.tsx +++ b/apps/docs/components/Feedback/Feedback.tsx @@ -2,9 +2,8 @@ import { createClient } from '@supabase/supabase-js' import { IS_PLATFORM } from '~/lib/constants' -import { useSendFeedbackMutation } from '~/lib/fetch/feedback' import { useSendTelemetryEvent } from '~/lib/telemetry' -import { useConstant, useIsLoggedIn, type Database } from 'common' +import { gotrueClient, useConstant, useIsLoggedIn, type Database } from 'common' import { Check, MessageSquareQuote, X } from 'lucide-react' import { usePathname } from 'next/navigation' import { @@ -17,7 +16,7 @@ import { } from 'react' import { Button, cn } from 'ui' -import { getLinearTeam, getSanitizedTabParams } from './Feedback.utils' +import { getSanitizedTabParams } from './Feedback.utils' import { FeedbackModal, type FeedbackFields } from './FeedbackModal' const FeedbackButton = forwardRef< @@ -78,7 +77,6 @@ function Feedback({ className }: { className?: string }) { const pathname = usePathname() ?? '' const sendTelemetryEvent = useSendTelemetryEvent() - const { mutate: sendFeedbackComment } = useSendFeedbackMutation() const supabase = useConstant(() => IS_PLATFORM ? createClient( @@ -96,14 +94,9 @@ function Feedback({ className }: { className?: string }) { async function sendFeedbackVote(response: Response) { if (!supabase) return - - const { error } = await supabase.from('feedback').insert({ - vote: response, - page: pathname, - metadata: { - query: getSanitizedTabParams(), - }, - }) + const { error } = await supabase + .from('feedback') + .insert({ vote: response, page: pathname, metadata: { query: getSanitizedTabParams() } }) if (error) console.error(error) } @@ -128,15 +121,20 @@ function Feedback({ className }: { className?: string }) { }, 100) } - async function handleSubmit({ page, comment, title }: FeedbackFields) { - sendFeedbackComment({ - message: comment, - pathname: page, - title, - // @ts-expect-error -- can't click this button without having a state.response - isHelpful: state.response === 'yes', - team: getLinearTeam(pathname), - }) + async function handleSubmit({ comment, title }: FeedbackFields) { + if (supabase) { + const userId = (await gotrueClient.getSession()).data.session?.user?.id ?? null + const { error } = await supabase.from('feedback_comments').insert({ + page: pathname, + // @ts-expect-error -- the comment modal only opens after a vote, so state.response is set + vote: state.response, + title, + comment, + user_id: userId, + metadata: { query: getSanitizedTabParams() }, + }) + if (error) console.error(error) + } setModalOpen(false) refocusButton() } diff --git a/apps/docs/components/Feedback/Feedback.utils.ts b/apps/docs/components/Feedback/Feedback.utils.ts index c93ebb55715..e1473f4fb67 100644 --- a/apps/docs/components/Feedback/Feedback.utils.ts +++ b/apps/docs/components/Feedback/Feedback.utils.ts @@ -1,45 +1,5 @@ import { pick } from 'lodash-es' -/** - * Gets the Notion team to send feedback to based on the pathname. - */ -const getLinearTeam = (pathname: string) => { - const DEFAULT_TEAM = 'Docs' - - // Pathname has format `/guides/(team)/**` - const pathParts = pathname.split('/') - - if (pathParts[1] !== 'guides' || !pathParts[2]) return DEFAULT_TEAM - - switch (pathParts[2]) { - case 'database': - return 'Postgres' - case 'auth': - return 'Auth' - case 'storage': - return 'Storage' - case 'functions': - return 'Functions' - case 'realtime': - return 'Realtime' - case 'ai': - return 'AI' - case 'local-development': - case 'self-hosting': - case 'deployment': - return 'Dev Workflows' - case 'integrations': - return 'API' - case 'security': - return 'Security' - case 'platform': - case 'monitoring-troubleshooting': - return 'Infra' - default: - return DEFAULT_TEAM - } -} - /** * Gets the tab selection state from the URL search params. * @@ -53,4 +13,4 @@ const getSanitizedTabParams = () => { return pick(Object.fromEntries(searchParams.entries()), queryGroups) } -export { getLinearTeam, getSanitizedTabParams } +export { getSanitizedTabParams } diff --git a/apps/docs/lib/fetch/feedback.ts b/apps/docs/lib/fetch/feedback.ts deleted file mode 100644 index 8bafd80ff59..00000000000 --- a/apps/docs/lib/fetch/feedback.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { type UseMutationOptions, useMutation } from '@tanstack/react-query' - -import { type ResponseError } from '~/types/fetch' -import { post } from './fetchWrappers' - -type SendFeedbackVariables = { - title: string - message: string - isHelpful: boolean - pathname?: string - team?: string -} - -export async function sendFeedback({ - message, - pathname, - title, - isHelpful, - team, -}: SendFeedbackVariables) { - const { data, error } = await post('/platform/feedback/docs', { - body: { - page: pathname ?? '', - isHelpful, - title, - feedback: message, - team, - }, - }) - if (error) throw Error(`Couldn't send feedback`, { cause: error }) - return data -} - -type SendFeedbackData = Awaited> - -export const useSendFeedbackMutation = ( - options: Omit< - UseMutationOptions, - 'mutationFn' - > = {} -) => { - return useMutation({ - ...options, - mutationFn: (vars) => sendFeedback(vars), - onError: (error, vars, ctx) => { - console.error(error) - options.onError?.(error, vars, ctx) - }, - }) -} diff --git a/packages/common/database-types.ts b/packages/common/database-types.ts index 1fe0d208dc7..67e0e217f69 100644 --- a/packages/common/database-types.ts +++ b/packages/common/database-types.ts @@ -151,6 +151,39 @@ export type Database = { } Relationships: [] } + feedback_comments: { + Row: { + comment: string + created_at: string + id: number + metadata: Json | null + page: string + title: string | null + user_id: string | null + vote: Database['public']['Enums']['feedback_vote'] | null + } + Insert: { + comment: string + created_at?: string + id?: never + metadata?: Json | null + page: string + title?: string | null + user_id?: string | null + vote?: Database['public']['Enums']['feedback_vote'] | null + } + Update: { + comment?: string + created_at?: string + id?: never + metadata?: Json | null + page?: string + title?: string | null + user_id?: string | null + vote?: Database['public']['Enums']['feedback_vote'] | null + } + Relationships: [] + } last_changed: { Row: { checksum: string diff --git a/supabase/migrations/20260617140245_create_feedback_comments.sql b/supabase/migrations/20260617140245_create_feedback_comments.sql new file mode 100644 index 00000000000..f9688eb2790 --- /dev/null +++ b/supabase/migrations/20260617140245_create_feedback_comments.sql @@ -0,0 +1,18 @@ +create table feedback_comments ( + id bigint primary key generated always as identity, + created_at timestamptz not null default now(), + page text not null, + vote feedback_vote, + title text, + comment text not null, + user_id uuid, + metadata jsonb +); + +alter table feedback_comments enable row level security; + +create policy "Anyone can insert feedback comments" +on feedback_comments +as permissive for insert +to public +with check (true);