From a2732cd115dad011c3856819fd2abf2a80ca2154 Mon Sep 17 00:00:00 2001 From: Mikyo King Date: Mon, 27 Nov 2023 14:38:06 -0700 Subject: [PATCH] feat: feature flags context (#1802) * feat: feature flags context * feature-flags hot key * fix imports --- app/package-lock.json | 16 ++++ app/package.json | 1 + app/src/App.tsx | 13 +-- app/src/contexts/FeatureFlagsContext.tsx | 107 +++++++++++++++++++++++ app/src/pages/trace/TracePage.tsx | 3 + 5 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 app/src/contexts/FeatureFlagsContext.tsx diff --git a/app/package-lock.json b/app/package-lock.json index 2110d5b848..929e15de61 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -31,6 +31,7 @@ "react-dom": "18.2", "react-error-boundary": "^4.0.3", "react-hook-form": "^7.43.8", + "react-hotkeys-hook": "^4.4.1", "react-relay": "^15.0.0", "react-resizable-panels": "^0.0.54", "react-router": "^6.7.0", @@ -8383,6 +8384,15 @@ "react": "^16.8.0 || ^17 || ^18" } }, + "node_modules/react-hotkeys-hook": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz", + "integrity": "sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw==", + "peerDependencies": { + "react": ">=16.8.1", + "react-dom": ">=16.8.1" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -16137,6 +16147,12 @@ "integrity": "sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==", "requires": {} }, + "react-hotkeys-hook": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz", + "integrity": "sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw==", + "requires": {} + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/app/package.json b/app/package.json index e985e9f5ad..1ef1bc1c77 100644 --- a/app/package.json +++ b/app/package.json @@ -28,6 +28,7 @@ "react-dom": "18.2", "react-error-boundary": "^4.0.3", "react-hook-form": "^7.43.8", + "react-hotkeys-hook": "^4.4.1", "react-relay": "^15.0.0", "react-resizable-panels": "^0.0.54", "react-router": "^6.7.0", diff --git a/app/src/App.tsx b/app/src/App.tsx index 992faaca46..0af233991e 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -4,6 +4,7 @@ import { ThemeProvider as EmotionThemeProvider } from "@emotion/react"; import { Provider, theme } from "@arizeai/components"; +import { FeatureFlagsProvider } from "./contexts/FeatureFlagsContext"; import { NotificationProvider, ThemeProvider, useTheme } from "./contexts"; import { GlobalStyles } from "./GlobalStyles"; import RelayEnvironment from "./RelayEnvironment"; @@ -26,11 +27,13 @@ export function AppContent() { - - - - - + + + + + + + diff --git a/app/src/contexts/FeatureFlagsContext.tsx b/app/src/contexts/FeatureFlagsContext.tsx new file mode 100644 index 0000000000..cb588d98a6 --- /dev/null +++ b/app/src/contexts/FeatureFlagsContext.tsx @@ -0,0 +1,107 @@ +import React, { createContext, PropsWithChildren, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; + +import { Dialog, DialogContainer, Switch, View } from "@arizeai/components"; + +type FeatureFlag = "evals"; +export type FeatureFlagsContextType = { + featureFlags: Record; + setFeatureFlags: (featureFlags: Record) => void; +}; + +export const LOCAL_STORAGE_FEATURE_FLAGS_KEY = "arize-phoenix-feature-flags"; + +const DEFAULT_FEATURE_FLAGS: Record = { + evals: false, +}; + +function getFeatureFlags(): Record { + const featureFlagsFromLocalStorage = localStorage.getItem( + LOCAL_STORAGE_FEATURE_FLAGS_KEY + ); + if (!featureFlagsFromLocalStorage) { + return DEFAULT_FEATURE_FLAGS; + } + + try { + const parsedFeatureFlags = JSON.parse(featureFlagsFromLocalStorage); + return Object.assign({}, DEFAULT_FEATURE_FLAGS, parsedFeatureFlags); + } catch (e) { + return DEFAULT_FEATURE_FLAGS; + } +} + +export const FeatureFlagsContext = + createContext(null); + +export function useFeatureFlags() { + const context = React.useContext(FeatureFlagsContext); + if (context === null) { + throw new Error( + "useFeatureFlags must be used within a FeatureFlagsProvider" + ); + } + return context; +} + +export function useFeatureFlag(featureFlag: FeatureFlag) { + const { featureFlags } = useFeatureFlags(); + return featureFlags[featureFlag]; +} + +export function FeatureFlagsProvider(props: React.PropsWithChildren) { + const [featureFlags, _setFeatureFlags] = useState< + Record + >(getFeatureFlags()); + const setFeatureFlags = (featureFlags: Record) => { + localStorage.setItem( + LOCAL_STORAGE_FEATURE_FLAGS_KEY, + JSON.stringify(featureFlags) + ); + _setFeatureFlags(featureFlags); + }; + + return ( + + {props.children} + + ); +} + +function FeatureFlagsControls(props: PropsWithChildren) { + const { children } = props; + const { featureFlags, setFeatureFlags } = useFeatureFlags(); + const [showControls, setShowControls] = useState(false); + useHotkeys("ctrl+shift+f", () => setShowControls(true)); + return ( + <> + {children} + setShowControls(false)} + > + {showControls && ( + + + {Object.keys(featureFlags).map((featureFlag) => ( + + setFeatureFlags({ + ...featureFlags, + [featureFlag]: isSelected, + }) + } + > + {featureFlag} + + ))} + + + )} + + + ); +} diff --git a/app/src/pages/trace/TracePage.tsx b/app/src/pages/trace/TracePage.tsx index 9b44abb5bb..628d7bca37 100644 --- a/app/src/pages/trace/TracePage.tsx +++ b/app/src/pages/trace/TracePage.tsx @@ -41,6 +41,7 @@ import { SpanItem } from "@phoenix/components/trace/SpanItem"; import { SpanKindIcon } from "@phoenix/components/trace/SpanKindIcon"; import { TraceTree } from "@phoenix/components/trace/TraceTree"; import { useTheme } from "@phoenix/contexts"; +import { useFeatureFlag } from "@phoenix/contexts/FeatureFlagsContext"; import { DOCUMENT_CONTENT, DOCUMENT_ID, @@ -267,6 +268,7 @@ function SelectedSpanDetails({ selectedSpan }: { selectedSpan: Span }) { const hasExceptions = useMemo(() => { return spanHasException(selectedSpan); }, [selectedSpan]); + const evalsEnabled = useFeatureFlag("evals"); return ( + {evalsEnabled ? Evals Tab : null} );