💡 feat: add DB-backed admin insights (#14898)

* feat: add Mongo-backed admin insights

* feat: gate insights with environment variable

* fix: tighten insights access and activity metrics

* fix: preserve insights date selections

* perf: parallelize insights search aggregation

* test: wait for MCP conflict recovery

* test: satisfy strict MCP recovery typing

* fix: disable insights pagination while loading

* fix: localize insights range shortcuts

* fix: bound insights search input
This commit is contained in:
Ravi Kumar L 2026-08-18 13:51:51 +02:00 committed by GitHub
parent 389cfebea1
commit 006e421cd2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 3447 additions and 51 deletions

View file

@ -456,6 +456,7 @@ if (cluster.isMaster) {
/** Routes */
app.use('/oauth', preAuthTenantMiddleware, routes.oauth);
app.use('/api/auth', preAuthTenantMiddleware, routes.auth);
app.use('/api/admin/insights', routes.insights);
app.use('/api/admin', routes.adminAuth);
app.use('/api/admin/skills', routes.adminSkills);
app.use('/api/actions', routes.actions);

View file

@ -308,6 +308,7 @@ const startServer = async () => {
app.use('/oauth', preAuthTenantMiddleware, routes.oauth);
/* API Endpoints */
app.use('/api/auth', preAuthTenantMiddleware, routes.auth);
app.use('/api/admin/insights', routes.insights);
app.use('/api/admin', routes.adminAuth);
app.use('/api/admin/config', routes.adminConfig);
app.use('/api/admin/langfuse', routes.adminLangfuse);

View file

@ -104,6 +104,7 @@ afterEach(() => {
delete process.env.SAML_SESSION_SECRET;
delete process.env.ALLOW_ACCOUNT_DELETION;
delete process.env.ADMIN_PANEL_URL;
delete process.env.ENABLE_INSIGHTS;
delete process.env.ANALYTICS_GTM_ID;
delete process.env.CUSTOM_FOOTER;
delete process.env.HELP_AND_FAQ_URL;
@ -174,6 +175,7 @@ describe('GET /api/config', () => {
expect(response.body).not.toHaveProperty('sharePointPickerGraphScope');
expect(response.body).not.toHaveProperty('sharePointPickerSharePointScope');
expect(response.body).not.toHaveProperty('conversationImportMaxFileSize');
expect(response.body).not.toHaveProperty('insightsEnabled');
});
it('should strip authenticated-only informational fields from unauthenticated response (#12688)', async () => {
@ -398,6 +400,18 @@ describe('GET /api/config', () => {
expect(response.body.conversationImportMaxFileSize).toBe(5000000);
});
it('should advertise Insights only when ENABLE_INSIGHTS is enabled', async () => {
mockGetAppConfig.mockResolvedValue(baseAppConfig);
const app = createApp(mockUser);
let response = await request(app).get('/api/config');
expect(response.body.insightsEnabled).toBe(false);
process.env.ENABLE_INSIGHTS = 'true';
response = await request(app).get('/api/config');
expect(response.body.insightsEnabled).toBe(true);
});
it('should advertise Langfuse fanout only when the toggle and collector URL are configured', async () => {
mockGetAppConfig.mockResolvedValue(baseAppConfig);
mockHasCapability.mockResolvedValue(true);

View file

@ -0,0 +1,93 @@
const express = require('express');
const request = require('supertest');
const mockCreateInsightsAccessHandler = jest.fn(() => (_req, res) => res.json({ access: true }));
const mockCreateInsightsHandler = jest.fn(() => (_req, res) => res.json({ summary: {} }));
const mockGrantedCapabilities = new Set(['access:admin', 'read:insights']);
const mockGetInsights = jest.fn();
let mockUser = { id: 'admin-id', role: 'ADMIN' };
jest.mock('@librechat/api', () => ({
createInsightsAccessHandler: (...args) => mockCreateInsightsAccessHandler(...args),
createInsightsHandler: (...args) => mockCreateInsightsHandler(...args),
isEnabled: (value) => value === 'true',
}));
jest.mock('@librechat/data-schemas', () => ({
SystemCapabilities: {
ACCESS_ADMIN: 'access:admin',
READ_INSIGHTS: 'read:insights',
},
}));
jest.mock('~/server/middleware', () => ({
requireJwtAuth: (req, _res, next) => {
req.user = mockUser;
next();
},
checkAdmin: (req, res, next) => {
if (req.user.role !== 'ADMIN') {
return res.status(403).json({ message: 'Forbidden' });
}
next();
},
}));
jest.mock('~/server/middleware/roles/capabilities', () => ({
requireCapability: (capability) => (_req, res, next) => {
if (!mockGrantedCapabilities.has(capability)) {
return res.status(403).json({ message: 'Forbidden' });
}
next();
},
}));
jest.mock('~/models', () => ({
getInsights: (...args) => mockGetInsights(...args),
}));
const insightsRouter = require('../insights');
function createApp() {
const app = express();
app.use('/api/admin/insights', insightsRouter);
return app;
}
describe('Insights routes', () => {
beforeEach(() => {
mockUser = { id: 'admin-id', role: 'ADMIN' };
mockGrantedCapabilities.clear();
mockGrantedCapabilities.add('access:admin');
mockGrantedCapabilities.add('read:insights');
});
it('requires the ADMIN role', async () => {
mockUser = { id: 'delegated-admin-id', role: 'DELEGATED_ADMIN' };
const response = await request(createApp()).get('/api/admin/insights');
expect(response.status).toBe(403);
});
it('serves the access probe and dashboard', async () => {
const app = createApp();
await expect(request(app).get('/api/admin/insights/access')).resolves.toMatchObject({
status: 200,
body: { access: true },
});
await expect(request(app).get('/api/admin/insights')).resolves.toMatchObject({
status: 200,
body: { summary: {} },
});
});
it.each(['access:admin', 'read:insights'])('requires %s', async (capability) => {
mockGrantedCapabilities.delete(capability);
const response = await request(createApp()).get('/api/admin/insights');
expect(response.status).toBe(403);
});
});

View file

@ -304,6 +304,7 @@ router.get('/', async function (req, res) {
: 0,
langfuseFanoutEnabled,
langfuseConnectionAccess,
insightsEnabled: isEnabled(process.env.ENABLE_INSIGHTS),
...(cloudFront ? { cloudFront } : {}),
...(rum ? { rum } : {}),
fileUploadSseEnabled: isEnabled(process.env.FILE_UPLOAD_SSE_ENABLED),

View file

@ -37,8 +37,10 @@ const keys = require('./keys');
const user = require('./user');
const mcp = require('./mcp');
const rum = require('./rum');
const insights = require('./insights');
module.exports = {
insights,
rum,
mcp,
auth,

View file

@ -0,0 +1,17 @@
const express = require('express');
const { createInsightsAccessHandler, createInsightsHandler, isEnabled } = require('@librechat/api');
const { SystemCapabilities } = require('@librechat/data-schemas');
const { requireJwtAuth, checkAdmin } = require('~/server/middleware');
const { requireCapability } = require('~/server/middleware/roles/capabilities');
const db = require('~/models');
const router = express.Router();
const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);
const requireInsightsAccess = requireCapability(SystemCapabilities.READ_INSIGHTS);
const isInsightsEnabled = () => isEnabled(process.env.ENABLE_INSIGHTS);
router.use(requireJwtAuth, checkAdmin, requireAdminAccess, requireInsightsAccess);
router.get('/access', createInsightsAccessHandler({ isInsightsEnabled }));
router.get('/', createInsightsHandler({ isInsightsEnabled, getInsights: db.getInsights }));
module.exports = router;