mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
📡 feat: Add Authenticated Proxy Mode for Browser RUM Telemetry (#13464)
This commit is contained in:
parent
88e5a2f23b
commit
a86e504a57
12 changed files with 511 additions and 22 deletions
|
|
@ -220,6 +220,7 @@ const startServer = async () => {
|
|||
|
||||
app.use('/api/tags', routes.tags);
|
||||
app.use('/api/mcp', routes.mcp);
|
||||
app.use('/api/rum', routes.rum);
|
||||
|
||||
app.use('/metrics', metricsRouter);
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ afterEach(() => {
|
|||
delete process.env.RUM_ENABLED;
|
||||
delete process.env.RUM_PROVIDER;
|
||||
delete process.env.RUM_URL;
|
||||
delete process.env.RUM_PROXY_TARGET_URL;
|
||||
delete process.env.RUM_SERVICE_NAME;
|
||||
delete process.env.RUM_AUTH_MODE;
|
||||
delete process.env.RUM_PUBLIC_TOKEN;
|
||||
|
|
@ -111,6 +112,38 @@ describe('GET /api/config RUM config', () => {
|
|||
expect(response.body).not.toHaveProperty('rum');
|
||||
});
|
||||
|
||||
it('includes proxy RUM config when enabled with valid env', async () => {
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
process.env.RUM_ENABLED = 'true';
|
||||
process.env.RUM_AUTH_MODE = 'proxy';
|
||||
process.env.RUM_PROXY_TARGET_URL = 'http://otel-collector:4318';
|
||||
const app = createApp(mockUser);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body.rum).toEqual({
|
||||
provider: 'hyperdx',
|
||||
enabled: true,
|
||||
url: '/api/rum',
|
||||
serviceName: 'librechat-web',
|
||||
authMode: 'proxy',
|
||||
consoleCapture: false,
|
||||
disableReplay: true,
|
||||
advancedNetworkCapture: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('omits proxy RUM config without a target collector URL', async () => {
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
process.env.RUM_ENABLED = 'true';
|
||||
process.env.RUM_AUTH_MODE = 'proxy';
|
||||
const app = createApp(mockUser);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body).not.toHaveProperty('rum');
|
||||
});
|
||||
|
||||
it('omits RUM config when the URL contains credentials', async () => {
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
process.env.RUM_ENABLED = 'true';
|
||||
|
|
|
|||
|
|
@ -32,8 +32,10 @@ const auth = require('./auth');
|
|||
const keys = require('./keys');
|
||||
const user = require('./user');
|
||||
const mcp = require('./mcp');
|
||||
const rum = require('./rum');
|
||||
|
||||
module.exports = {
|
||||
rum,
|
||||
mcp,
|
||||
auth,
|
||||
adminAuth,
|
||||
|
|
|
|||
22
api/server/routes/rum.js
Normal file
22
api/server/routes/rum.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const express = require('express');
|
||||
const { getRumProxyBodyLimit, isRumProxyEnabled, proxyRumRequest } = require('@librechat/api');
|
||||
const { requireJwtAuth } = require('~/server/middleware');
|
||||
|
||||
const router = express.Router();
|
||||
const rawOtlpBody = express.raw({
|
||||
limit: getRumProxyBodyLimit(),
|
||||
type: ['application/x-protobuf', 'application/octet-stream'],
|
||||
});
|
||||
|
||||
function requireRumProxyEnabled(_req, res, next) {
|
||||
if (!isRumProxyEnabled()) {
|
||||
return res.status(404).json({ message: 'RUM proxy is not configured' });
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
router.post('/v1/traces', requireRumProxyEnabled, requireJwtAuth, rawOtlpBody, proxyRumRequest);
|
||||
router.post('/v1/logs', requireRumProxyEnabled, requireJwtAuth, rawOtlpBody, proxyRumRequest);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
const { isEnabled } = require('@librechat/api');
|
||||
const { getRumProxyClientUrl, isEnabled, isRumProxyEnabled } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
|
||||
const DEFAULT_RUM_SERVICE_NAME = 'librechat-web';
|
||||
|
|
@ -80,22 +80,34 @@ function getRumConfig() {
|
|||
}
|
||||
|
||||
const authMode = process.env.RUM_AUTH_MODE || 'publicToken';
|
||||
if (authMode !== 'publicToken') {
|
||||
if (authMode !== 'publicToken' && authMode !== 'proxy') {
|
||||
logger.warn(`[config] Unsupported RUM auth mode "${authMode}", disabling RUM`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rumUrl = process.env.RUM_URL;
|
||||
const parsedUrl = rumUrl ? parseUrl(rumUrl) : undefined;
|
||||
let rumUrl;
|
||||
if (authMode === 'proxy') {
|
||||
rumUrl = getRumProxyClientUrl();
|
||||
|
||||
if (!parsedUrl || !isSafeRumUrl(parsedUrl)) {
|
||||
logger.warn('[config] Invalid RUM_URL, disabling RUM');
|
||||
return undefined;
|
||||
}
|
||||
if (!isRumProxyEnabled()) {
|
||||
logger.warn('[config] RUM proxy mode requires RUM_PROXY_TARGET_URL, disabling RUM');
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
rumUrl = process.env.RUM_URL;
|
||||
const parsedUrl = rumUrl ? parseUrl(rumUrl) : undefined;
|
||||
|
||||
if (!process.env.RUM_PUBLIC_TOKEN) {
|
||||
logger.warn('[config] RUM publicToken mode requires RUM_PUBLIC_TOKEN, disabling RUM');
|
||||
return undefined;
|
||||
if (!parsedUrl || !isSafeRumUrl(parsedUrl)) {
|
||||
logger.warn('[config] Invalid RUM_URL, disabling RUM');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!process.env.RUM_PUBLIC_TOKEN) {
|
||||
logger.warn('[config] RUM publicToken mode requires RUM_PUBLIC_TOKEN, disabling RUM');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
rumUrl = parsedUrl.href.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
const rawTracePropagationTargets = parseCsvEnv(process.env.RUM_TRACE_PROPAGATION_TARGETS);
|
||||
|
|
@ -123,10 +135,10 @@ function getRumConfig() {
|
|||
return {
|
||||
provider: 'hyperdx',
|
||||
enabled: true,
|
||||
url: parsedUrl.href.replace(/\/$/, ''),
|
||||
url: rumUrl,
|
||||
serviceName: process.env.RUM_SERVICE_NAME || DEFAULT_RUM_SERVICE_NAME,
|
||||
authMode,
|
||||
publicToken: process.env.RUM_PUBLIC_TOKEN,
|
||||
...(authMode === 'publicToken' ? { publicToken: process.env.RUM_PUBLIC_TOKEN } : {}),
|
||||
...(tracePropagationTargets.length > 0 ? { tracePropagationTargets } : {}),
|
||||
consoleCapture,
|
||||
disableReplay: parseBooleanEnv(process.env.RUM_DISABLE_REPLAY, true),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue