mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(studio): enable Assistant tracing for High Compliance projects (#50759)
Assistant chats from High Compliance projects now flow to Braintrust like any other project. The constraint that required suppressing them no longer applies, see AI-1241 for the details. `isTracingAllowed` now takes only the project region to maintain EU exclusion. Traces also carry an `isHighComplianceProject` metadata field, so the project's status at the time of the trace is recorded rather than looked up later against a setting customers can toggle. To verify, see [this sample trace](https://www.braintrust.dev/app/supabase.io/p/Assistant/logs?r=afabbdcc-aa89-446e-aa52-78aaa90d44a4&v=Production&s=afabbdcc-aa89-446e-aa52-78aaa90d44a4&tvt=trace) from a High Compliance project on staging which indicates that tracing is now enabled for these projects and that it carries metadata showing the high compliance status. | High Compliance project setting | `isHighComplianceProject` metadata | |--------|--------| | <img width="1554" height="454" alt="CleanShot 2026-09-22 at 5 14 58 PM@2x" src="https://github.com/user-attachments/assets/23901c6e-0d79-44e8-a6dd-43cdedba1799" /> | <img width="1674" height="990" alt="CleanShot 2026-09-22 at 5 17 40 PM@2x" src="https://github.com/user-attachments/assets/fb2fba55-bcc1-4136-a432-b33a5c7f9ca2" /> | Closes AI-1241 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Changes** * AI project compliance information is now represented by a unified high-compliance project status. * AI response tracing is now determined by project region: tracing remains disabled for EU and unknown regions, while known non-EU regions are eligible. * AI feedback and SQL generation now use the updated compliance and regional handling. * **Tests** * Updated coverage to reflect the revised compliance and tracing behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
4730e3a640
commit
fd5ef806d6
9 files changed
+46
-175
No files matched your search
@@ -10,10 +10,6 @@ vi.mock('@/data/projects/project-detail-query', () => ({
|
||||
getProjectDetail: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/data/subscriptions/org-subscription-query', () => ({
|
||||
getOrgSubscription: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/data/config/project-settings-v2-query', () => ({
|
||||
getProjectSettings: vi.fn(),
|
||||
}))
|
||||
@@ -22,10 +18,6 @@ vi.mock('@/hooks/misc/useOrgOptedIntoAi', () => ({
|
||||
getAiOptInLevel: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/components/interfaces/Billing/Subscription/Subscription.utils', () => ({
|
||||
subscriptionHasHipaaAddon: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/data/entitlements/entitlements-query', () => ({
|
||||
checkEntitlement: vi.fn(),
|
||||
}))
|
||||
@@ -37,29 +29,22 @@ const PROJECT_REF = 'test-project'
|
||||
|
||||
describe('getAIDetails', () => {
|
||||
let mockGetOrganizations: ReturnType<typeof vi.fn>
|
||||
let mockGetOrgSubscription: ReturnType<typeof vi.fn>
|
||||
let mockGetProjectDetail: ReturnType<typeof vi.fn>
|
||||
let mockGetProjectSettings: ReturnType<typeof vi.fn>
|
||||
let mockGetAiOptInLevel: ReturnType<typeof vi.fn>
|
||||
let mockSubscriptionHasHipaaAddon: ReturnType<typeof vi.fn>
|
||||
let mockCheckEntitlement: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(async () => {
|
||||
const orgsQuery = await import('@/data/organizations/organizations-query')
|
||||
const subscriptionQuery = await import('@/data/subscriptions/org-subscription-query')
|
||||
const projectQuery = await import('@/data/projects/project-detail-query')
|
||||
const settingsQuery = await import('@/data/config/project-settings-v2-query')
|
||||
const aiHook = await import('@/hooks/misc/useOrgOptedIntoAi')
|
||||
const subscriptionUtils =
|
||||
await import('@/components/interfaces/Billing/Subscription/Subscription.utils')
|
||||
const entitlementsQuery = await import('@/data/entitlements/entitlements-query')
|
||||
|
||||
mockGetOrganizations = vi.mocked(orgsQuery.getOrganizations)
|
||||
mockGetOrgSubscription = vi.mocked(subscriptionQuery.getOrgSubscription)
|
||||
mockGetProjectDetail = vi.mocked(projectQuery.getProjectDetail)
|
||||
mockGetProjectSettings = vi.mocked(settingsQuery.getProjectSettings)
|
||||
mockGetAiOptInLevel = vi.mocked(aiHook.getAiOptInLevel)
|
||||
mockSubscriptionHasHipaaAddon = vi.mocked(subscriptionUtils.subscriptionHasHipaaAddon)
|
||||
mockCheckEntitlement = vi.mocked(entitlementsQuery.checkEntitlement)
|
||||
|
||||
mockGetOrganizations.mockResolvedValue([
|
||||
@@ -71,8 +56,6 @@ describe('getAIDetails', () => {
|
||||
organization_id: 1,
|
||||
})
|
||||
mockGetProjectSettings.mockResolvedValue({ is_sensitive: false })
|
||||
mockGetOrgSubscription.mockResolvedValue({ addons: [] })
|
||||
mockSubscriptionHasHipaaAddon.mockReturnValue(false)
|
||||
mockCheckEntitlement.mockResolvedValue({ hasAccess: false })
|
||||
mockGetAiOptInLevel.mockReturnValue('schema')
|
||||
})
|
||||
@@ -87,15 +70,26 @@ describe('getAIDetails', () => {
|
||||
expect(result).toEqual({
|
||||
aiOptInLevel: 'schema',
|
||||
hasAccessToAdvanceModel: false,
|
||||
hasHipaaAddon: false,
|
||||
orgId: 1,
|
||||
orgSlug: ORG_SLUG,
|
||||
planId: 'pro',
|
||||
region: 'us-east-1',
|
||||
isSensitive: false,
|
||||
isHighComplianceProject: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('flags a High Compliance project', async () => {
|
||||
mockGetProjectSettings.mockResolvedValue({ is_sensitive: true })
|
||||
|
||||
const result = await getAIDetails({
|
||||
orgSlug: ORG_SLUG,
|
||||
projectRef: PROJECT_REF,
|
||||
authorization: AUTH,
|
||||
})
|
||||
|
||||
expect(result.isHighComplianceProject).toBe(true)
|
||||
})
|
||||
|
||||
it('calls getAiOptInLevel with the matched org opt_in_tags', async () => {
|
||||
const opt_in_tags = ['AI_SQL_GENERATOR_OPT_IN']
|
||||
mockGetOrganizations.mockResolvedValue([
|
||||
@@ -144,7 +138,6 @@ describe('getAIDetails', () => {
|
||||
await getAIDetails({ orgSlug: ORG_SLUG, projectRef: PROJECT_REF, authorization: AUTH })
|
||||
|
||||
expect(mockGetOrganizations).toHaveBeenCalledWith({ headers: HEADERS })
|
||||
expect(mockGetOrgSubscription).toHaveBeenCalledWith({ orgSlug: ORG_SLUG }, undefined, HEADERS)
|
||||
expect(mockCheckEntitlement).toHaveBeenCalledWith(
|
||||
ORG_SLUG,
|
||||
'assistant.advance_model',
|
||||
@@ -192,16 +185,14 @@ describe('getAIDetails', () => {
|
||||
expect(result.planId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('leaves hasHipaaAddon undefined so tracing checks fail closed', async () => {
|
||||
mockSubscriptionHasHipaaAddon.mockReturnValue(false)
|
||||
|
||||
it('leaves the region undefined so tracing checks fail closed', async () => {
|
||||
const result = await getAIDetails({
|
||||
orgSlug: ORG_SLUG,
|
||||
projectRef: PROJECT_REF,
|
||||
authorization: AUTH,
|
||||
})
|
||||
|
||||
expect(result.hasHipaaAddon).toBeUndefined()
|
||||
expect(result.region).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -231,33 +222,4 @@ describe('getAIDetails', () => {
|
||||
expect(result.aiOptInLevel).toBe('disabled')
|
||||
expect(result.region).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps the opt-in level for a sensitive project in a HIPAA org', async () => {
|
||||
mockSubscriptionHasHipaaAddon.mockReturnValue(true)
|
||||
mockGetAiOptInLevel.mockReturnValue('schema_and_log_and_data')
|
||||
mockGetProjectSettings.mockResolvedValue({ is_sensitive: true })
|
||||
|
||||
const result = await getAIDetails({
|
||||
orgSlug: ORG_SLUG,
|
||||
projectRef: PROJECT_REF,
|
||||
authorization: AUTH,
|
||||
})
|
||||
|
||||
expect(result.aiOptInLevel).toBe('schema_and_log_and_data')
|
||||
expect(result.hasHipaaAddon).toBe(true)
|
||||
expect(result.isSensitive).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the opt-in level for a sensitive project outside a HIPAA org', async () => {
|
||||
mockSubscriptionHasHipaaAddon.mockReturnValue(false)
|
||||
mockGetProjectSettings.mockResolvedValue({ is_sensitive: true })
|
||||
|
||||
const result = await getAIDetails({
|
||||
orgSlug: ORG_SLUG,
|
||||
projectRef: PROJECT_REF,
|
||||
authorization: AUTH,
|
||||
})
|
||||
|
||||
expect(result.aiOptInLevel).toBe('schema')
|
||||
})
|
||||
})
|
||||
@@ -1,20 +1,18 @@
|
||||
import { subscriptionHasHipaaAddon } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
|
||||
import { getProjectSettings } from '@/data/config/project-settings-v2-query'
|
||||
import { checkEntitlement } from '@/data/entitlements/entitlements-query'
|
||||
import { getOrganizations } from '@/data/organizations/organizations-query'
|
||||
import { getProjectDetail } from '@/data/projects/project-detail-query'
|
||||
import { getOrgSubscription } from '@/data/subscriptions/org-subscription-query'
|
||||
import { getAiOptInLevel, type AiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi'
|
||||
|
||||
export type AIDetails = {
|
||||
aiOptInLevel: AiOptInLevel
|
||||
hasAccessToAdvanceModel: boolean
|
||||
hasHipaaAddon: boolean | undefined
|
||||
orgId: number | undefined
|
||||
orgSlug: string | undefined
|
||||
planId: string | undefined
|
||||
region: string | undefined
|
||||
isSensitive: boolean | null | undefined
|
||||
/** "High Compliance" in the dashboard, `is_sensitive` in the platform API. */
|
||||
isHighComplianceProject: boolean | undefined
|
||||
}
|
||||
|
||||
// Resolves the AI opt-in level, model access and tracing inputs for one org/project pair.
|
||||
@@ -34,19 +32,17 @@ export const getAIDetails = async ({
|
||||
...(authorization && { Authorization: authorization }),
|
||||
}
|
||||
|
||||
const [organizations, subscription, advanceModelAccess, project, projectSettings] =
|
||||
await Promise.all([
|
||||
getOrganizations({ headers }),
|
||||
getOrgSubscription({ orgSlug }, undefined, headers),
|
||||
checkEntitlement(orgSlug, 'assistant.advance_model', undefined, headers),
|
||||
// skipWake: only organization_id and region are needed, neither requires a running project
|
||||
getProjectDetail({ ref: projectRef, skipWake: true }, undefined, headers),
|
||||
getProjectSettings({ projectRef }, undefined, headers),
|
||||
])
|
||||
const [organizations, advanceModelAccess, project, projectSettings] = await Promise.all([
|
||||
getOrganizations({ headers }),
|
||||
checkEntitlement(orgSlug, 'assistant.advance_model', undefined, headers),
|
||||
// skipWake: only organization_id and region are needed, neither requires a running project
|
||||
getProjectDetail({ ref: projectRef, skipWake: true }, undefined, headers),
|
||||
getProjectSettings({ projectRef }, undefined, headers),
|
||||
])
|
||||
|
||||
const selectedOrg = organizations.find((org) => org.slug === orgSlug)
|
||||
const region = project?.region
|
||||
const isSensitive = projectSettings?.is_sensitive
|
||||
const isHighComplianceProject = projectSettings?.is_sensitive ?? undefined
|
||||
|
||||
const isProjectInOrg = selectedOrg !== undefined && project?.organization_id === selectedOrg.id
|
||||
|
||||
@@ -54,26 +50,22 @@ export const getAIDetails = async ({
|
||||
return {
|
||||
aiOptInLevel: 'disabled',
|
||||
hasAccessToAdvanceModel: false,
|
||||
// Undefined rather than false so isTracingAllowed fails closed
|
||||
hasHipaaAddon: undefined,
|
||||
orgId: undefined,
|
||||
orgSlug: undefined,
|
||||
planId: undefined,
|
||||
region,
|
||||
isSensitive,
|
||||
// Undefined rather than the real region so isTracingAllowed fails closed
|
||||
region: undefined,
|
||||
isHighComplianceProject: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription)
|
||||
|
||||
return {
|
||||
aiOptInLevel: getAiOptInLevel(selectedOrg.opt_in_tags),
|
||||
hasAccessToAdvanceModel: advanceModelAccess.hasAccess,
|
||||
hasHipaaAddon,
|
||||
orgId: selectedOrg.id,
|
||||
orgSlug: selectedOrg.slug,
|
||||
planId: selectedOrg.plan.id,
|
||||
region,
|
||||
isSensitive,
|
||||
isHighComplianceProject,
|
||||
}
|
||||
}
|
||||
@@ -2,71 +2,18 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { isTracingAllowed } from './braintrust-logger'
|
||||
|
||||
const baseAllowed = {
|
||||
orgHasHipaaAddon: false,
|
||||
projectIsSensitive: false,
|
||||
projectRegion: 'us-east-1',
|
||||
}
|
||||
|
||||
describe('isTracingAllowed', () => {
|
||||
it('allows tracing when all flags are explicitly off/non-EU', () => {
|
||||
expect(isTracingAllowed(baseAllowed)).toBe(true)
|
||||
})
|
||||
|
||||
it('disallows tracing when HIPAA addon is active and project is sensitive', () => {
|
||||
expect(
|
||||
isTracingAllowed({ ...baseAllowed, orgHasHipaaAddon: true, projectIsSensitive: true })
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('allows tracing when HIPAA addon is active but project is not sensitive', () => {
|
||||
expect(
|
||||
isTracingAllowed({ ...baseAllowed, orgHasHipaaAddon: true, projectIsSensitive: false })
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('allows tracing when project is sensitive but no HIPAA addon', () => {
|
||||
expect(
|
||||
isTracingAllowed({ ...baseAllowed, orgHasHipaaAddon: false, projectIsSensitive: true })
|
||||
).toBe(true)
|
||||
it('allows tracing for non-EU regions', () => {
|
||||
expect(isTracingAllowed({ projectRegion: 'us-east-1' })).toBe(true)
|
||||
expect(isTracingAllowed({ projectRegion: 'ap-southeast-1' })).toBe(true)
|
||||
})
|
||||
|
||||
it('disallows tracing for EU regions', () => {
|
||||
expect(isTracingAllowed({ ...baseAllowed, projectRegion: 'eu-west-1' })).toBe(false)
|
||||
expect(isTracingAllowed({ ...baseAllowed, projectRegion: 'eu-central-1' })).toBe(false)
|
||||
expect(isTracingAllowed({ projectRegion: 'eu-west-1' })).toBe(false)
|
||||
expect(isTracingAllowed({ projectRegion: 'eu-central-1' })).toBe(false)
|
||||
})
|
||||
|
||||
it('allows tracing for non-EU regions', () => {
|
||||
expect(isTracingAllowed({ ...baseAllowed, projectRegion: 'ap-southeast-1' })).toBe(true)
|
||||
})
|
||||
|
||||
it('allows tracing when HIPAA addon is false and is_sensitive is null (DB default)', () => {
|
||||
expect(isTracingAllowed({ ...baseAllowed, projectIsSensitive: null })).toBe(true)
|
||||
})
|
||||
|
||||
it('disallows tracing when HIPAA addon is unknown and is_sensitive is null', () => {
|
||||
expect(
|
||||
isTracingAllowed({ ...baseAllowed, orgHasHipaaAddon: undefined, projectIsSensitive: null })
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('disallows tracing when flags are undefined (unknown = restricted)', () => {
|
||||
expect(
|
||||
isTracingAllowed({
|
||||
orgHasHipaaAddon: undefined,
|
||||
projectIsSensitive: undefined,
|
||||
projectRegion: undefined,
|
||||
})
|
||||
).toBe(false)
|
||||
expect(isTracingAllowed({ ...baseAllowed, projectRegion: undefined })).toBe(false)
|
||||
expect(isTracingAllowed({ ...baseAllowed, orgHasHipaaAddon: undefined })).toBe(false)
|
||||
// projectIsSensitive unknown only matters when orgHasHipaaAddon is also unknown
|
||||
expect(
|
||||
isTracingAllowed({
|
||||
...baseAllowed,
|
||||
orgHasHipaaAddon: undefined,
|
||||
projectIsSensitive: undefined,
|
||||
})
|
||||
).toBe(false)
|
||||
it('disallows tracing when the region is unknown', () => {
|
||||
expect(isTracingAllowed({ projectRegion: undefined })).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -13,25 +13,9 @@ if (IS_TRACING_ENABLED) {
|
||||
})
|
||||
}
|
||||
|
||||
// Checks compliance flags for tracing and returns true only when all checks pass.
|
||||
// Defaults to disabling tracing when states are unknown.
|
||||
export function isTracingAllowed({
|
||||
orgHasHipaaAddon,
|
||||
projectIsSensitive,
|
||||
projectRegion,
|
||||
}: {
|
||||
orgHasHipaaAddon: boolean | undefined
|
||||
projectIsSensitive: boolean | null | undefined
|
||||
projectRegion: string | undefined
|
||||
}) {
|
||||
// Disable tracing for EU (or unknown) regions
|
||||
// Checks that the project is outside the EU. An unknown region fails closed.
|
||||
export function isTracingAllowed({ projectRegion }: { projectRegion: string | undefined }) {
|
||||
if (projectRegion === undefined || projectRegion.startsWith('eu-')) return false
|
||||
|
||||
// Disable tracing for orgs with an unknown HIPAA addon state
|
||||
if (orgHasHipaaAddon === undefined) return false
|
||||
|
||||
// Disable tracing for projects within a HIPAA-enabled org that are sensitive (or unknown sensitivity)
|
||||
if (orgHasHipaaAddon && projectIsSensitive !== false) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -41,6 +41,7 @@ export async function generateAssistantResponse({
|
||||
orgId,
|
||||
orgSlug,
|
||||
planId,
|
||||
isHighComplianceProject,
|
||||
includesLogsSnippets,
|
||||
isExplorerEnabled,
|
||||
systemProviderOptions,
|
||||
@@ -63,6 +64,7 @@ export async function generateAssistantResponse({
|
||||
orgId?: number
|
||||
orgSlug?: string
|
||||
planId?: string
|
||||
isHighComplianceProject?: boolean
|
||||
/** Whether any user message in the conversation attached a logs (ClickHouse) query. */
|
||||
includesLogsSnippets?: boolean
|
||||
isExplorerEnabled?: boolean
|
||||
@@ -175,6 +177,7 @@ export async function generateAssistantResponse({
|
||||
orgId,
|
||||
orgSlug,
|
||||
planId,
|
||||
isHighComplianceProject,
|
||||
requestedModel,
|
||||
gitBranch: process.env.VERCEL_GIT_COMMIT_REF,
|
||||
environment: process.env.NEXT_PUBLIC_ENVIRONMENT,
|
||||
|
||||
@@ -14,9 +14,7 @@ vi.mock('@/lib/ai/ai-details', () => ({
|
||||
getAIDetails: vi.fn().mockResolvedValue({
|
||||
aiOptInLevel: 'schema_and_log_and_data',
|
||||
hasAccessToAdvanceModel: true,
|
||||
hasHipaaAddon: false,
|
||||
region: 'us-east-1',
|
||||
isSensitive: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
|
||||
@@ -13,9 +13,7 @@ vi.mock('@/lib/ai/ai-details', () => ({
|
||||
getAIDetails: vi.fn().mockResolvedValue({
|
||||
aiOptInLevel: 'schema_and_log_and_data',
|
||||
hasAccessToAdvanceModel: true,
|
||||
hasHipaaAddon: false,
|
||||
region: 'us-east-1',
|
||||
isSensitive: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
|
||||
Reference in new issue
Block a user