From c1f3bb3bb9e9700e0cd6ac5a3e38c378f12d8038 Mon Sep 17 00:00:00 2001 From: Stojan Dimitrovski Date: Fri, 4 Aug 2023 08:37:07 +0200 Subject: [PATCH] feat: add feature flag for enabling GoTrue navigatorLock --- packages/common/gotrue.ts | 6 ++-- studio/components/ui/Flag/FlagProvider.tsx | 21 +++--------- studio/lib/configcat.ts | 24 ++++++++++++++ studio/lib/gotrue.ts | 37 ++++++++++++++++++++++ studio/lib/local-storage.ts | 26 +++++++++++++++ 5 files changed, 96 insertions(+), 18 deletions(-) create mode 100644 studio/lib/configcat.ts diff --git a/packages/common/gotrue.ts b/packages/common/gotrue.ts index f64c0dd3c63..1f6444c9002 100644 --- a/packages/common/gotrue.ts +++ b/packages/common/gotrue.ts @@ -10,9 +10,11 @@ const debug = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' && globalThis?.localStorage?.getItem(AUTH_DEBUG_KEY) === 'true' -const navigatorLockEnabled = +const navigatorLockEnabled = !!( process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' && - globalThis?.localStorage?.getItem(AUTH_NAVIGATOR_LOCK_KEY) === 'true' + globalThis?.localStorage?.getItem(AUTH_NAVIGATOR_LOCK_KEY) === 'true' && + globalThis?.navigator?.locks +) export const gotrueClient = new GoTrueClient({ url: process.env.NEXT_PUBLIC_GOTRUE_URL, diff --git a/studio/components/ui/Flag/FlagProvider.tsx b/studio/components/ui/Flag/FlagProvider.tsx index 187f80e9bd9..fdd8f97f42b 100644 --- a/studio/components/ui/Flag/FlagProvider.tsx +++ b/studio/components/ui/Flag/FlagProvider.tsx @@ -5,8 +5,7 @@ import * as configcat from 'configcat-js' import { IS_PLATFORM } from 'lib/constants' import FlagContext from './FlagContext' import { useUser } from 'lib/auth' - -let client: configcat.IConfigCatClient +import { getFlags } from 'lib/configcat' const FlagProvider = ({ children }: PropsWithChildren<{}>) => { const user = useUser() @@ -14,20 +13,10 @@ const FlagProvider = ({ children }: PropsWithChildren<{}>) => { const { Provider } = FlagContext const [store, setStore] = useState({}) - const getFlags = async (user?: User) => { - if (!client) { - client = configcat.getClient( - process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY ?? '', - configcat.PollingMode.AutoPoll, - { pollIntervalSeconds: 600 } - ) - } - + const processFlags = async (user?: User) => { const flagStore: any = {} - const flagValues = - user?.email !== undefined - ? await client.getAllValuesAsync(new configcat.User(user.email)) - : await client.getAllValuesAsync() + const flagValues = await getFlags(user) + flagValues.forEach((item: any) => { flagStore[item.settingKey] = item.settingValue }) @@ -39,7 +28,7 @@ const FlagProvider = ({ children }: PropsWithChildren<{}>) => { // as per https://configcat.com/docs/sdk-reference/js/#polling-modes: // The polling downloads the config.json at the set interval and are stored in the internal cache // which subsequently all getValueAsync() calls are served from there - if (IS_PLATFORM) getFlags(user ?? undefined) + if (IS_PLATFORM) processFlags(user ?? undefined) }, [user]) return {children} diff --git a/studio/lib/configcat.ts b/studio/lib/configcat.ts new file mode 100644 index 00000000000..b937c33feda --- /dev/null +++ b/studio/lib/configcat.ts @@ -0,0 +1,24 @@ +import { User } from '@supabase/supabase-js' +import * as configcat from 'configcat-js' + +let client: configcat.IConfigCatClient + +export function getClient() { + if (client) { + return client + } + + client = configcat.getClient( + process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY ?? '', + configcat.PollingMode.AutoPoll, + { pollIntervalSeconds: 600 } + ) + + return client +} + +export async function getFlags(user?: User) { + return user?.email !== undefined + ? await getClient().getAllValuesAsync(new configcat.User(user.email)) + : await getClient().getAllValuesAsync() +} diff --git a/studio/lib/gotrue.ts b/studio/lib/gotrue.ts index 9a285edf300..c132a36ab51 100644 --- a/studio/lib/gotrue.ts +++ b/studio/lib/gotrue.ts @@ -1,5 +1,38 @@ +import { IS_PLATFORM } from './constants' import { User } from '@supabase/gotrue-js' import { gotrueClient } from 'common' +import { getFlags } from './configcat' +import { getNavigatorLockFeatureFlagThreshold, setNavigatorLockEnabled } from './local-storage' + +// The first time this file is imported, ConfigCat will be asked for all +// available feature flags. The client that this is running in will have +// determined and saved a random number [0, 100) under +// `supabase.dashboard.ff.threshold.navigatorLock`. If there is a number-valued +// feature flag `navigatorLockThreshold`, the +// `supabase.dashboard.auth.navigatorLock.enabled` localStorage key will be set +// to true if the value chosen by the browser is <= the value in the feature +// flag.On the _following_ refresh of the page, `packages/common/gotrue.ts` +// will read this value and enable the GoTrueClient navigatorLock. +// ConfigCat does not have a native way to do this, as percent-based rollouts +// are only available when ConfigCat has a user ID, and not without one, which +// can be the case here (GoTrue is used when not authenticated too). +async function determineNavigatorLockFeatureFlag() { + const flags = await getFlags() + const value = flags.find((flag) => flag.settingKey === 'navigatorLockThreshold')?.settingValue + + if (typeof value === 'number' && value > 0) { + const threshold = getNavigatorLockFeatureFlagThreshold() + + if (typeof threshold === 'number') { + setNavigatorLockEnabled(threshold <= value) + } else { + setNavigatorLockEnabled(false) + } + } else { + setNavigatorLockEnabled(false) + } +} + export { STORAGE_KEY } from 'common' export const auth = gotrueClient @@ -62,3 +95,7 @@ export const getReturnToPath = (fallback = '/projects') => { return validReturnTo + (remainingSearchParams ? `?${remainingSearchParams}` : '') } + +if (IS_PLATFORM && globalThis.window) { + determineNavigatorLockFeatureFlag() +} diff --git a/studio/lib/local-storage.ts b/studio/lib/local-storage.ts index ce3d2b3f271..87f7f8f4662 100644 --- a/studio/lib/local-storage.ts +++ b/studio/lib/local-storage.ts @@ -6,6 +6,8 @@ export const LOCAL_STORAGE_KEYS_ALLOWLIST = [ 'supabaseDarkMode', 'supabase.dashboard.sign_in_clicks', 'supabase.dashboard.auth.debug', + 'supabase.dashboard.auth.navigatorLock.enabled', + 'supabase.dashboard.auth.ff.threshold.navigatorLock', ] export function clearLocalStorage() { @@ -55,7 +57,31 @@ export function resetSignInClicks(): number { return clicks } +export function getNavigatorLockFeatureFlagThreshold() { + const str = localStorage.getItem('supabase.dashboard.auth.ff.threshold.navigatorLock') + + return str ? parseInt(str) : null +} + +function determineNavigatorLockFeatureFlagThreshold() { + if (getNavigatorLockFeatureFlagThreshold()) { + return + } + + localStorage.setItem( + 'supabase.dashboard.auth.ff.threshold.navigatorLock', + `${Math.floor(Math.random() * 100)}` + ) +} + +export function setNavigatorLockEnabled(enabled: boolean) { + localStorage.setItem('supabase.dashboard.auth.navigatorLock.enabled', enabled ? 'true' : 'false') +} + if (globalThis && globalThis.localStorage && IS_PLATFORM) { // populate the value based on the current local storage state inferSignInClicks() + + // setup the navigator lock group on initial load + determineNavigatorLockFeatureFlagThreshold() }