mirror of
https://github.com/supabase/supabase.git
synced 2026-06-20 22:06:04 +08:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature + two follow-on fixes — small, scoped to telemetry / experiment plumbing. ## What is the current behavior? PostHog feature flags evaluated in Studio only have access to the `gotrue_id` person property (set in `useTelemetryIdentify`) and the `organization`/`project` group associations from pageviews. Flags can't target users by org membership without a behavioral cohort, which refreshes on a ~hourly schedule and lags behind real-time signup state. This is blocking the rollout of the `dataApiRevokeOnCreateDefault` experiment ahead of the May 30 default-privileges breaking change — we need to target brand-new dashboard signups with no prior org membership, and there's no person property to filter on. ## What is the new behavior? Three changes, scoped tightly to make experiment targeting reliable for brand-new signups: ### 1. Mirror `org_count` to a PostHog person property (`apps/studio/lib/telemetry.tsx`) The Studio `Telemetry` component now mirrors the user's current org-list length to a PostHog person property `org_count` via `posthog.identify(user.id, { org_count })`. The effect: - Subscribes to `useOrganizationsQuery` (shares the same React Query cache as `useSelectedOrganizationQuery`, so no extra network requests). - Dedupes via a ref keyed on `{ userId, orgCount }` so we only call identify when the value actually changes — handles user-switch (logout/login as different user with same count) correctly. - Generic enough to be useful beyond this experiment — analytics segmentation by org membership, future flags that depend on multi-org behavior, etc. ### 2. Merge pre-init identify properties (`packages/common/posthog-client.ts`) The previous `pendingIdentification` slot was a single-write buffer — calling `posthogClient.identify()` before the PostHog SDK initialized would overwrite any prior queued identify. Latent until this PR added a second identify caller (`org_count`), which exposed the last-write-wins behavior on first-visitor-before-consent flows. Now merges properties across pre-init calls for the same user so both `{ gotrue_id }` and `{ org_count }` land on the person record when the SDK flushes. Caught during Codex review. ### 3. Gate the exposure event on `org_count` being present (`apps/studio/hooks/misc/useDataApiRevokeOnCreateDefault.ts`) `useTrackDefaultPrivilegesExposure` previously fired on the first non-undefined value of the `dataApiRevokeOnCreateDefault` flag. For brand-new signups, this races the `org_count` identify: the initial `/flags/` response (before targeting can match) returns the untargeted variant, the exposure locks it in via `hasTracked`, then our identify fires and a subsequent `/flags/` refresh updates the flag — but the exposure has already recorded the wrong variant. Fix: gate the exposure on `org_count` being present on the SDK person, subscribing via `onFeatureFlags` so we pick up the post-identify `/flags/` response. Adds `posthogClient.getPersonProperty` as the local-state reader. Without this, the experiment would have a ~5-15% noise floor on cohort assignment for new signups. ## Verification End-to-end verified locally against the staging PostHog project (34343): - Local Studio's PostHog SDK has `$stored_person_properties: { gotrue_id: <uuid>, org_count: 1 }` after sign-in. - Both `$set` events landed server-side within ~300ms of each other, and the staging person record now shows `org_count = 1.0` with `gotrue_id` preserved. - Targeting query `person.properties.org_count == 1` works end-to-end against staging. ## Additional context Ref: [GROWTH-853](https://linear.app/supabase/issue/GROWTH-853) Targeting plan for the flag once shipped: `person.org_count == 1` plus a behavioral filter on recent `sign_up` event, at 5% rollout. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Telemetry now records and syncs the user's organization count as an analytics person property and avoids redundant identifications when unchanged. * Analytics client now merges queued identification properties made before initialization and exposes a method to read stored person properties. * **Bug Fixes** * Tracking now waits for organization-count readiness before firing certain exposure events to prevent missing data. * **Tests** * Added/updated tests to cover person-property behavior and gating logic. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45946) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
78 lines
2.9 KiB
TypeScript
78 lines
2.9 KiB
TypeScript
import { posthogClient } from 'common'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
|
|
import { usePHFlag } from '../ui/useFlag'
|
|
import { IS_TEST_ENV } from '@/lib/constants'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
|
|
/**
|
|
* Controls the default state of the "Automatically expose new tables"
|
|
* checkbox at project creation. When the flag is on, the checkbox defaults
|
|
* to unchecked (i.e. revoke SQL runs). When off/absent, the checkbox defaults
|
|
* to checked (current behaviour — default grants remain).
|
|
*/
|
|
export const useDataApiRevokeOnCreateDefaultEnabled = (): boolean => {
|
|
const flag = usePHFlag<boolean>('dataApiRevokeOnCreateDefault')
|
|
|
|
// Preserve current behaviour (default grants remain) in tests so existing
|
|
// E2E flows don't change silently. Tests that need the revoke-default path
|
|
// should opt in explicitly.
|
|
if (IS_TEST_ENV) {
|
|
return false
|
|
}
|
|
|
|
return !!flag
|
|
}
|
|
|
|
type DefaultPrivilegesExposureOptions =
|
|
| { surface: 'main'; dataApiEnabled: boolean }
|
|
| { surface: 'vercel' }
|
|
|
|
/**
|
|
* Fires `project_creation_default_privileges_exposed` once per mount after both
|
|
* the `dataApiRevokeOnCreateDefault` flag resolves AND the `org_count` person
|
|
* property has been set on the PostHog SDK. Waiting for both signals avoids
|
|
* locking in the variant on the initial /flags/ response, which races the
|
|
* org_count identify for brand-new signups — the exposure must reflect the
|
|
* targeting-aware flag value. Deduplicated via ref so re-renders and mid-session
|
|
* flag flips don't re-fire.
|
|
*/
|
|
export const useTrackDefaultPrivilegesExposure = (options: DefaultPrivilegesExposureOptions) => {
|
|
const track = useTrack()
|
|
const flag = usePHFlag<boolean>('dataApiRevokeOnCreateDefault')
|
|
const hasTracked = useRef(false)
|
|
const [orgCountReady, setOrgCountReady] = useState(
|
|
() => posthogClient.getPersonProperty('org_count') !== undefined
|
|
)
|
|
|
|
const { surface } = options
|
|
const dataApiEnabled = options.surface === 'main' ? options.dataApiEnabled : null
|
|
|
|
// Mark ready once org_count appears on the SDK person. A /flags/ response
|
|
// received after our identify will have included org_count in the evaluation,
|
|
// so subscribing via onFeatureFlags is the right signal that the flag store
|
|
// reflects the targeting-aware value.
|
|
useEffect(() => {
|
|
if (orgCountReady) return
|
|
const check = () => {
|
|
if (posthogClient.getPersonProperty('org_count') !== undefined) {
|
|
setOrgCountReady(true)
|
|
}
|
|
}
|
|
check()
|
|
return posthogClient.onFeatureFlags(check)
|
|
}, [orgCountReady])
|
|
|
|
useEffect(() => {
|
|
if (hasTracked.current) return
|
|
if (flag === undefined) return
|
|
if (!orgCountReady) return
|
|
hasTracked.current = true
|
|
track('project_creation_default_privileges_exposed', {
|
|
surface,
|
|
...(dataApiEnabled !== null && { dataApiEnabled }),
|
|
dataApiRevokeOnCreateDefaultEnabled: flag,
|
|
})
|
|
}, [flag, orgCountReady, track, surface, dataApiEnabled])
|
|
}
|