Files
supabase/apps/studio/data/ai/rate-message-mutation.ts
Matt Rossman 20d1ac0f83 feat(assistant): send user feedback to Braintrust traces (#43021)
- Expose Braintrust span ID to client via `x-braintrust-span-id`
response header, captured in chat transport
- On feedback submission, call `logFeedback()` with `scores["User
Rating"]` (1/0), `comment`, and on the root span assign
`metadata.feedbackCategory`
- Silently skipped when tracing is disabled (HIPAA, missing env vars)
- Log `requestedModel` in trace metadata so we can see what model the
user selected vs what was actually used after throttling

Example traces:

- [Thumbs
up](https://www.braintrust.dev/app/supabase.io/p/Assistant/trace?object_type=project_logs&object_id=5a8d02e5-b3b6-40cc-ba76-ecee286478f4&r=0bb71680-784c-45c1-a234-cba0242562d6&s=0bb71680-784c-45c1-a234-cba0242562d6)
- [Thumbs down + negative
feedback](https://www.braintrust.dev/app/supabase.io/p/Assistant/trace?object_type=project_logs&object_id=5a8d02e5-b3b6-40cc-ba76-ecee286478f4&r=d5a78084-6c9a-4230-8615-1e864bb9bac7&s=d5a78084-6c9a-4230-8615-1e864bb9bac7)

<img width="645" height="173" alt="CleanShot 2026-02-19 at 13 30 25@2x"
src="https://github.com/user-attachments/assets/6c463e83-27c6-4afb-a8d0-a329ed61270a"
/>

Closes AI-442
2026-02-19 15:00:07 -05:00

75 lines
1.9 KiB
TypeScript

import { UIMessage } from '@ai-sdk/react'
import { useMutation } from '@tanstack/react-query'
import type { RateMessageResponse } from 'components/ui/AIAssistantPanel/Message.utils'
import { constructHeaders, fetchHandler } from 'data/fetchers'
import { BASE_PATH } from 'lib/constants'
import { ResponseError, UseCustomMutationOptions } from 'types'
export type RateMessageVariables = {
rating: 'positive' | 'negative'
messages: UIMessage[]
messageId: string
projectRef: string
orgSlug?: string
reason?: string
spanId?: string
}
export async function rateMessage({
rating,
messages,
messageId,
projectRef,
orgSlug,
reason,
spanId,
}: RateMessageVariables) {
const url = `${BASE_PATH}/api/ai/feedback/rate`
const headers = await constructHeaders({ 'Content-Type': 'application/json' })
const response = await fetchHandler(url, {
headers,
method: 'POST',
body: JSON.stringify({ rating, messages, messageId, projectRef, orgSlug, reason, spanId }),
})
let body: any
try {
body = await response.json()
} catch {}
if (!response.ok) {
throw new ResponseError(body?.message, response.status)
}
return body as RateMessageResponse
}
type RateMessageData = Awaited<ReturnType<typeof rateMessage>>
export const useRateMessageMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseCustomMutationOptions<RateMessageData, ResponseError, RateMessageVariables>,
'mutationFn'
> = {}) => {
return useMutation<RateMessageData, ResponseError, RateMessageVariables>({
mutationFn: (vars) => rateMessage(vars),
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
console.error(`Failed to rate message: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
})
}