Files
supabase/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx
Ali Waseem 3cb440e844 fix(studio): fix multiple Sentry errors (#44715)
## Summary

-
[**SUPABASE-APP-E2R**](https://supabase.sentry.io/issues/SUPABASE-APP-E2R):
Guard against undefined entries in notifications array in
`AdvisorButton` (optional chaining on `.some()` callbacks)
-
[**SUPABASE-APP-EBA**](https://supabase.sentry.io/issues/SUPABASE-APP-EBA):
Remove render-time `handleError()` throw in `useEdgeFunctionsDiff` — the
hook already handles missing body data gracefully
-
[**SUPABASE-APP-BVN**](https://supabase.sentry.io/issues/SUPABASE-APP-BVN)
/
[**SUPABASE-APP-BTV**](https://supabase.sentry.io/issues/SUPABASE-APP-BTV):
Guard `localStorage` access in `FeaturePreviewContext` with try-catch,
matching the established pattern in `useLocalStorage.ts` (Safari private
browsing)
-
[**SUPABASE-APP-AV3**](https://supabase.sentry.io/issues/SUPABASE-APP-AV3):
Filter stale folder IDs before passing `expandedIds` to
`react-accessible-treeview` in the SQL editor nav

## Test plan

- [x] Verify AdvisorButton renders without errors when notifications
data has sparse pages
- [x] Verify branch merge page loads when edge function body fetch fails
- [x] Verify feature previews initialize correctly in Safari private
browsing
- [x] Verify SQL editor folder expand/collapse works after deleting a
folder

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Feature preview now falls back safely when browser storage is
unavailable
* Notifications display updated to tolerate missing entries without
errors
* Private snippets navigation no longer preserves expansion state for
removed nodes

* **Refactor**
  * Streamlined error aggregation in edge functions diff processing
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-09 11:10:47 -06:00

177 lines
5.4 KiB
TypeScript

import { FeatureFlagContext, LOCAL_STORAGE_KEYS, useFlag } from 'common'
import { noop } from 'lodash'
import { useQueryState } from 'nuqs'
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type PropsWithChildren,
} from 'react'
import { useFeaturePreviews } from './useFeaturePreviews'
import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
import { EMPTY_OBJ } from '@/lib/void'
type FeaturePreviewContextType = {
flags: { [key: string]: boolean }
onUpdateFlag: (key: string, value: boolean) => void
}
const FeaturePreviewContext = createContext<FeaturePreviewContextType>({
flags: EMPTY_OBJ,
onUpdateFlag: noop,
})
export const useFeaturePreviewContext = () => useContext(FeaturePreviewContext)
export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren<{}>) => {
const { hasLoaded } = useContext(FeatureFlagContext)
const featurePreviews = useFeaturePreviews()
const [flags, setFlags] = useState(() =>
featurePreviews.reduce((a, b) => ({ ...a, [b.key]: false }), {})
)
const initializeFlags = useStaticEffectEvent(() => {
setFlags(
featurePreviews.reduce((a, b) => {
const defaultOptIn = b.isDefaultOptIn
try {
const localStorageValue = window.localStorage.getItem(b.key)
return {
...a,
[b.key]: !localStorageValue ? defaultOptIn : localStorageValue === 'true',
}
} catch {
return { ...a, [b.key]: defaultOptIn }
}
}, {})
)
})
useEffect(() => {
if (typeof window !== 'undefined') {
initializeFlags()
}
}, [hasLoaded, initializeFlags])
const value = {
flags,
onUpdateFlag: (key: string, value: boolean) => {
try {
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem(key, value ? 'true' : 'false')
}
} catch {
// Silently fail in restricted storage modes (e.g. Safari private browsing)
}
const updatedFlags = { ...flags, [key]: value }
setFlags(updatedFlags)
},
}
return <FeaturePreviewContext.Provider value={value}>{children}</FeaturePreviewContext.Provider>
}
// Helpers
export const useIsColumnLevelPrivilegesEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]
}
export const useUnifiedLogsPreview = () => {
const { flags, onUpdateFlag } = useFeaturePreviewContext()
const unifiedLogsEnabled = useFlag('unifiedLogs')
const isEnabled = unifiedLogsEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]
const enable = () => onUpdateFlag(LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, true)
const disable = () => onUpdateFlag(LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, false)
return { isEnabled, enable, disable }
}
export const useIsBranching2Enabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0]
}
export const useIsPgDeltaDiffEnabled = () => {
const { flags } = useFeaturePreviewContext()
const pgDeltaDiffEnabled = useFlag('pgdeltaDiff')
return pgDeltaDiffEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_PG_DELTA_DIFF]
}
export const useIsAdvisorRulesEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES]
}
export const useIsPlatformWebhooksEnabled = () => {
const { flags } = useFeaturePreviewContext()
const platformWebhooksEnabled = useFlag('platformWebhooks')
return platformWebhooksEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]
}
export const useIsJitDbAccessEnabled = () => {
const { flags } = useFeaturePreviewContext()
const jitDbAccessEnabled = useFlag('jitDbAccess')
return jitDbAccessEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]
}
export const useIsTableFilterBarEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_TABLE_FILTER_BAR]
}
export const useIsFloatingMobileToolbarEnabled = () => {
const { flags } = useFeaturePreviewContext()
const showFloatingMobileToolbar = useFlag('enableFloatingMobileToolbar')
return showFloatingMobileToolbar && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_FLOATING_MOBILE_TOOLBAR]
}
export const useFeaturePreviewModal = () => {
const featurePreviews = useFeaturePreviews()
const [featurePreviewModal, setFeaturePreviewModal] = useQueryState('featurePreviewModal')
const selectedFeatureKeyFromQuery = featurePreviewModal?.trim() ?? null
const showFeaturePreviewModal = selectedFeatureKeyFromQuery !== null
const selectedFeatureKey = (
!selectedFeatureKeyFromQuery ? featurePreviews[0].key : selectedFeatureKeyFromQuery
) as (typeof featurePreviews)[number]['key']
const selectFeaturePreview = useCallback(
(featureKey: (typeof featurePreviews)[number]['key']) => {
setFeaturePreviewModal(featureKey)
},
[setFeaturePreviewModal]
)
const toggleFeaturePreviewModal = useCallback(
(value: boolean) => {
if (!value) {
setFeaturePreviewModal(null)
} else {
selectFeaturePreview(selectedFeatureKey)
}
},
[selectFeaturePreview, setFeaturePreviewModal, selectedFeatureKey]
)
return useMemo(
() => ({
showFeaturePreviewModal,
selectedFeatureKey,
selectFeaturePreview,
toggleFeaturePreviewModal,
}),
[showFeaturePreviewModal, selectedFeatureKey, selectFeaturePreview, toggleFeaturePreviewModal]
)
}