🪢 feat: Langfuse Fanout Connection Setting (#14108)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions

* feat: encrypt tenant Langfuse secret in admin config

Add generic per-field secret encryption to the admin config layer: registered
secret paths (langfuse.secretKey) are encrypted with encryptV3 on write and a
non-secret fingerprint companion is stored. Admin config reads (base + per
principal) redact registered secrets so they are never returned; the fingerprint
is kept so the UI can show which key is configured.

The Langfuse fanout read path decrypts the tenant secret before export. Adds
secretKeyFingerprint to langfuseConfigSchema and tests for the encrypt/redact
policy.

* fix(api): secure admin config secret handling

* fix(api): preserve encrypted langfuse config secrets

* fix(api): couple config secret fingerprint deletion

* fix(api): read langfuse fanout collector url from env

* fix(api): display langfuse secret key hint

* fix(api): remove langfuse secret fingerprint breadcrumbs

* fix(api): use langfuse destination keys for tenant config

* fix(api): remove langfuse config compatibility fallbacks

* refactor(api): simplify langfuse secret helpers

* refactor(api): simplify langfuse config secret handling

* feat: in-app Langfuse connection settings panel

Add a discoverable, admin-gated Langfuse connection panel inside LibreChat
Settings (Dify-style): enable toggle, host, public key, masked write-only secret,
configured-key fingerprint, and a test-connection action. Backed by a dedicated
/api/admin/langfuse/connection endpoint that encrypts the secret at rest, returns
metadata plus fingerprint on read, and validates credentials. Builds on the
per-field encryption and fanout decrypt from the langfuse-config-encryption branch.

* refactor: align Langfuse secret field to CustomUserVars pattern

Use the established SecretInput plus Set/Unset state pill (com_ui_set/com_ui_unset)
from the MCP CustomUserVars UI for the saved-secret state, instead of a bespoke
masked input.

* fix: drop em dash from saved-secret placeholder

* feat: show loading state on Langfuse test connection button

* feat: gate in-app Langfuse settings on fanout config and admin role

* test: align Langfuse connection spec with SecretInput refactor

* feat(langfuse): refine tenant connection controls

* fix(admin): refine Langfuse connection verification

* fix(langfuse): refine tenant connection settings

* fix(langfuse): simplify export enablement controls

* fix(langfuse): validate tenant export configuration

* fix(langfuse): align startup fanout gate

* fix(admin): time out Langfuse verification

* fix(ui): rename Langfuse connection setting

* fix(admin): enforce Langfuse config capability

* feat(langfuse): require explicit tenant export activation

* feat(langfuse): support single-tenant connection settings

* fix(i18n): remove obsolete integrations label

* fix(langfuse): authenticate ingestion verification

* fix(langfuse): validate public key independently

* fix(langfuse): localize connection errors

* perf(config): skip Langfuse checks for non-admins

* fix(langfuse): preserve trace sampling for feedback

* test(langfuse): fix feedback sampling fixture

* fix(langfuse): align secret preview field

* fix(langfuse): harden connection settings state

* fix(langfuse): preserve trace destination state

* fix(langfuse): enforce tenant-wide routing invariants

* fix(langfuse): preserve verified connection invariants

* fix(langfuse): preserve stable project identity

* fix(langfuse): warm project identity asynchronously

---------

Co-authored-by: Ravi Kumar L <ravi.lazar@clickhouse.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Dustin Healy 2026-07-29 15:33:10 -07:00 committed by GitHub
parent 91adcf3f2c
commit af795be0c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 4356 additions and 527 deletions

View file

@ -64,7 +64,12 @@ export default function Content({ activeTab, query, ctx }: ContentProps) {
return null;
}
return (
<Section key={section.id} heading={localize(section.labelKey)} danger={section.danger}>
<Section
key={section.id}
heading={localize(section.labelKey)}
icon={section.icon}
danger={section.danger}
>
{entries.map((e) => {
const Cmp = e.Component;
return (

View file

@ -3,19 +3,21 @@ import { cn } from '~/utils';
interface SectionProps {
heading: string;
icon?: ReactNode;
danger?: boolean;
children: ReactNode;
}
export default function Section({ heading, danger, children }: SectionProps) {
export default function Section({ heading, icon, danger, children }: SectionProps) {
return (
<section className="mb-7">
<h3
className={cn(
'mb-2 px-1 text-xs font-semibold uppercase tracking-wide',
'mb-2 flex items-center gap-1.5 px-1 text-xs font-semibold uppercase tracking-wide',
danger ? 'text-red-500' : 'text-text-secondary',
)}
>
{icon}
{heading}
</h3>
<div

View file

@ -18,6 +18,7 @@ const ctx: SettingsContextValue = {
allowAccountDeletion: true,
aboutEnabled: false,
engineTTS: 'browser',
langfuseConnectionAccess: false,
};
function setup(extra: Partial<SettingsContextValue> = {}, query = '') {
@ -46,6 +47,16 @@ describe('Sidebar', () => {
expect(screen.getByText('About')).toBeInTheDocument();
});
it('shows the Langfuse tab when Langfuse is available to the user', () => {
setup({ langfuseConnectionAccess: true });
expect(screen.getByText('Langfuse')).toBeInTheDocument();
});
it('hides the Langfuse tab without Langfuse connection access', () => {
setup({ langfuseConnectionAccess: false });
expect(screen.queryByText('Langfuse')).not.toBeInTheDocument();
});
it('forwards typing to onQueryChange', async () => {
const { onQueryChange } = setup();
await userEvent.type(screen.getByRole('textbox'), 'theme');

View file

@ -1,10 +1,28 @@
import { isValidElementType } from 'react-is';
import { SettingsTabValues } from 'librechat-data-provider';
import type { SettingsContextValue } from '../types';
import en from '~/locales/en/translation.json';
import { registry } from '../registry';
import { TABS } from '../types';
const validTabSections = new Map(TABS.map((t) => [t.id, new Set(t.sections.map((s) => s.id))]));
const settingsContext: SettingsContextValue = {
balanceEnabled: false,
hasAnyPersonalizationFeature: false,
hasMemoryOptOut: false,
hasRemoteAgents: false,
hasUserProvidedEndpoints: false,
hasMultiConvo: false,
hasPrompts: false,
isLocalProvider: true,
twoFactorEnabled: false,
allowAccountDeletion: true,
aboutEnabled: false,
engineTTS: 'browser',
langfuseConnectionAccess: false,
};
describe('settings registry', () => {
it('has unique ids', () => {
const ids = registry.map((e) => e.id);
@ -30,4 +48,42 @@ describe('settings registry', () => {
expect(isValidElementType(entry.Component)).toBe(true);
}
});
describe('Langfuse connection visibility', () => {
const langfuseEntry = registry.find((entry) => entry.id === 'langfuseConnection');
it('places the connection in the Langfuse tab', () => {
expect(langfuseEntry).toMatchObject({
tab: SettingsTabValues.LANGFUSE,
section: 'langfuse',
});
});
it('shows the connection when the user can manage it', () => {
expect(
langfuseEntry?.show?.({
...settingsContext,
langfuseConnectionAccess: true,
}),
).toBe(true);
});
it('hides the connection without Langfuse config access', () => {
expect(
langfuseEntry?.show?.({
...settingsContext,
langfuseConnectionAccess: false,
}),
).toBe(false);
});
it('shows the connection in single-tenant mode without fanout', () => {
expect(
langfuseEntry?.show?.({
...settingsContext,
langfuseConnectionAccess: true,
}),
).toBe(true);
});
});
});

View file

@ -27,6 +27,7 @@ export function useSettingsContext(): SettingsContextValue {
});
const balanceEnabled = startupConfig?.balance?.enabled === true;
const langfuseConnectionAccess = startupConfig?.langfuseConnectionAccess === true;
const isLocalProvider = user?.provider === 'local';
const twoFactorEnabled = user?.twoFactorEnabled === true;
const allowAccountDeletion = startupConfig?.allowAccountDeletion !== false;
@ -51,6 +52,7 @@ export function useSettingsContext(): SettingsContextValue {
allowAccountDeletion,
aboutEnabled,
engineTTS,
langfuseConnectionAccess,
}),
[
balanceEnabled,
@ -65,6 +67,7 @@ export function useSettingsContext(): SettingsContextValue {
allowAccountDeletion,
aboutEnabled,
engineTTS,
langfuseConnectionAccess,
],
);
}

View file

@ -18,6 +18,7 @@ import {
import DisplayUsernameMessages from '../SettingsTabs/Account/DisplayUsernameMessages';
import ConversationModeSwitch from '../SettingsTabs/Speech/ConversationModeSwitch';
import EnableTwoFactorItem from '../SettingsTabs/Account/TwoFactorAuthentication';
import LangfuseConnection from '../SettingsTabs/Integrations/LangfuseConnection';
import ImportConversations from '../SettingsTabs/Data/ImportConversations';
import { toggleControl, ThemeSetting, LangSetting } from './controls';
import BackupCodesItem from '../SettingsTabs/Account/BackupCodesItem';
@ -500,6 +501,16 @@ export const registry: SettingEntry[] = [
labelKey: 'com_ui_settings_label_revoke_keys',
Component: RevokeKeys,
},
// Langfuse
{
id: 'langfuseConnection',
tab: SettingsTabValues.LANGFUSE,
section: 'langfuse',
labelKey: 'com_ui_langfuse_title',
keywords: ['langfuse', 'observability', 'tracing', 'telemetry', 'traces'],
show: (ctx) => ctx.langfuseConnectionAccess,
Component: LangfuseConnection,
},
// Data controls · Danger zone
{
id: 'deleteCache',

View file

@ -9,6 +9,7 @@ export type SettingsTab =
| SettingsTabValues.GENERAL
| SettingsTabValues.CHAT
| SettingsTabValues.SPEECH
| SettingsTabValues.LANGFUSE
| SettingsTabValues.DATA
| SettingsTabValues.ACCOUNT
| SettingsTabValues.ABOUT;
@ -27,6 +28,7 @@ export type SectionId =
| 'memory'
| 'data'
| 'apiKeys'
| 'langfuse'
| 'danger'
| 'profile'
| 'security'
@ -46,6 +48,7 @@ export interface SettingsContextValue {
allowAccountDeletion: boolean;
aboutEnabled: boolean;
engineTTS: string;
langfuseConnectionAccess: boolean;
}
export interface SettingEntry {
@ -61,6 +64,7 @@ export interface SettingEntry {
export interface SectionMeta {
id: SectionId;
labelKey: TranslationKeys;
icon?: ReactNode;
danger?: boolean;
}
@ -72,6 +76,17 @@ export interface TabMeta {
show?: (ctx: SettingsContextValue) => boolean;
}
function createLangfuseIcon(className: string): ReactNode {
return createElement('span', {
className: `${className} inline-block shrink-0 bg-current`,
'aria-hidden': true,
style: {
WebkitMask: 'url(/assets/langfuse-icon-monochrome.svg) center / contain no-repeat',
mask: 'url(/assets/langfuse-icon-monochrome.svg) center / contain no-repeat',
},
});
}
export const TABS: TabMeta[] = [
{
id: SettingsTabValues.GENERAL,
@ -104,6 +119,19 @@ export const TABS: TabMeta[] = [
{ id: 'tts', labelKey: 'com_ui_settings_section_tts' },
],
},
{
id: SettingsTabValues.LANGFUSE,
labelKey: 'com_ui_settings_tab_langfuse',
icon: createLangfuseIcon('h-4 w-4'),
sections: [
{
id: 'langfuse',
labelKey: 'com_ui_settings_section_langfuse',
icon: createLangfuseIcon('h-3.5 w-3.5'),
},
],
show: (ctx) => ctx.langfuseConnectionAccess,
},
{
id: SettingsTabValues.DATA,
labelKey: 'com_ui_settings_tab_data',

View file

@ -0,0 +1,613 @@
import { useState, useEffect, useRef } from 'react';
import {
Button,
CircleHelpIcon,
Dropdown,
HoverCard,
HoverCardContent,
HoverCardPortal,
HoverCardTrigger,
Input,
Label,
SecretInput,
Spinner,
useToastContext,
} from '@librechat/client';
import type {
TLangfuseConnectionStatus,
TLangfuseConnectionTestErrorCode,
} 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' | 'unverified' | 'checking' | 'connected' | 'failed';
function getStoredConnectionTestKey(status?: TLangfuseConnectionStatus): string | undefined {
if (status?.configured !== true || !status.destination || !status.publicKey) {
return undefined;
}
return [status.destination, status.publicKey].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 'unverified':
return 'com_ui_langfuse_status_not_verified';
case 'idle':
default:
return 'com_ui_langfuse_status_not_configured';
}
}
function getConnectionTestErrorLabelKey(
errorCode?: TLangfuseConnectionTestErrorCode,
): TranslationKeys {
switch (errorCode) {
case 'invalid_credentials':
return 'com_ui_langfuse_test_invalid_credentials';
case 'access_denied':
return 'com_ui_langfuse_test_access_denied';
case 'rate_limited':
return 'com_ui_langfuse_test_rate_limited';
case 'server_error':
return 'com_ui_langfuse_test_server_error';
case 'timeout':
return 'com_ui_langfuse_test_timeout';
case 'missing_secret':
return 'com_ui_langfuse_test_missing_secret';
case 'stored_secret_unavailable':
return 'com_ui_langfuse_test_stored_secret_unavailable';
case 'unexpected_response':
return 'com_ui_langfuse_test_unexpected_response';
case 'unreachable':
default:
return 'com_ui_langfuse_test_error';
}
}
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();
const { showToast } = useToastContext();
const {
data: status,
isLoading: isConnectionLoading,
isError: isConnectionError,
isFetching: isConnectionFetching,
refetch: refetchConnection,
} = useGetLangfuseConnectionQuery();
const updateMutation = useUpdateLangfuseConnectionMutation();
const testMutation = useTestLangfuseConnectionMutation();
const [connectionStatus, setConnectionStatus] = useState<TLangfuseConnectionStatus>();
const [destination, setDestination] = useState('');
const [publicKey, setPublicKey] = useState('');
const [secretKey, setSecretKey] = useState('');
const [isEditingPublicKey, setIsEditingPublicKey] = useState(false);
const [isEditingSecretKey, setIsEditingSecretKey] = useState(false);
const [connectionTestState, setConnectionTestState] = useState<ConnectionTestState>('idle');
const [connectionTestMessage, setConnectionTestMessage] = useState('');
const autoTestedConnectionRef = useRef<string>();
const connectionTestRequestRef = useRef(0);
const publicKeyInputRef = useRef<HTMLInputElement>(null);
const secretKeyInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isEditingPublicKey) {
publicKeyInputRef.current?.focus();
}
}, [isEditingPublicKey]);
useEffect(() => {
if (isEditingSecretKey) {
secretKeyInputRef.current?.focus();
}
}, [isEditingSecretKey]);
useEffect(() => {
if (!status) {
return;
}
setConnectionStatus(status);
}, [status]);
useEffect(() => {
if (!connectionStatus) {
return;
}
setDestination(connectionStatus.destination ?? '');
setPublicKey(connectionStatus.publicKey ?? '');
}, [connectionStatus]);
const secretConfigured = connectionStatus?.configured === true;
const destinations = connectionStatus?.destinations ?? [];
const connectionDestinationAvailable = destinations.some(
({ key }) => key === connectionStatus?.destination,
);
const storedDestinationUnavailable =
secretConfigured && Boolean(connectionStatus?.destination) && !connectionDestinationAvailable;
const destinationOptions = [
...(storedDestinationUnavailable && connectionStatus?.destination
? [
{
value: connectionStatus.destination,
label: `${connectionStatus.destination} - ${localize(
'com_ui_langfuse_destination_unavailable',
)}`,
},
]
: []),
...destinations.map(({ key, baseUrl }) => ({
value: key,
label: `${key} - ${baseUrl}`,
})),
];
const trimmedPublicKey = publicKey.trim();
const trimmedSecretKey = secretKey.trim();
const publicKeyInputVisible = !secretConfigured || isEditingPublicKey;
const secretInputVisible = !secretConfigured || isEditingSecretKey;
const displayPublicKey = getDisplayPublicKey(publicKey);
const connectionCredentialsChanged =
destination !== (connectionStatus?.destination ?? '') ||
trimmedPublicKey !== (connectionStatus?.publicKey ?? '');
const hasUnsavedChanges = connectionCredentialsChanged || trimmedSecretKey !== '';
const isEditing =
!secretConfigured || isEditingPublicKey || isEditingSecretKey || hasUnsavedChanges;
const canSubmit =
destination !== '' &&
trimmedPublicKey !== '' &&
((!connectionCredentialsChanged && secretConfigured) || trimmedSecretKey !== '');
const busy = testMutation.isLoading || updateMutation.isLoading;
useEffect(() => {
const storedConnectionTestKey = getStoredConnectionTestKey(connectionStatus);
if (!connectionStatus) {
return;
}
if (!storedConnectionTestKey) {
return;
}
if (!connectionStatus.destinations?.some(({ key }) => key === connectionStatus.destination)) {
connectionTestRequestRef.current += 1;
setConnectionTestState('failed');
setConnectionTestMessage(localize('com_ui_langfuse_destination_removed'));
return;
}
if (autoTestedConnectionRef.current === storedConnectionTestKey) {
return;
}
autoTestedConnectionRef.current = storedConnectionTestKey;
const requestId = ++connectionTestRequestRef.current;
setConnectionTestState('checking');
testMutation.mutate(
{
destination: connectionStatus.destination ?? '',
publicKey: connectionStatus.publicKey ?? '',
},
{
onSuccess: (result) => {
if (requestId !== connectionTestRequestRef.current) {
return;
}
setConnectionTestState(result.success ? 'connected' : 'failed');
setConnectionTestMessage(
result.success ? '' : localize(getConnectionTestErrorLabelKey(result.errorCode)),
);
},
onError: () => {
if (requestId !== connectionTestRequestRef.current) {
return;
}
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: true,
destination,
publicKey: trimmedPublicKey,
...(trimmedSecretKey ? { secretKey: trimmedSecretKey } : {}),
};
connectionTestRequestRef.current += 1;
updateMutation.mutate(payload, {
onSuccess: (nextStatus) => {
autoTestedConnectionRef.current = getStoredConnectionTestKey(nextStatus);
setConnectionStatus(nextStatus);
setConnectionTestState('connected');
setConnectionTestMessage('');
setSecretKey('');
setIsEditingPublicKey(false);
setIsEditingSecretKey(false);
showToast({ message: localize('com_ui_langfuse_saved'), status: 'success' });
},
onError: () => {
setConnectionTestState('failed');
setConnectionTestMessage(localize('com_ui_langfuse_save_error'));
showToast({ message: localize('com_ui_langfuse_save_error'), status: 'error' });
},
});
};
const handleCancel = () => {
const storedDestination = connectionStatus?.destination;
setDestination(storedDestination ?? '');
setPublicKey(connectionStatus?.publicKey ?? '');
setSecretKey('');
setIsEditingPublicKey(false);
setIsEditingSecretKey(false);
if (!storedDestination || !connectionStatus?.publicKey) {
setConnectionTestState('idle');
setConnectionTestMessage('');
return;
}
if (!connectionStatus.destinations?.some(({ key }) => key === storedDestination)) {
connectionTestRequestRef.current += 1;
setConnectionTestState('failed');
setConnectionTestMessage(localize('com_ui_langfuse_destination_removed'));
return;
}
const requestId = ++connectionTestRequestRef.current;
setConnectionTestState('checking');
setConnectionTestMessage('');
testMutation.mutate(
{ destination: storedDestination, publicKey: connectionStatus.publicKey },
{
onSuccess: (result) => {
if (requestId !== connectionTestRequestRef.current) return;
setConnectionTestState(result.success ? 'connected' : 'failed');
setConnectionTestMessage(
result.success ? '' : localize(getConnectionTestErrorLabelKey(result.errorCode)),
);
},
onError: () => {
if (requestId !== connectionTestRequestRef.current) return;
setConnectionTestState('failed');
setConnectionTestMessage(localize('com_ui_langfuse_test_error'));
},
},
);
};
const handleDestinationChange = (nextDestination: string) => {
setDestination(nextDestination);
const requestId = ++connectionTestRequestRef.current;
const credentialsChanged =
nextDestination !== (connectionStatus?.destination ?? '') ||
trimmedPublicKey !== (connectionStatus?.publicKey ?? '');
if (secretConfigured && credentialsChanged) {
setIsEditingSecretKey(true);
}
if (
nextDestination === '' ||
trimmedPublicKey === '' ||
((!secretConfigured || credentialsChanged) && trimmedSecretKey === '')
) {
setConnectionTestState(credentialsChanged ? 'unverified' : 'idle');
setConnectionTestMessage('');
return;
}
setConnectionTestState('checking');
setConnectionTestMessage('');
testMutation.mutate(
{
destination: nextDestination,
publicKey: trimmedPublicKey,
...(trimmedSecretKey ? { secretKey: trimmedSecretKey } : {}),
},
{
onSuccess: (result) => {
if (requestId !== connectionTestRequestRef.current) {
return;
}
setConnectionTestState(result.success ? 'connected' : 'failed');
setConnectionTestMessage(
result.success ? '' : localize(getConnectionTestErrorLabelKey(result.errorCode)),
);
},
onError: () => {
if (requestId !== connectionTestRequestRef.current) {
return;
}
setConnectionTestState('failed');
setConnectionTestMessage(localize('com_ui_langfuse_test_error'));
},
},
);
};
const handleEnabledChange = () => {
if (!secretConfigured || !connectionStatus?.destination || !connectionStatus.publicKey) {
return;
}
const nextEnabled = connectionStatus.enabled !== true;
const requestId = ++connectionTestRequestRef.current;
const saveEnabledState = () => {
updateMutation.mutate(
{
enabled: nextEnabled,
destination: connectionStatus.destination ?? '',
publicKey: connectionStatus.publicKey ?? '',
},
{
onSuccess: (nextStatus) => {
if (requestId !== connectionTestRequestRef.current) {
return;
}
autoTestedConnectionRef.current = getStoredConnectionTestKey(nextStatus);
setConnectionStatus(nextStatus);
showToast({ message: localize('com_ui_langfuse_saved'), status: 'success' });
},
onError: () => {
if (requestId !== connectionTestRequestRef.current) {
return;
}
showToast({ message: localize('com_ui_langfuse_save_error'), status: 'error' });
},
},
);
};
saveEnabledState();
};
if (isConnectionLoading && connectionStatus == null) {
return (
<div
data-testid="langfuse-connection-loading"
className="flex items-center justify-center rounded-xl border border-border-light py-12"
>
<Spinner className="h-6 w-6 text-text-secondary" />
<span className="sr-only">{localize('com_ui_loading')}</span>
</div>
);
}
if (isConnectionError && connectionStatus == null) {
return (
<div className="flex flex-col items-center gap-3 rounded-xl border border-border-light px-6 py-10 text-center">
<p className="text-sm text-text-secondary">{localize('com_ui_langfuse_load_error')}</p>
<Button
variant="outline"
size="sm"
onClick={() => refetchConnection()}
disabled={isConnectionFetching}
>
{localize('com_ui_retry')}
</Button>
</div>
);
}
return (
<div className="flex flex-col gap-4">
<HoverCard openDelay={50}>
<div className="flex flex-col gap-2">
<div className="flex items-start gap-4">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<div className="font-medium">{localize('com_ui_langfuse_title')}</div>
<div className="rounded-full border border-purple-600/40 bg-purple-500/10 px-2 py-0.5 text-xs font-medium text-purple-700 hover:bg-purple-700/10 dark:text-purple-400">
{localize('com_ui_beta')}
</div>
<HoverCardTrigger>
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger>
</div>
<div className="mt-1 max-w-md text-xs text-text-secondary">
{localize('com_ui_langfuse_description')}
</div>
</div>
</div>
<div
className={`flex items-center gap-1.5 text-xs ${connectionStatusTextClass}`}
aria-live="polite"
title={connectionStatusTitle}
>
{connectionTestState === 'checking' ? (
<Spinner className="h-3 w-3" />
) : (
<span className={`h-2 w-2 rounded-full ${connectionStatusDotClass}`} />
)}
<span>{connectionStatusLabel}</span>
</div>
</div>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<p className="text-sm text-text-secondary">{localize('com_ui_langfuse_beta_info')}</p>
</HoverCardContent>
</HoverCardPortal>
</HoverCard>
<div className="flex flex-col gap-1.5">
<Label id="langfuse-destination-label">{localize('com_ui_langfuse_destination')}</Label>
<Dropdown
value={destination}
label={destination === '' ? localize('com_ui_select') : ''}
onChange={handleDestinationChange}
options={destinationOptions}
disabled={destinations.length === 0 || busy}
className="w-full"
sizeClasses="z-50 w-[var(--popover-anchor-width)]"
testId="langfuse-destination"
aria-labelledby="langfuse-destination-label"
/>
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="langfuse-public-token">{localize('com_ui_langfuse_public_key')}</Label>
{secretConfigured && !isEditingPublicKey && (
<button
type="button"
className="w-full rounded-lg border border-border-light px-3 py-2 text-left hover:border-border-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
aria-label={`${localize('com_ui_edit')} ${localize('com_ui_langfuse_public_key')}`}
disabled={busy}
onClick={() => setIsEditingPublicKey(true)}
>
<code className="block min-w-0 truncate font-mono text-sm text-text-primary">
{displayPublicKey}
</code>
</button>
)}
{publicKeyInputVisible && (
<Input
ref={publicKeyInputRef}
id="langfuse-public-token"
autoComplete="off"
data-lpignore="true"
data-1p-ignore="true"
data-bwignore="true"
data-form-type="other"
value={publicKey}
disabled={busy}
placeholder="pk-lf-..."
onChange={(e) => {
connectionTestRequestRef.current += 1;
const nextPublicKey = e.target.value;
setPublicKey(nextPublicKey);
if (
secretConfigured &&
nextPublicKey.trim() !== (connectionStatus?.publicKey ?? '')
) {
setIsEditingSecretKey(true);
}
setConnectionTestState('unverified');
setConnectionTestMessage('');
}}
/>
)}
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="langfuse-private-token">{localize('com_ui_langfuse_secret_key')}</Label>
{secretConfigured && !isEditingSecretKey && (
<button
type="button"
className="w-full rounded-lg border border-border-light px-3 py-2 text-left hover:border-border-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
aria-label={`${localize('com_ui_edit')} ${localize('com_ui_langfuse_secret_key')}`}
disabled={busy}
onClick={() => setIsEditingSecretKey(true)}
>
<code className="block min-w-0 truncate font-mono text-sm text-text-primary">
{connectionStatus?.secretKeyPreview}
</code>
</button>
)}
{secretInputVisible && (
<SecretInput
ref={secretKeyInputRef}
id="langfuse-private-token"
autoComplete="off"
data-lpignore="true"
data-1p-ignore="true"
data-bwignore="true"
data-form-type="other"
value={secretKey}
disabled={busy}
placeholder="sk-lf-..."
onChange={(e) => {
connectionTestRequestRef.current += 1;
setSecretKey(e.target.value);
setConnectionTestState('unverified');
setConnectionTestMessage('');
}}
/>
)}
</div>
<div className="flex min-h-9 items-center justify-end gap-2">
{isEditing ? (
<>
<Button variant="outline" disabled={busy} onClick={handleCancel}>
{localize('com_ui_cancel')}
</Button>
<Button disabled={!canSubmit || busy} onClick={handleSave}>
{testMutation.isLoading ? (
<span className="flex items-center gap-2">
<Spinner className="h-4 w-4" />
{localize('com_ui_langfuse_testing')}
</span>
) : (
localize('com_ui_langfuse_save_and_enable')
)}
</Button>
</>
) : (
<Button
variant={connectionStatus?.enabled === true ? 'outline' : 'submit'}
disabled={
busy || (connectionStatus?.enabled !== true && !connectionDestinationAvailable)
}
onClick={handleEnabledChange}
>
{localize(
connectionStatus?.enabled === true
? 'com_ui_langfuse_disable'
: 'com_ui_langfuse_enable',
)}
</Button>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,619 @@
import userEvent from '@testing-library/user-event';
import { act, render, screen, fireEvent, waitFor } from '@testing-library/react';
import LangfuseConnection from '../LangfuseConnection';
const mockGet = jest.fn();
const mockUpdate = jest.fn();
const mockTest = jest.fn();
const mockRefetch = jest.fn();
const destinationLabels = {
eu: 'eu - https://cloud.langfuse.com',
us: 'us - https://us.cloud.langfuse.com',
};
async function selectDestination(destination: keyof typeof destinationLabels) {
await userEvent.click(screen.getByTestId('langfuse-destination'));
await userEvent.click(screen.getByRole('option', { name: destinationLabels[destination] }));
}
jest.mock('~/data-provider', () => ({
useGetLangfuseConnectionQuery: () => mockGet(),
useUpdateLangfuseConnectionMutation: () => ({ mutate: mockUpdate, isLoading: false }),
useTestLangfuseConnectionMutation: () => ({ mutate: mockTest, isLoading: false }),
}));
jest.mock('~/hooks', () => ({
useLocalize: () => (key: string) => key,
}));
jest.mock('@librechat/client', () => ({
...jest.requireActual('@librechat/client'),
useToastContext: () => ({ showToast: jest.fn() }),
}));
beforeEach(() => {
global.ResizeObserver = class MockedResizeObserver {
observe = jest.fn();
unobserve = jest.fn();
disconnect = jest.fn();
};
mockGet.mockReset();
mockUpdate.mockReset();
mockTest.mockReset();
mockRefetch.mockReset();
mockTest.mockImplementation((_payload, options) => {
options?.onSuccess?.({ success: true });
});
mockGet.mockReturnValue({
isLoading: false,
isError: false,
isFetching: false,
refetch: mockRefetch,
data: {
configured: false,
enabled: false,
destinations: [
{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' },
{ key: 'us', baseUrl: 'https://us.cloud.langfuse.com' },
],
},
});
});
describe('LangfuseConnection', () => {
it('renders the connection form fields', () => {
render(<LangfuseConnection />);
expect(screen.getByTestId('langfuse-destination')).toHaveTextContent('com_ui_select');
expect(screen.getByLabelText('com_ui_langfuse_public_key')).toHaveAttribute(
'data-lpignore',
'true',
);
expect(screen.getByLabelText('com_ui_langfuse_public_key')).toHaveAttribute(
'data-1p-ignore',
'true',
);
expect(screen.getByLabelText('com_ui_langfuse_public_key')).toHaveAttribute(
'data-form-type',
'other',
);
expect(screen.getByLabelText('com_ui_langfuse_public_key')).toHaveAttribute(
'data-bwignore',
'true',
);
expect(screen.getByLabelText(/com_ui_langfuse_secret_key/)).toHaveAttribute(
'data-lpignore',
'true',
);
expect(screen.getByLabelText(/com_ui_langfuse_secret_key/)).toHaveAttribute(
'data-1p-ignore',
'true',
);
expect(screen.getByLabelText(/com_ui_langfuse_secret_key/)).toHaveAttribute(
'data-form-type',
'other',
);
expect(screen.getByLabelText(/com_ui_langfuse_secret_key/)).toHaveAttribute(
'data-bwignore',
'true',
);
expect(screen.getByLabelText(/com_ui_langfuse_secret_key/)).toHaveAttribute(
'autocomplete',
'off',
);
expect(screen.getByLabelText(/com_ui_langfuse_secret_key/)).toHaveAttribute('type', 'password');
expect(screen.getByRole('button', { name: 'Show secret' })).toBeInTheDocument();
expect(screen.queryByText('com_ui_langfuse_test')).not.toBeInTheDocument();
expect(screen.getByText('com_ui_langfuse_status_not_configured')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'com_ui_cancel' })).toBeVisible();
expect(screen.getByRole('button', { name: 'com_ui_langfuse_save_and_enable' })).toBeVisible();
expect(
screen.queryByRole('button', { name: 'com_ui_langfuse_enable' }),
).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: 'com_ui_langfuse_disable' }),
).not.toBeInTheDocument();
expect(mockTest).not.toHaveBeenCalled();
});
it('renders a loading state while the stored connection is loading', () => {
mockGet.mockReturnValue({
data: undefined,
isLoading: true,
isError: false,
isFetching: true,
refetch: mockRefetch,
});
render(<LangfuseConnection />);
expect(screen.getByTestId('langfuse-connection-loading')).toBeVisible();
expect(screen.getByText('com_ui_loading')).toBeInTheDocument();
expect(screen.queryByText('com_ui_langfuse_status_not_configured')).not.toBeInTheDocument();
});
it('renders a retryable error when the stored connection cannot be loaded', async () => {
mockGet.mockReturnValue({
data: undefined,
isLoading: false,
isError: true,
isFetching: false,
refetch: mockRefetch,
});
render(<LangfuseConnection />);
expect(screen.getByText('com_ui_langfuse_load_error')).toBeVisible();
expect(screen.queryByText('com_ui_langfuse_status_not_configured')).not.toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'com_ui_retry' }));
expect(mockRefetch).toHaveBeenCalledTimes(1);
});
it.each([
{
credential: 'public key',
editButton: 'com_ui_edit com_ui_langfuse_public_key',
inputLabel: 'com_ui_langfuse_public_key',
value: 'pk-lf-updated',
},
{
credential: 'secret key',
editButton: 'com_ui_edit com_ui_langfuse_secret_key',
inputLabel: 'com_ui_langfuse_secret_key',
value: 'sk-lf-updated',
},
])(
'keeps edited $credential unverified when an earlier automatic test completes',
async ({ editButton, inputLabel, value }) => {
let completeTest: ((result: { success: boolean }) => void) | undefined;
mockTest.mockImplementation((_payload, options) => {
completeTest = options?.onSuccess;
});
mockGet.mockReturnValue({
data: {
configured: true,
enabled: true,
destinations: [{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' }],
destination: 'eu',
publicKey: 'pk-lf-original',
secretKeyPreview: 'sk-lf-...inal',
},
});
render(<LangfuseConnection />);
await waitFor(() => expect(mockTest).toHaveBeenCalledTimes(1));
await userEvent.click(screen.getByRole('button', { name: editButton }));
fireEvent.change(screen.getByLabelText(inputLabel), { target: { value } });
expect(screen.getByText('com_ui_langfuse_status_not_verified')).toBeVisible();
act(() => completeTest?.({ success: true }));
expect(screen.getByText('com_ui_langfuse_status_not_verified')).toBeVisible();
expect(screen.queryByText('com_ui_langfuse_status_connected')).not.toBeInTheDocument();
},
);
it('prefills stored values, tests on load, and keeps destination editable', async () => {
mockGet.mockReturnValue({
data: {
configured: true,
enabled: true,
destinations: [
{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' },
{ key: 'us', baseUrl: 'https://us.cloud.langfuse.com' },
],
destination: 'us',
publicKey: 'pk-lf-12345678-515f',
secretKeyPreview: 'sk-lf-...515f',
},
});
render(<LangfuseConnection />);
expect(screen.getByTestId('langfuse-destination')).toHaveTextContent(destinationLabels.us);
expect(screen.queryByLabelText('com_ui_langfuse_public_key')).not.toBeInTheDocument();
expect(screen.getByText('pk-lf-...515f')).toBeInTheDocument();
expect(screen.queryByLabelText('com_ui_langfuse_secret_key')).not.toBeInTheDocument();
expect(screen.getByText('sk-lf-...515f')).toBeInTheDocument();
expect(screen.queryByText('com_ui_langfuse_save_and_enable')).not.toBeInTheDocument();
expect(screen.getByTestId('langfuse-destination')).toBeEnabled();
expect(screen.getByRole('button', { name: 'com_ui_langfuse_disable' })).toBeEnabled();
await waitFor(() => expect(mockTest).toHaveBeenCalledTimes(1));
expect(mockTest.mock.calls[0][0]).toEqual({
destination: 'us',
publicKey: 'pk-lf-12345678-515f',
});
expect(screen.getByText('com_ui_langfuse_status_connected')).toBeInTheDocument();
});
it('shows a failed saved-connection status when the load-time test fails', async () => {
mockTest.mockImplementation((_payload, options) => {
options?.onSuccess?.({ success: false, errorCode: 'invalid_credentials' });
});
mockGet.mockReturnValue({
data: {
configured: true,
enabled: true,
destinations: [{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' }],
destination: 'eu',
publicKey: 'pk-lf-1',
secretKeyPreview: 'sk-lf-...515f',
},
});
render(<LangfuseConnection />);
await waitFor(() =>
expect(screen.getByText('com_ui_langfuse_test_invalid_credentials')).toBeInTheDocument(),
);
expect(
screen.getByText('com_ui_langfuse_test_invalid_credentials').closest('div'),
).toHaveAttribute('title', 'com_ui_langfuse_status_failed_hover');
});
it('saves the typed secret key without a duplicate preflight test', async () => {
render(<LangfuseConnection />);
await selectDestination('us');
fireEvent.change(screen.getByLabelText('com_ui_langfuse_public_key'), {
target: { value: 'pk-lf-1' },
});
fireEvent.change(screen.getByLabelText(/com_ui_langfuse_secret_key/), {
target: { value: 'sk-lf-secret' },
});
await userEvent.click(screen.getByText('com_ui_langfuse_save_and_enable'));
expect(mockTest).not.toHaveBeenCalled();
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(mockUpdate.mock.calls[0][0]).toEqual({
enabled: true,
destination: 'us',
publicKey: 'pk-lf-1',
secretKey: 'sk-lf-secret',
});
});
it('shows the display secret key immediately after saving a new connection', async () => {
mockUpdate.mockImplementation((_payload, options) => {
options?.onSuccess?.({
configured: true,
enabled: true,
destinations: [
{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' },
{ key: 'us', baseUrl: 'https://us.cloud.langfuse.com' },
],
destination: 'us',
publicKey: 'pk-lf-1',
secretKeyPreview: 'sk-lf-...cret',
});
});
render(<LangfuseConnection />);
await selectDestination('us');
fireEvent.change(screen.getByLabelText('com_ui_langfuse_public_key'), {
target: { value: 'pk-lf-1' },
});
fireEvent.change(screen.getByLabelText(/com_ui_langfuse_secret_key/), {
target: { value: 'sk-lf-secret' },
});
await userEvent.click(screen.getByText('com_ui_langfuse_save_and_enable'));
expect(mockTest).not.toHaveBeenCalled();
expect(screen.queryByLabelText('com_ui_langfuse_secret_key')).not.toBeInTheDocument();
expect(screen.getByText('sk-lf-...cret')).toBeInTheDocument();
});
it('requires secret re-entry before saving a destination change', async () => {
mockGet.mockReturnValue({
data: {
configured: true,
enabled: true,
destinations: [
{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' },
{ key: 'us', baseUrl: 'https://us.cloud.langfuse.com' },
],
destination: 'eu',
publicKey: 'pk-lf-1',
secretKeyPreview: 'sk-lf-...515f',
},
});
render(<LangfuseConnection />);
await waitFor(() => expect(mockTest).toHaveBeenCalledTimes(1));
mockTest.mockClear();
await selectDestination('us');
expect(mockTest).not.toHaveBeenCalled();
expect(screen.getByLabelText(/com_ui_langfuse_secret_key/)).toBeVisible();
expect(screen.getByText('com_ui_langfuse_status_not_verified')).toBeInTheDocument();
expect(screen.getByText('com_ui_langfuse_save_and_enable')).toBeDisabled();
fireEvent.change(screen.getByLabelText(/com_ui_langfuse_secret_key/), {
target: { value: 'sk-lf-replacement' },
});
await userEvent.click(screen.getByText('com_ui_langfuse_save_and_enable'));
expect(mockTest).not.toHaveBeenCalled();
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(mockUpdate.mock.calls[0][0]).toMatchObject({
destination: 'us',
publicKey: 'pk-lf-1',
secretKey: 'sk-lf-replacement',
});
});
it('opens each configured key independently when its masked value is clicked', async () => {
mockGet.mockReturnValue({
data: {
configured: true,
enabled: true,
destinations: [{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' }],
destination: 'eu',
publicKey: 'pk-lf-1',
secretKeyPreview: 'sk-lf-...515f',
},
});
render(<LangfuseConnection />);
await waitFor(() => expect(mockTest).toHaveBeenCalledTimes(1));
mockTest.mockClear();
expect(screen.queryByLabelText('com_ui_langfuse_secret_key')).not.toBeInTheDocument();
await userEvent.click(
screen.getByRole('button', {
name: 'com_ui_edit com_ui_langfuse_public_key',
}),
);
expect(screen.getByRole('button', { name: 'com_ui_cancel' })).toBeVisible();
expect(screen.getByRole('button', { name: 'com_ui_langfuse_save_and_enable' })).toBeVisible();
expect(
screen.queryByRole('button', { name: 'com_ui_langfuse_disable' }),
).not.toBeInTheDocument();
expect(screen.getByLabelText('com_ui_langfuse_public_key')).toHaveValue('pk-lf-1');
expect(screen.getByLabelText('com_ui_langfuse_public_key')).toHaveFocus();
expect(
screen.queryByLabelText(/com_ui_langfuse_secret_key/, { selector: 'input' }),
).not.toBeInTheDocument();
await userEvent.click(
screen.getByRole('button', {
name: 'com_ui_edit com_ui_langfuse_secret_key',
}),
);
const secretKeyInput = screen.getByLabelText(/com_ui_langfuse_secret_key/);
expect(secretKeyInput).toHaveValue('');
expect(secretKeyInput).toHaveClass('w-full');
expect(secretKeyInput).toHaveFocus();
fireEvent.change(secretKeyInput, {
target: { value: 'sk-lf-replacement' },
});
await userEvent.click(screen.getByText('com_ui_langfuse_save_and_enable'));
expect(mockTest).not.toHaveBeenCalled();
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(mockUpdate.mock.calls[0][0]).toMatchObject({
destination: 'eu',
publicKey: 'pk-lf-1',
secretKey: 'sk-lf-replacement',
});
});
it('restores the stored connection when editing is cancelled', async () => {
mockGet.mockReturnValue({
data: {
configured: true,
enabled: true,
destinations: [
{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' },
{ key: 'us', baseUrl: 'https://us.cloud.langfuse.com' },
],
destination: 'eu',
publicKey: 'pk-lf-original',
secretKeyPreview: 'sk-lf-...515f',
},
});
render(<LangfuseConnection />);
await waitFor(() => expect(mockTest).toHaveBeenCalledTimes(1));
await selectDestination('us');
expect(screen.getByText('com_ui_langfuse_status_not_verified')).toBeVisible();
await userEvent.click(
screen.getByRole('button', {
name: 'com_ui_edit com_ui_langfuse_public_key',
}),
);
fireEvent.change(screen.getByLabelText('com_ui_langfuse_public_key'), {
target: { value: 'pk-lf-edited' },
});
fireEvent.change(screen.getByLabelText(/com_ui_langfuse_secret_key/), {
target: { value: 'sk-lf-edited' },
});
mockTest.mockImplementationOnce((_payload, options) => {
options?.onSuccess?.({ success: true });
});
await userEvent.click(screen.getByRole('button', { name: 'com_ui_cancel' }));
expect(await screen.findByText('com_ui_langfuse_status_connected')).toBeVisible();
expect(mockTest.mock.calls.at(-1)?.[0]).toEqual({
destination: 'eu',
publicKey: 'pk-lf-original',
});
expect(screen.getByRole('button', { name: 'com_ui_langfuse_disable' })).toBeEnabled();
expect(screen.getByTestId('langfuse-destination')).toHaveTextContent(destinationLabels.eu);
expect(screen.getByText('pk-lf-...inal')).toBeInTheDocument();
expect(screen.getByText('sk-lf-...515f')).toBeInTheDocument();
expect(screen.queryByText('com_ui_langfuse_save_and_enable')).not.toBeInTheDocument();
expect(mockUpdate).not.toHaveBeenCalled();
});
it('shows a save failure when mandatory server verification rejects the connection', async () => {
mockUpdate.mockImplementation((_payload, options) => {
options?.onError?.();
});
render(<LangfuseConnection />);
await selectDestination('us');
fireEvent.change(screen.getByLabelText('com_ui_langfuse_public_key'), {
target: { value: 'pk-lf-1' },
});
fireEvent.change(screen.getByLabelText(/com_ui_langfuse_secret_key/), {
target: { value: 'sk-lf-secret' },
});
await userEvent.click(screen.getByText('com_ui_langfuse_save_and_enable'));
expect(mockTest).not.toHaveBeenCalled();
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(screen.getByText('com_ui_langfuse_save_error')).toBeVisible();
});
it('replaces a connected status with a failure when an edited public key is rejected', async () => {
mockGet.mockReturnValue({
data: {
configured: true,
enabled: true,
destinations: [{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' }],
destination: 'eu',
publicKey: 'pk-lf-valid',
secretKeyPreview: 'sk-lf-...515f',
},
});
render(<LangfuseConnection />);
await waitFor(() => expect(screen.getByText('com_ui_langfuse_status_connected')).toBeVisible());
mockTest.mockClear();
mockUpdate.mockImplementation((_payload, options) => {
options?.onError?.();
});
await userEvent.click(
screen.getByRole('button', {
name: 'com_ui_edit com_ui_langfuse_public_key',
}),
);
fireEvent.change(screen.getByLabelText('com_ui_langfuse_public_key'), {
target: { value: 'pk-lf-mangled' },
});
expect(screen.getByText('com_ui_langfuse_status_not_verified')).toBeVisible();
fireEvent.change(screen.getByLabelText(/com_ui_langfuse_secret_key/), {
target: { value: 'sk-lf-replacement' },
});
await userEvent.click(screen.getByText('com_ui_langfuse_save_and_enable'));
expect(mockTest).not.toHaveBeenCalled();
expect(mockUpdate).toHaveBeenCalledWith(
expect.objectContaining({
publicKey: 'pk-lf-mangled',
secretKey: 'sk-lf-replacement',
}),
expect.any(Object),
);
expect(screen.getByText('com_ui_langfuse_save_error')).toBeVisible();
});
it('saves immediately without testing when disabling a configured connection', async () => {
mockGet.mockReturnValue({
data: {
configured: true,
enabled: true,
destinations: [{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' }],
destination: 'eu',
publicKey: 'pk-lf-1',
secretKeyPreview: 'sk-lf-...515f',
},
});
render(<LangfuseConnection />);
await waitFor(() => expect(mockTest).toHaveBeenCalledTimes(1));
mockTest.mockClear();
mockUpdate.mockImplementation((_payload, options) => {
options?.onSuccess?.({
configured: true,
enabled: false,
destinations: [{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' }],
destination: 'eu',
publicKey: 'pk-lf-1',
secretKeyPreview: 'sk-lf-...515f',
updatedAt: '2026-07-10T15:30:00.000Z',
});
});
await userEvent.click(screen.getByRole('button', { name: 'com_ui_langfuse_disable' }));
expect(mockTest).not.toHaveBeenCalled();
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(mockUpdate.mock.calls[0][0]).toMatchObject({
enabled: false,
destination: 'eu',
publicKey: 'pk-lf-1',
});
expect(screen.queryByText('com_ui_langfuse_save_and_enable')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'com_ui_langfuse_enable' })).toBeEnabled();
});
it('allows disabling a connection whose saved destination was removed', async () => {
mockGet.mockReturnValue({
data: {
configured: true,
enabled: true,
destinations: [{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' }],
destination: 'removed-destination',
publicKey: 'pk-lf-1',
secretKeyPreview: 'sk-lf-...515f',
},
});
render(<LangfuseConnection />);
expect(screen.getByTestId('langfuse-destination')).toHaveTextContent(
'removed-destination - com_ui_langfuse_destination_unavailable',
);
expect(screen.getByText('com_ui_langfuse_destination_removed')).toBeVisible();
expect(mockTest).not.toHaveBeenCalled();
await userEvent.click(screen.getByRole('button', { name: 'com_ui_langfuse_disable' }));
expect(mockUpdate).toHaveBeenCalledWith(
{
enabled: false,
destination: 'removed-destination',
publicKey: 'pk-lf-1',
},
expect.any(Object),
);
});
it('saves immediately without testing when enabling a configured connection', async () => {
mockGet.mockReturnValue({
data: {
configured: true,
enabled: false,
destinations: [{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' }],
destination: 'eu',
publicKey: 'pk-lf-1',
secretKeyPreview: 'sk-lf-...515f',
},
});
mockUpdate.mockImplementation((_payload, options) => {
options?.onSuccess?.({
configured: true,
enabled: true,
destinations: [{ key: 'eu', baseUrl: 'https://cloud.langfuse.com' }],
destination: 'eu',
publicKey: 'pk-lf-1',
secretKeyPreview: 'sk-lf-...515f',
updatedAt: '2026-07-10T15:31:00.000Z',
});
});
render(<LangfuseConnection />);
await waitFor(() => expect(mockTest).toHaveBeenCalledTimes(1));
mockTest.mockClear();
await userEvent.click(screen.getByRole('button', { name: 'com_ui_langfuse_enable' }));
expect(mockTest).not.toHaveBeenCalled();
expect(mockUpdate).toHaveBeenCalledWith(
{ enabled: true, destination: 'eu', publicKey: 'pk-lf-1' },
expect.any(Object),
);
expect(screen.queryByText('com_ui_langfuse_save_and_enable')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'com_ui_langfuse_disable' })).toBeEnabled();
});
});

View file

@ -0,0 +1,45 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { dataService, QueryKeys, MutationKeys } from 'librechat-data-provider';
import type {
TLangfuseConnectionStatus,
TUpdateLangfuseConnectionRequest,
TLangfuseConnectionTestRequest,
TLangfuseConnectionTestResponse,
} from 'librechat-data-provider';
import type { UseQueryResult, UseMutationResult } from '@tanstack/react-query';
export const useGetLangfuseConnectionQuery = (
enabled = true,
): UseQueryResult<TLangfuseConnectionStatus> =>
useQuery<TLangfuseConnectionStatus>(
[QueryKeys.langfuseConnection],
() => dataService.getLangfuseConnection(),
{ enabled, refetchOnWindowFocus: false },
);
export const useUpdateLangfuseConnectionMutation = (): UseMutationResult<
TLangfuseConnectionStatus,
unknown,
TUpdateLangfuseConnectionRequest
> => {
const queryClient = useQueryClient();
return useMutation(
(payload: TUpdateLangfuseConnectionRequest) => dataService.updateLangfuseConnection(payload),
{
mutationKey: [MutationKeys.updateLangfuseConnection],
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.langfuseConnection], data);
},
},
);
};
export const useTestLangfuseConnectionMutation = (): UseMutationResult<
TLangfuseConnectionTestResponse,
unknown,
TLangfuseConnectionTestRequest
> =>
useMutation(
(payload: TLangfuseConnectionTestRequest) => dataService.testLangfuseConnection(payload),
{ mutationKey: [MutationKeys.testLangfuseConnection] },
);

View file

@ -3,6 +3,7 @@ export * from './Agents';
export * from './Endpoints';
export * from './Skills';
export * from './Files';
export * from './Langfuse';
/* Memories */
export * from './Memories';
export * from './Messages';

View file

@ -1730,6 +1730,36 @@
"com_ui_settings_search_placeholder": "Search settings",
"com_ui_settings_section_accessibility": "Accessibility",
"com_ui_settings_section_api_keys": "API keys",
"com_ui_langfuse_title": "Langfuse connection",
"com_ui_langfuse_description": "Send this organization's traces and feedback scores to your own Langfuse project.",
"com_ui_langfuse_beta_info": "This feature is in beta. Enabling this connection will send traces from all agents in your org to Langfuse.",
"com_ui_langfuse_destination": "Destination",
"com_ui_langfuse_destination_unavailable": "Unavailable",
"com_ui_langfuse_destination_removed": "The saved Langfuse destination is no longer available. Disable the connection or select another destination.",
"com_ui_langfuse_public_key": "Public key",
"com_ui_langfuse_secret_key": "Secret key",
"com_ui_langfuse_status_checking": "Checking connection",
"com_ui_langfuse_status_connected": "Verified with Langfuse",
"com_ui_langfuse_status_failed": "Connection failed",
"com_ui_langfuse_status_failed_hover": "Check Langfuse to see if traces are still failing. A one-time ping with the keys just failed.",
"com_ui_langfuse_status_not_configured": "Not configured",
"com_ui_langfuse_status_not_verified": "Not verified",
"com_ui_langfuse_testing": "Testing connection",
"com_ui_langfuse_save_and_enable": "Save & enable",
"com_ui_langfuse_enable": "Enable",
"com_ui_langfuse_disable": "Disable",
"com_ui_langfuse_saved": "Langfuse connection saved",
"com_ui_langfuse_load_error": "Failed to load the Langfuse connection",
"com_ui_langfuse_save_error": "Failed to save the Langfuse connection",
"com_ui_langfuse_test_error": "Could not connect to Langfuse",
"com_ui_langfuse_test_invalid_credentials": "Langfuse rejected these keys. Check the destination and keys",
"com_ui_langfuse_test_access_denied": "Langfuse denied access. Check the API key type and project status.",
"com_ui_langfuse_test_rate_limited": "Langfuse is rate limiting verification. Try again later.",
"com_ui_langfuse_test_server_error": "Langfuse is returning server errors. This may be a Langfuse incident.",
"com_ui_langfuse_test_timeout": "Langfuse verification timed out",
"com_ui_langfuse_test_missing_secret": "A secret key is required to test the connection",
"com_ui_langfuse_test_stored_secret_unavailable": "The stored secret key could not be used",
"com_ui_langfuse_test_unexpected_response": "Langfuse returned an unexpected response",
"com_ui_settings_section_appearance": "Appearance",
"com_ui_settings_section_billing": "Billing",
"com_ui_settings_section_commands": "Commands",
@ -1745,6 +1775,8 @@
"com_ui_settings_section_sending": "Sending",
"com_ui_settings_section_stt": "Speech to text",
"com_ui_settings_section_tts": "Text to speech",
"com_ui_settings_section_langfuse": "Langfuse",
"com_ui_settings_tab_langfuse": "Langfuse",
"com_ui_settings_tab_data": "Data & Privacy",
"com_ui_share": "Share",
"com_ui_share_create_message": "Your name and any messages you add after sharing stay private.",