useFlag.ts 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. import * as Sentry from '@sentry/nextjs'
  2. import { IS_PLATFORM, useFeatureFlags } from 'common'
  3. import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
  4. import { trackFeatureFlag } from '@/lib/posthog'
  5. const isObjectEmpty = (obj: Object) => {
  6. return Object.keys(obj).length === 0
  7. }
  8. /**
  9. * Hook to retrieve a PostHog feature flag value.
  10. *
  11. * @returns `undefined | false | T` where:
  12. * - `undefined` = PostHog store is still loading OR flag doesn't exist (treat as "don't show")
  13. * - `false` = Flag is explicitly set to false (typically means "disabled" or "control" for experiments)
  14. * - `T` = The actual flag value (string, boolean, or custom type like variant names)
  15. *
  16. * @example Experiment usage convention:
  17. * ```typescript
  18. * const variant = usePHFlag<ExperimentVariant | false>('experimentName')
  19. *
  20. * // undefined = loading/doesn't exist, don't render anything yet
  21. * if (variant === undefined) return null
  22. *
  23. * // false = explicitly disabled, show control
  24. * if (variant === false) return <Control />
  25. *
  26. * // Otherwise, variant has a value, show experiment
  27. * return <Experiment variant={variant} />
  28. * ```
  29. *
  30. * @todo TODO(Alaister): move this to packages/common/feature-flags.tsx and rename to useFlag
  31. * @todo TODO(sean): Refactor to have explicit loading/disabled/value states
  32. * See https://linear.app/briven/issue/GROWTH-539
  33. */
  34. export function usePHFlag<T = string | boolean>(name: string) {
  35. const flagStore = useFeatureFlags()
  36. // [Joshen] Prepend PH flags with "PH" in local storage for easier identification of PH flags
  37. const [trackedValue, setTrackedValue] = useLocalStorageQuery(`ph_${name}`, '')
  38. const store = flagStore.posthog
  39. const flagValue = store[name]
  40. if (!IS_PLATFORM) return false
  41. // Flag store has not been initialized
  42. if (isObjectEmpty(store)) return undefined
  43. if (!isObjectEmpty(store) && flagValue === undefined) {
  44. console.error(`Flag key "${name}" does not exist in PostHog flag store`)
  45. return undefined
  46. }
  47. if (trackedValue !== flagValue) {
  48. try {
  49. // [Joshen] Only fire the track endpoint once across sessions unless the flag value changes
  50. // Note: This cannot guarantee excess calls in the event for e.g user clears local storage or uses incognito
  51. // trackFeatureFlag checks for telemetry consent before actually firing the request too
  52. trackFeatureFlag({ feature_flag_name: name, feature_flag_value: flagValue })
  53. setTrackedValue(flagValue as string)
  54. } catch (error: any) {
  55. Sentry.withScope((scope) => {
  56. scope.setTag('type', 'phTrackFailure')
  57. Sentry.captureException(error)
  58. })
  59. console.error(error.message)
  60. }
  61. }
  62. return flagValue as T
  63. }