diff --git a/agenta-web/.eslintrc.json b/agenta-web/.eslintrc.json index 188940439e..fb31c1c1e3 100644 --- a/agenta-web/.eslintrc.json +++ b/agenta-web/.eslintrc.json @@ -2,6 +2,8 @@ "extends": ["next/core-web-vitals"], "rules": { "react/no-unescaped-entities": 0, - "react/display-name": 0 + "react/display-name": 0, + "@next/next/no-sync-scripts": 0, + "react/no-children-prop": 0 } } diff --git a/agenta-web/next.config.js b/agenta-web/next.config.js index 28a2e7311f..19f9495156 100644 --- a/agenta-web/next.config.js +++ b/agenta-web/next.config.js @@ -6,7 +6,7 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({ const nextConfig = { output: "standalone", reactStrictMode: true, - pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"], + pageExtensions: ["ts", "tsx", "js", "jsx"], productionBrowserSourceMaps: true, transpilePackages: [ "@lobehub/ui", @@ -26,6 +26,11 @@ const nextConfig = { images: { remotePatterns: [{hostname: "fps.cdnpk.net"}], }, + ...(process.env.NEXT_PUBLIC_FF === "cloud" && { + experimental: { + instrumentationHook: true, + }, + }), async redirects() { return [ @@ -54,8 +59,58 @@ const nextConfig = { ) } + if (process.env.NEXT_PUBLIC_FF === "cloud") { + config.plugins.push( + new webpack.DefinePlugin({ + __SENTRY_DEBUG__: false, + __SENTRY_TRACING__: true, + __RRWEB_EXCLUDE_IFRAME__: true, + __RRWEB_EXCLUDE_SHADOW_DOM__: true, + __SENTRY_EXCLUDE_REPLAY_WORKER__: true, + }), + ) + } + return config }, } -module.exports = withBundleAnalyzer(nextConfig) +if (process.env.NEXT_PUBLIC_FF === "cloud") { + const {withSentryConfig} = require("@sentry/nextjs") + + module.exports = withBundleAnalyzer( + withSentryConfig( + nextConfig, + { + // For all available options, see: + // https://github.com/getsentry/sentry-webpack-plugin#options + + // Suppresses source map uploading logs during build + silent: true, + org: "agenta-ai", + project: "javascript-nextjs", + }, + { + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + + // Transpiles SDK to be compatible with IE11 (increases bundle size) + transpileClientSDK: true, + + // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load) + tunnelRoute: "/monitoring", + + // Hides source maps from generated client bundles + hideSourceMaps: true, + + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + }, + ), + ) +} else { + module.exports = withBundleAnalyzer(nextConfig) +} diff --git a/agenta-web/src/_app.tsx b/agenta-web/src/_app.tsx deleted file mode 100644 index e03e20b16c..0000000000 --- a/agenta-web/src/_app.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import "@/styles/globals.css" -import type {AppProps} from "next/app" -import Layout from "@/components/Layout/Layout" -import ThemeContextProvider from "@/components/Layout/ThemeContextProvider" - -export default function App({Component, pageProps}: AppProps) { - return ( - - - - - - ) -} diff --git a/agenta-web/src/components/OrgWrapper/OrgWrapper.tsx b/agenta-web/src/components/OrgWrapper/OrgWrapper.tsx new file mode 100644 index 0000000000..2cfbc210ae --- /dev/null +++ b/agenta-web/src/components/OrgWrapper/OrgWrapper.tsx @@ -0,0 +1,29 @@ +import {useState, useEffect, useCallback, ComponentType, FC} from "react" +import {isDemo} from "@/lib/helpers/utils" +import {dynamicContext} from "@/lib/helpers/dynamic" + +type OrgWrapperProps = {children: React.ReactNode} + +const OrgWrapper: FC = ({children}) => { + const [OrgContextProvider, setOrgContextProvider] = + useState | null>(null) + + const initializeOrgProvider = useCallback(async () => { + const Provider = await dynamicContext("org.context") + setOrgContextProvider(() => Provider.default) + }, []) + + useEffect(() => { + if (isDemo()) { + initializeOrgProvider() + } + }, [initializeOrgProvider]) + + if (isDemo() && OrgContextProvider) { + return {children} + } + + return <>{children} +} + +export default OrgWrapper diff --git a/agenta-web/src/components/Scripts/GlobalScripts.tsx b/agenta-web/src/components/Scripts/GlobalScripts.tsx new file mode 100644 index 0000000000..e491d3722b --- /dev/null +++ b/agenta-web/src/components/Scripts/GlobalScripts.tsx @@ -0,0 +1,34 @@ +import {useState, useEffect, useCallback, ComponentType} from "react" +import Head from "next/head" +import {isDemo} from "@/lib/helpers/utils" +import {dynamicComponent} from "@/lib/helpers/dynamic" + +const GlobalScripts = () => { + const [CloudScripts, setCloudScripts] = useState(null) + + const initializeScripts = useCallback(() => { + const Scripts = dynamicComponent("Scripts/assets/CloudScripts") + setCloudScripts((prev: any) => prev ?? Scripts) + }, []) + + useEffect(() => { + if (!CloudScripts && isDemo()) { + initializeScripts() + } + }, [initializeScripts, CloudScripts]) + + if (isDemo() && CloudScripts) { + return + } + + return ( + <> + + Agenta: The LLMOps platform. + + + + ) +} + +export default GlobalScripts diff --git a/agenta-web/src/lib/helpers/analytics/AgPosthogProvider.tsx b/agenta-web/src/lib/helpers/analytics/AgPosthogProvider.tsx index 747f99134c..9e9fb4252b 100644 --- a/agenta-web/src/lib/helpers/analytics/AgPosthogProvider.tsx +++ b/agenta-web/src/lib/helpers/analytics/AgPosthogProvider.tsx @@ -3,8 +3,10 @@ import {useRouter} from "next/router" import {useAtom} from "jotai" import {posthogAtom, type PostHogConfig} from "./store/atoms" import {CustomPosthogProviderType} from "./types" +import {CLOUD_CONFIG, OSS_CONFIG} from "./assets/constants" +import {isDemo} from "../utils" -const CustomPosthogProvider: CustomPosthogProviderType = ({children, config}) => { +const CustomPosthogProvider: CustomPosthogProviderType = ({children}) => { const router = useRouter() const [loadingPosthog, setLoadingPosthog] = useState(false) const [posthogClient, setPosthogClient] = useAtom(posthogAtom) @@ -26,12 +28,12 @@ const CustomPosthogProvider: CustomPosthogProviderType = ({children, config}) => if (process.env.NODE_ENV === "development") posthog.debug() }, capture_pageview: false, - ...config, + ...((isDemo() ? CLOUD_CONFIG : OSS_CONFIG) as Partial), }) } finally { setLoadingPosthog(false) } - }, [loadingPosthog, config, posthogClient, setPosthogClient]) + }, [loadingPosthog, posthogClient, setPosthogClient]) useEffect(() => { initPosthog() diff --git a/agenta-web/src/lib/helpers/analytics/assets/constants.ts b/agenta-web/src/lib/helpers/analytics/assets/constants.ts new file mode 100644 index 0000000000..1f416c7d1f --- /dev/null +++ b/agenta-web/src/lib/helpers/analytics/assets/constants.ts @@ -0,0 +1,12 @@ +export const CLOUD_CONFIG = { + session_recording: { + maskAllInputs: false, + maskInputOptions: { + password: true, + email: true, + }, + }, +} +export const OSS_CONFIG = { + persistence: "localStorage+cookie", +} diff --git a/agenta-web/src/lib/helpers/analytics/types.d.ts b/agenta-web/src/lib/helpers/analytics/types.d.ts index d55cc0d99c..15a6c5b63a 100644 --- a/agenta-web/src/lib/helpers/analytics/types.d.ts +++ b/agenta-web/src/lib/helpers/analytics/types.d.ts @@ -3,5 +3,4 @@ import {type PostHogConfig} from "./store/atoms" export interface CustomPosthogProviderType extends React.FC<{ children: React.ReactNode - config: Partial }> {} diff --git a/agenta-web/src/lib/helpers/auth/AuthProvider.tsx b/agenta-web/src/lib/helpers/auth/AuthProvider.tsx new file mode 100644 index 0000000000..92f100a22a --- /dev/null +++ b/agenta-web/src/lib/helpers/auth/AuthProvider.tsx @@ -0,0 +1,37 @@ +import {useEffect, useCallback} from "react" +import SuperTokensReact, {SuperTokensWrapper} from "supertokens-auth-react" +import {AuthProviderType} from "./types" +import {isDemo} from "../utils" +import {dynamicConfig} from "../dynamic" +;(async () => { + if (typeof window !== "undefined" && isDemo()) { + const {frontendConfig} = await dynamicConfig("frontendConfig") + SuperTokensReact.init(frontendConfig()) + } +})() + +const AuthProvider: AuthProviderType = ({children, pageProps}) => { + const doRefresh = useCallback(async () => { + if (isDemo() && pageProps.fromSupertokens === "needs-refresh") { + const session = await import("supertokens-auth-react/recipe/session") + + if (await session.attemptRefreshingSession()) { + location.reload() + } else { + SuperTokensReact.redirectToAuth() + } + } + }, [pageProps.fromSupertokens]) + + useEffect(() => { + doRefresh() + }, [doRefresh]) + + if (isDemo() && pageProps.fromSupertokens === "needs-refresh") { + return null + } + + return isDemo() ? {children} : <>{children} +} + +export default AuthProvider diff --git a/agenta-web/src/lib/helpers/auth/types.d.ts b/agenta-web/src/lib/helpers/auth/types.d.ts new file mode 100644 index 0000000000..65ab18f7a3 --- /dev/null +++ b/agenta-web/src/lib/helpers/auth/types.d.ts @@ -0,0 +1,5 @@ +export interface AuthProviderType + extends React.FC<{ + children: React.ReactNode + pageProps: any + }> {} diff --git a/agenta-web/src/lib/helpers/dynamic.ts b/agenta-web/src/lib/helpers/dynamic.ts index effbc89eaa..18018e44fa 100644 --- a/agenta-web/src/lib/helpers/dynamic.ts +++ b/agenta-web/src/lib/helpers/dynamic.ts @@ -38,3 +38,11 @@ export async function dynamicLib(path: string, fallback?: any) { return fallback } } + +export async function dynamicConfig(path: string, fallback?: any) { + try { + return await import(`@/config/${path}`) + } catch (error) { + return fallback + } +} diff --git a/agenta-web/src/lib/helpers/sentry/hook/useSentryIntegrations.ts b/agenta-web/src/lib/helpers/sentry/hook/useSentryIntegrations.ts new file mode 100644 index 0000000000..e5e1df0b02 --- /dev/null +++ b/agenta-web/src/lib/helpers/sentry/hook/useSentryIntegrations.ts @@ -0,0 +1,20 @@ +import {useCallback, useEffect, useRef} from "react" +import {dynamicLib} from "../../dynamic" +import {isDemo} from "../../utils" + +export const useSentryIntegrations = () => { + const isLoading = useRef(false) + + const initializeSentryIntegrations = useCallback(async () => { + const initSentry = await dynamicLib("helpers/sentry/lazyLoadSentryIntegrations") + initSentry?.lazyLoadSentryIntegrations() + }, []) + + useEffect(() => { + if (!isLoading.current && isDemo()) { + isLoading.current = true + + initializeSentryIntegrations() + } + }, []) +} diff --git a/agenta-web/src/pages/_app.tsx b/agenta-web/src/pages/_app.tsx index d47ae0bf67..7f99cda146 100644 --- a/agenta-web/src/pages/_app.tsx +++ b/agenta-web/src/pages/_app.tsx @@ -1,5 +1,4 @@ import type {AppProps} from "next/app" -import Head from "next/head" import dynamic from "next/dynamic" import "@/styles/globals.css" @@ -9,8 +8,12 @@ import ThemeContextProvider from "@/components/Layout/ThemeContextProvider" import AppContextProvider from "@/contexts/app.context" import ProfileContextProvider from "@/contexts/profile.context" import ProjectContextProvider from "@/contexts/project.context" +import AuthProvider from "@/lib/helpers/auth/AuthProvider" +import OrgWrapper from "@/components/OrgWrapper/OrgWrapper" +import GlobalScripts from "@/components/Scripts/GlobalScripts" import {Inter} from "next/font/google" import AgSWRConfig from "@/lib/api/SWRConfig" +import {useSentryIntegrations} from "@/lib/helpers/sentry/hook/useSentryIntegrations" const NoMobilePageWrapper = dynamicComponent("NoMobilePageWrapper/NoMobilePageWrapper") const CustomPosthogProvider = dynamic(() => import("@/lib/helpers/analytics/AgPosthogProvider")) @@ -21,31 +24,31 @@ const inter = Inter({ }) export default function App({Component, pageProps}: AppProps) { + useSentryIntegrations() + return ( <> - - Agenta: The LLMOps platform. - - + +
- - - - - - - - - - - - - + + + + + + + + + + + + + + + + +
diff --git a/agenta-web/src/services/observability/api/index.ts b/agenta-web/src/services/observability/api/index.ts deleted file mode 100644 index 30a1282c03..0000000000 --- a/agenta-web/src/services/observability/api/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -//Prefix convention: -// - fetch: GET single entity from server -// - fetchAll: GET all entities from server -// - create: POST data to server -// - update: PUT data to server -// - delete: DELETE data from server