From 30214e03025c6a5f4e4d47ec9e49e5517f89da09 Mon Sep 17 00:00:00 2001 From: Ravi Kumar L Date: Wed, 8 Jul 2026 16:20:41 +0200 Subject: [PATCH] feat(langfuse): refine tenant connection controls --- api/server/routes/config.js | 4 +- .../Integrations/LangfuseConnection.tsx | 443 +++++++++++++----- .../__tests__/LangfuseConnection.spec.tsx | 230 +++++++-- client/src/locales/en/translation.json | 15 +- .../api/src/admin/langfuse.handler.spec.ts | 159 +++++-- packages/api/src/admin/langfuse.ts | 115 +++-- .../__tests__/run-summarization.test.ts | 74 +-- packages/api/src/langfuse/config.ts | 19 +- packages/api/src/langfuse/destinations.ts | 4 +- packages/api/src/langfuse/feedback.spec.ts | 52 -- .../specs/config-schemas.spec.ts | 8 +- packages/data-provider/src/config.ts | 5 - packages/data-provider/src/types.ts | 14 +- 13 files changed, 777 insertions(+), 365 deletions(-) diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 189e6895b1..47c84f6fcd 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -277,7 +277,9 @@ router.get('/', async function (req, res) { conversationImportMaxFileSize: process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES ? parseInt(process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES, 10) : 0, - langfuseFanoutEnabled: /^(true|1)$/i.test((process.env.LANGFUSE_FANOUT_ENABLED ?? '').trim()), + langfuseFanoutEnabled: /^(true|1|yes|on)$/i.test( + (process.env.LANGFUSE_FANOUT_ENABLED ?? '').trim(), + ), ...(cloudFront ? { cloudFront } : {}), ...(rum ? { rum } : {}), }; diff --git a/client/src/components/Nav/SettingsTabs/Integrations/LangfuseConnection.tsx b/client/src/components/Nav/SettingsTabs/Integrations/LangfuseConnection.tsx index 49e1487595..26baa29b3b 100644 --- a/client/src/components/Nav/SettingsTabs/Integrations/LangfuseConnection.tsx +++ b/client/src/components/Nav/SettingsTabs/Integrations/LangfuseConnection.tsx @@ -1,6 +1,11 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Button, + CircleHelpIcon, + HoverCard, + HoverCardContent, + HoverCardPortal, + HoverCardTrigger, Input, Label, SecretInput, @@ -8,12 +13,62 @@ import { Switch, useToastContext, } from '@librechat/client'; +import type { TLangfuseConnectionStatus } from 'librechat-data-provider'; +import type { TranslationKeys } from '~/hooks'; import { useGetLangfuseConnectionQuery, useUpdateLangfuseConnectionMutation, useTestLangfuseConnectionMutation, } from '~/data-provider'; import { useLocalize } from '~/hooks'; +import { ESide } from '~/common'; + +type ConnectionTestState = 'idle' | 'checking' | 'connected' | 'failed'; + +function getStoredConnectionTestKey(status?: TLangfuseConnectionStatus): string | undefined { + if (status?.configured !== true || !status.destination || !status.publicKey) { + return undefined; + } + + return [status.destination, status.publicKey, status.updatedAt ?? ''].join('\u0000'); +} + +function getConnectionStatusLabelKey(state: ConnectionTestState): TranslationKeys { + switch (state) { + case 'checking': + return 'com_ui_langfuse_status_checking'; + case 'connected': + return 'com_ui_langfuse_status_connected'; + case 'failed': + return 'com_ui_langfuse_status_failed'; + case 'idle': + default: + return 'com_ui_langfuse_status_not_configured'; + } +} + +function getConnectionStatusDotClass(state: ConnectionTestState): string { + switch (state) { + case 'connected': + return 'bg-green-500'; + case 'failed': + return 'bg-red-500'; + case 'checking': + return 'bg-yellow-500'; + case 'idle': + default: + return 'border border-border-medium'; + } +} + +function getDisplayPublicKey(publicKey: string): string { + const trimmedPublicKey = publicKey.trim(); + if (trimmedPublicKey.length <= 12) { + return trimmedPublicKey; + } + + return `${trimmedPublicKey.slice(0, 6)}...${trimmedPublicKey.slice(-4)}`; +} export default function LangfuseConnection() { const localize = useLocalize(); @@ -22,153 +77,326 @@ export default function LangfuseConnection() { const updateMutation = useUpdateLangfuseConnectionMutation(); const testMutation = useTestLangfuseConnectionMutation(); + const [connectionStatus, setConnectionStatus] = useState(); const [enabled, setEnabled] = useState(false); - const [baseUrl, setBaseUrl] = useState(''); + const [destination, setDestination] = useState(''); const [publicKey, setPublicKey] = useState(''); const [secretKey, setSecretKey] = useState(''); + const [isEditingPublicKey, setIsEditingPublicKey] = useState(false); + const [isEditingSecret, setIsEditingSecret] = useState(false); + const [connectionTestState, setConnectionTestState] = useState('idle'); + const [connectionTestMessage, setConnectionTestMessage] = useState(''); + const autoTestedConnectionRef = useRef(); useEffect(() => { if (!status) { return; } - setEnabled(status.enabled === true); - setBaseUrl(status.baseUrl ?? ''); - setPublicKey(status.publicKey ?? ''); + setConnectionStatus(status); }, [status]); - const secretConfigured = status?.configured === true; - const trimmedBaseUrl = baseUrl.trim(); + useEffect(() => { + if (!connectionStatus) { + return; + } + setEnabled(connectionStatus.enabled === true); + const availableDestinations = connectionStatus.destinations ?? []; + const storedDestination = availableDestinations.some( + (option) => option.key === connectionStatus.destination, + ) + ? connectionStatus.destination + : undefined; + setDestination(storedDestination ?? ''); + setPublicKey(connectionStatus.publicKey ?? ''); + }, [connectionStatus]); + + const secretConfigured = connectionStatus?.configured === true; + const destinations = connectionStatus?.destinations ?? []; + const selectedDestination = destinations.find((option) => option.key === destination); const trimmedPublicKey = publicKey.trim(); const trimmedSecretKey = secretKey.trim(); + const publicKeyInputVisible = !secretConfigured || isEditingPublicKey; + const secretInputVisible = !secretConfigured || isEditingSecret; + const displayPublicKey = getDisplayPublicKey(publicKey); const canSubmit = - trimmedBaseUrl !== '' && - trimmedPublicKey !== '' && - (trimmedSecretKey !== '' || secretConfigured); + destination !== '' && + (!publicKeyInputVisible || trimmedPublicKey !== '') && + (!secretInputVisible || trimmedSecretKey !== ''); - const handleSave = () => { - updateMutation.mutate( + useEffect(() => { + const storedConnectionTestKey = getStoredConnectionTestKey(connectionStatus); + if (!connectionStatus || !storedConnectionTestKey) { + setConnectionTestState('idle'); + setConnectionTestMessage(''); + return; + } + + if (autoTestedConnectionRef.current === storedConnectionTestKey) { + return; + } + + autoTestedConnectionRef.current = storedConnectionTestKey; + setConnectionTestState('checking'); + testMutation.mutate( { - enabled, - baseUrl: trimmedBaseUrl, - publicKey: trimmedPublicKey, - ...(trimmedSecretKey ? { secretKey: trimmedSecretKey } : {}), + destination: connectionStatus.destination ?? '', + publicKey: connectionStatus.publicKey ?? '', }, { - onSuccess: () => { + onSuccess: (result) => { + setConnectionTestState(result.success ? 'connected' : 'failed'); + setConnectionTestMessage(result.success ? '' : (result.message ?? '')); + }, + onError: () => { + setConnectionTestState('failed'); + setConnectionTestMessage(localize('com_ui_langfuse_test_error')); + }, + }, + ); + }, [connectionStatus, localize, testMutation]); + + const connectionStatusLabel = + connectionTestState === 'failed' && connectionTestMessage !== '' + ? connectionTestMessage + : localize(getConnectionStatusLabelKey(connectionTestState)); + const connectionStatusDotClass = getConnectionStatusDotClass(connectionTestState); + const connectionStatusTextClass = + connectionTestState === 'failed' ? 'text-red-600 dark:text-red-400' : 'text-text-secondary'; + const connectionStatusTitle = + connectionTestState === 'failed' ? localize('com_ui_langfuse_status_failed_hover') : undefined; + + const handleSave = () => { + const payload = { + enabled, + destination, + publicKey: trimmedPublicKey, + ...(trimmedSecretKey ? { secretKey: trimmedSecretKey } : {}), + }; + + const saveConnection = () => { + updateMutation.mutate(payload, { + onSuccess: (nextStatus) => { + autoTestedConnectionRef.current = getStoredConnectionTestKey(nextStatus); + setConnectionStatus(nextStatus); + setConnectionTestState(enabled ? 'connected' : 'idle'); + setConnectionTestMessage(''); setSecretKey(''); + setIsEditingPublicKey(false); + setIsEditingSecret(false); showToast({ message: localize('com_ui_langfuse_saved'), status: 'success' }); }, onError: () => showToast({ message: localize('com_ui_langfuse_save_error'), status: 'error' }), - }, - ); - }; + }); + }; + + if (!enabled) { + saveConnection(); + return; + } - const handleTest = () => { testMutation.mutate( { - baseUrl: trimmedBaseUrl, + destination, publicKey: trimmedPublicKey, ...(trimmedSecretKey ? { secretKey: trimmedSecretKey } : {}), }, { - onSuccess: (result) => - showToast({ - message: result.success - ? localize('com_ui_langfuse_test_success') - : (result.message ?? localize('com_ui_langfuse_test_error')), - status: result.success ? 'success' : 'error', - }), - onError: () => - showToast({ message: localize('com_ui_langfuse_test_error'), status: 'error' }), + onSuccess: (result) => { + if (!result.success) { + setConnectionTestState('failed'); + setConnectionTestMessage(result.message ?? localize('com_ui_langfuse_test_error')); + showToast({ + message: result.message ?? localize('com_ui_langfuse_test_error'), + status: 'error', + }); + return; + } + + setConnectionTestState('connected'); + setConnectionTestMessage(''); + saveConnection(); + }, + onError: () => { + setConnectionTestState('failed'); + setConnectionTestMessage(localize('com_ui_langfuse_test_error')); + showToast({ message: localize('com_ui_langfuse_test_error'), status: 'error' }); + }, }, ); }; return (
-
-
-
- {localize('com_ui_langfuse_title')} -
-
- {localize('com_ui_langfuse_description')} -
-
- -
- -
- - setBaseUrl(e.target.value)} - /> -
- -
- - setPublicKey(e.target.value)} - /> -
- -
-
- -