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`.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Taha Le Bras authored and GitHub committed 2026-06-18 11:20:56 +02:00
1 parent 569a733f27
commit acbec2949d
5 files changed
+71 -112

No files matched your search

+19 -21
View File
@@ -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<Database>(
@@ -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()
}
@@ -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 }
-50
View File
@@ -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<ReturnType<typeof sendFeedback>>
export const useSendFeedbackMutation = (
options: Omit<
UseMutationOptions<SendFeedbackData, ResponseError, SendFeedbackVariables>,
'mutationFn'
> = {}
) => {
return useMutation<SendFeedbackData, ResponseError, SendFeedbackVariables>({
...options,
mutationFn: (vars) => sendFeedback(vars),
onError: (error, vars, ctx) => {
console.error(error)
options.onError?.(error, vars, ctx)
},
})
}
+33
View File
@@ -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
@@ -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);