mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
569a733f27
commit
acbec2949d
5 files changed
+71
-112
No files matched your search
@@ -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 }
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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);
|
||||
Reference in new issue
Block a user