From 8b983eca8c9d31625278d95c4e747ef164dac339 Mon Sep 17 00:00:00 2001 From: mameikagou Date: Wed, 26 Aug 2026 02:38:36 +0800 Subject: [PATCH] fix(monitoring): handle SQLite token statistics errors --- src/langbot/pkg/persistence/tenant_uow.py | 2 +- .../unit_tests/persistence/test_tenant_uow.py | 1 + .../monitoring/components/TokenMonitoring.tsx | 10 ++++++- .../unit/token-monitoring-error.test.mjs | 26 +++++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 web/tests/unit/token-monitoring-error.test.mjs diff --git a/src/langbot/pkg/persistence/tenant_uow.py b/src/langbot/pkg/persistence/tenant_uow.py index 0ec114a8f6..0d21560abb 100644 --- a/src/langbot/pkg/persistence/tenant_uow.py +++ b/src/langbot/pkg/persistence/tenant_uow.py @@ -209,7 +209,7 @@ class _ScopedSessionGuardState: 'now': sqlalchemy.sql.functions.now, 'sum': sqlalchemy.sql.functions.sum, } -_ALLOWED_SCOPED_GENERIC_FUNCTIONS = frozenset({'date_trunc', 'length', 'nullif'}) +_ALLOWED_SCOPED_GENERIC_FUNCTIONS = frozenset({'date_trunc', 'length', 'nullif', 'strftime'}) _ALLOWED_SCOPED_CUSTOM_OPERATORS = frozenset({'<=>'}) _ALLOWED_SCOPED_STATEMENT_TYPES = ( sqlalchemy.sql.dml.UpdateBase, diff --git a/tests/unit_tests/persistence/test_tenant_uow.py b/tests/unit_tests/persistence/test_tenant_uow.py index 34e907095b..b9d3b1350f 100644 --- a/tests/unit_tests/persistence/test_tenant_uow.py +++ b/tests/unit_tests/persistence/test_tenant_uow.py @@ -964,6 +964,7 @@ async def test_scoped_session_rejects_raw_or_unapproved_sql( sa.func.date_trunc('hour', sa.column('timestamp')), sa.func.length(sa.literal('value')), sa.func.nullif(sa.literal('value'), sa.literal('')), + sa.func.strftime('%Y-%m-%d %H:00', sa.column('timestamp')), ), sa.select(sa.column('embedding').op('<=>')(sa.literal([0.1]))), sa.select(sa.cast(sa.column('embedding'), Vector(384))), diff --git a/web/src/app/home/monitoring/components/TokenMonitoring.tsx b/web/src/app/home/monitoring/components/TokenMonitoring.tsx index 129f5f713c..bfc254cf31 100644 --- a/web/src/app/home/monitoring/components/TokenMonitoring.tsx +++ b/web/src/app/home/monitoring/components/TokenMonitoring.tsx @@ -77,6 +77,14 @@ function formatNumber(n: number): string { return n.toLocaleString(); } +function getErrorMessage(error: unknown): string { + if (error instanceof Error) return error.message; + if (typeof error === 'object' && error !== null && 'msg' in error) { + return String((error as { msg?: unknown }).msg ?? ''); + } + return String(error); +} + const TOOLTIP_STYLE: React.CSSProperties = { backgroundColor: 'var(--card)', border: '1px solid var(--border)', @@ -152,7 +160,7 @@ export default function TokenMonitoring({ }); setStats(result); } catch (e) { - setError(e instanceof Error ? e.message : String(e)); + setError(getErrorMessage(e)); } finally { setLoading(false); } diff --git a/web/tests/unit/token-monitoring-error.test.mjs b/web/tests/unit/token-monitoring-error.test.mjs new file mode 100644 index 0000000000..262b4c78ca --- /dev/null +++ b/web/tests/unit/token-monitoring-error.test.mjs @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const source = fs.readFileSync( + path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../src/app/home/monitoring/components/TokenMonitoring.tsx', + ), + 'utf8', +); + +test('TokenMonitoring prefers the API error message for plain error objects', () => { + const helperStart = source.indexOf('function getErrorMessage'); + const helperEnd = source.indexOf('\n}\n', helperStart) + 2; + const helper = source.slice(helperStart, helperEnd); + + assert.ok(helperStart >= 0, 'TokenMonitoring error helper is missing'); + assert.match(helper, /error instanceof Error/); + assert.match(helper, /typeof error === 'object'/); + assert.match(helper, /'msg' in error/); + assert.match(helper, /error as \{ msg\?: unknown \}/); + assert.match(source, /setError\(getErrorMessage\(e\)\)/); +});