useDataApiRevokeOnCreateDefault.ts75 lines · main
1// @ts-nocheck
2import { useEffect, useRef } from 'react'
3
4import { usePHFlag } from '../ui/useFlag'
5import { IS_TEST_ENV } from '@/lib/constants'
6import { 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 */
14export 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
27type 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 */
50export 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}