useDataApiRevokeOnCreateDefault.ts75 lines · main
| 1 | // @ts-nocheck |
| 2 | import { useEffect, useRef } from 'react' |
| 3 | |
| 4 | import { usePHFlag } from '../ui/useFlag' |
| 5 | import { IS_TEST_ENV } from '@/lib/constants' |
| 6 | import { useTrack } from '@/lib/telemetry/track' |
| 7 | |
| 8 | /** |
| 9 | * Controls the default state of the "Automatically expose new tables" |
| 10 | * checkbox at project creation. When the flag is on, the checkbox defaults |
| 11 | * to unchecked (i.e. revoke SQL runs). When off/absent, the checkbox defaults |
| 12 | * to checked (current behaviour — default grants remain). |
| 13 | */ |
| 14 | export const useDataApiRevokeOnCreateDefaultEnabled = (): boolean => { |
| 15 | const flag = usePHFlag<boolean>('dataApiRevokeOnCreateDefault') |
| 16 | |
| 17 | // Preserve current behaviour (default grants remain) in tests so existing |
| 18 | // E2E flows don't change silently. Tests that need the revoke-default path |
| 19 | // should opt in explicitly. |
| 20 | if (IS_TEST_ENV) { |
| 21 | return false |
| 22 | } |
| 23 | |
| 24 | return !!flag |
| 25 | } |
| 26 | |
| 27 | type DefaultPrivilegesExposureOptions = |
| 28 | | { |
| 29 | surface: 'main' |
| 30 | dataApiDefaultPrivileges: boolean |
| 31 | hasUserModified: boolean |
| 32 | } |
| 33 | | { |
| 34 | surface: 'vercel' |
| 35 | orgSlug: string | undefined |
| 36 | dataApiDefaultPrivileges: boolean |
| 37 | hasUserModified: boolean |
| 38 | } |
| 39 | |
| 40 | /** |
| 41 | * Fires `project_creation_default_privileges_exposed` once per mount, once the |
| 42 | * flag has resolved AND the form value is consistent with the flag's expected |
| 43 | * default. The convergence gate avoids a render-ordering race: caller-side |
| 44 | * sync effects (in /new/[slug] and the Vercel deploy flow) update the form |
| 45 | * value when the flag resolves late, but child effects fire before parent |
| 46 | * effects in the same commit, so without the gate the exposure would capture |
| 47 | * the stale pre-sync value. If the user has dirtied the field, fire |
| 48 | * immediately with their explicit value. Deduplicated via ref. |
| 49 | */ |
| 50 | export const useTrackDefaultPrivilegesExposure = (options: DefaultPrivilegesExposureOptions) => { |
| 51 | const track = useTrack() |
| 52 | const flag = usePHFlag<boolean>('dataApiRevokeOnCreateDefault') |
| 53 | const hasTracked = useRef(false) |
| 54 | |
| 55 | const { surface, dataApiDefaultPrivileges, hasUserModified } = options |
| 56 | const orgSlug = options.surface === 'vercel' ? options.orgSlug : undefined |
| 57 | |
| 58 | useEffect(() => { |
| 59 | if (hasTracked.current) return |
| 60 | if (flag === undefined) return |
| 61 | if (surface === 'vercel' && !orgSlug) return |
| 62 | // Gate on form-flag convergence unless the user explicitly dirtied the field. |
| 63 | if (!hasUserModified && dataApiDefaultPrivileges !== !flag) return |
| 64 | hasTracked.current = true |
| 65 | track( |
| 66 | 'project_creation_default_privileges_exposed', |
| 67 | { |
| 68 | surface, |
| 69 | dataApiDefaultPrivileges, |
| 70 | dataApiRevokeOnCreateDefaultEnabled: flag, |
| 71 | }, |
| 72 | surface === 'vercel' ? { organization: orgSlug } : undefined |
| 73 | ) |
| 74 | }, [flag, track, surface, dataApiDefaultPrivileges, hasUserModified, orgSlug]) |
| 75 | } |